2023高薪前端面试题(一、前端基础——JavaScript)

说一下闭包

参考回答:
一句话可以概括:闭包就是能够读取其他函数内部变量的函数,或者子函数在外调
用,子函数所在的父函数的作用域不会被释放。

闭包是一个封闭的空间,里面存储了在其他地方会引用到的该作用域的值,在 JavaScript 中是通过作用域链来实现的闭包

只要在函数中使用了外部的数据,就创建了闭包,这种情况下所创建的闭包,我们在编码时是不需要去关心的。

我们还可以通过一些手段手动创建闭包,从而让外部环境访问到函数内部的局部变量,让局部变量持续保存下来,不随着它的上下文环境一起销毁。

使用闭包可以解决一个全局变量污染的问题。

如果是自动产生的闭包,我们无需操心闭包的销毁,而如果是手动创建的闭包,可以把被引用的变量设置为 null,即手动清除变量,这样下次 JavaScript 垃圾回收器在进行垃圾回收时,发现此变量已经没有任何引用了,就会把设为 null 的量给回收了。

面试题-闭包_闭包面试题_汪补补的博客-CSDN博客

什么是闭包?以及闭包的作用_fuyuyuki的博客-CSDN博客

1、this指向规则

(1)没有调用者,默认指向全局window,例如 

fn();

(2)有调用者则指向调用者,例如

obj.fn();

(3)严格模式下,undefined

2、作用域

JS作用域

作用域是可访问变量、对象、函数的集合。

(1)函数限定变量作用域

在javascript中,函数里面定义的变量,可以在函数里面被访问,但在函数外无法访问(局部作用域)

在ES5中,只有全局作用域和函数作用域,并没有块作用域(let和const);

(2)ES6作用域

作用域链:

先看函数里边有没有定义某个变量,没有就往外找(由内往外找)

变量提升:指的是变量的声明会提前

3、预解析

预解析做什么事?
变量的声明提前了 ---- 提前到当前所在的作用域的最上面
函数的声明也会被提前 — 提前到当前所在域的最上面

在当前作用域中,JavaScript代码执行之前,浏览器首先会默认的把所有带var和function声明的变量进行提前的声明或者定义。

区别:

var : 提前声明,所以变量在赋值前调用是undefined

function:提前声明并定义,所以函数在声明前调用是正常输出

  • num2 = 12; 相当于给window增加了一个num2的属性名,属性值是12;
  • var num1 = 12; 相当于给全局作用域增加了一个全局变量num1,但是不仅如此,它也相当于给window增加了一个属性名num,属性值是12;

JavaScript中的预解析 - 简书

ES6 闭包相关面试题 - 简书

4、全局变量和局部变量同名的坑

全局变量是不会做用于同名局部变量的作用域的。

前端闭包相关面试题

问题1:什么是闭包
答:闭包就是能够读取其他函数内部变量的函数,通俗的讲就是函数a的内部函数b,被 函数a外部的一个变量引用的时候,就创建了一个闭包。

问题2:闭包应用场景
答:最常见的是函数封装的时候,再就是在使用定时器的时候,
即:当我们需要在模块中定义一些变量,并希望这些变量一直保存在内存中但又不会 “污染” 全局的变量时,就可以用闭包来定义这个模块。
它的最大用处有 两个,一个是它可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中

问题3:简单写一个闭包
答:

function a() {
    var i=0;
    function b() {
        alert(++i);
    }
    return b;
}

var c = a();
c();//外部的变量



问题4:闭包的优缺点
优点:
① 减少全局变量;
② 减少传递函数的参数量;
③ 封装;

缺点:
① 使用闭包会占有内存资源,过多的使用闭包会导致内存溢出等
问题5:内存泄漏,解决方法
答:简单的说就是把那些不需要的变量,但是垃圾回收又收不走的的那些赋值为null,然后让垃圾回收走


问题6:如何使用
答:
1.定义外层函数,封装被保护的局部变量。
2.定义内层函数,执行对外部函数变量的操作。
3.外层函数返回内层函数的对象,并且外层函数被调用,结果保存在一个全局的变量中。


什么是 js 的闭包?有什么作用,用闭包写个单例模式

参考回答:

1、什么是闭包?

MDN 对闭包的定义是:闭包是指那些能够访问自由变量的函数,自由变量是指在函数中使用的,但既不是函数参数又不是函数的局部变量的变量,由此可以看出,

闭包=函数+函数能够访问的自由变量

所以从技术的角度讲,所有 JS 函数都是闭包,但是这是理论上的闭包,还有一个实践角度上的闭包,从实践角度上来说,只有满足

(1)即使创建它的上下文已经销毁,它仍然存在;

(2)在代码中引入了自由变量,才称为闭包。

2、闭包的应用:

  • 模仿块级作用域。
  • 保存外部函数的变量。
  • 封装私有变量。

3、单例模式:

单例模式是只允许实例化一次的对象类。

创建思路:

  • 创建一个闭包函数
  • 在闭包函数中创建保存实例的变量 instance,并赋值 null
  • 编写单例函数
  • 在闭包函数中返回单例的实例变量
  • 返回前先判断instance是否为空,为空则先创建再返回
var Singleton = (function(){
	var instance;
	var CreateSingleton = function (name) {
		this.name = name;
		if(instance) {
			return instance;
		}
	    this.getName(); // 打印实例名字
	    return instance = this; //instance = this; return instance;
	}
	CreateSingleton.prototype.getName = function() { // 获取实例的名字
		console.log(this.name)
	}
	return CreateSingleton;
})();
// 创建实例对象 1
var a = new Singleton('a');
// 创建实例对象 2
var b = new Singleton('b');
console.log(a===b);

JavaScript设计模式-单例模式_铛铛铛铛Huan的博客-CSDN博客


pink老师讲闭包

闭包容易引起歧义的地方

1、闭包一定有return吗?

答:不是。

解析:(1)简单理解:闭包 = 内层函数 + 引用的外层函数变量

先看个简单代码:

 closure就是闭包

//普通闭包
function outer () {
	const a = 1;
	function f () {
		console.log(a);
	}
	f();
}
outer();

(2)外部如果想要使用闭包里面的变量,则此时需要return

// 含有return的闭包
function outer () {
	const a = 10;
	return function () {
		console.log(a);
	}
}
const fn = outer();
fn()

2、闭包一定会有内存泄漏吗?

答:不是。

  

解析:谁会存在内存泄漏? count变量

借助于垃圾回收机制的标记清除法可以看出:

  • result是一个全局变量,代码执行完毕也不会立即销毁;
  • result使用fn函数;
  • fn用到fun函数;
  • fun函数里面用到count;
  • count被引用就不会回收,所以一直存在;
  • 此时,闭包引起了内存泄漏。

注意:

(1)不是所有的内存泄漏都要回收的;比如react里面很多闭包不能回收。

3、闭包应用:实现数据的私有,即变量私有化,防止全局变量被污染,外面的人可以用,但无法直接修改

// 比如,统计函数调用次数
// 此时,修改count的值修改的是全局变量,会改变统计次数
let count = 1
function fn() {
	count++
	console.log(`函数被调用了${count}次`);
}
fn() //2
fn() //3

// 实现数据私有,此时外面可以使用count值,但是无法直接修改
function fn() {
	let count = 1
	function fun() {
		count++
		console.log(`函数被调用了${count}次`);
	}
	return fun
}
const result = fn()
result() //2
result() //3

没有用闭包时,count值被修改导致调用次数出错:

 用了闭包,count值不会被修改,函数可以正常调用输出:

 4、闭包场景:节流,防抖,vue3,reacthooks


闭包 有什么用

参考回答:

(1)什么是闭包:

闭包是指有权访问另外一个函数作用域中的变量的函数。
闭包就是函数的局部变量集合,只是这些局部变量在函数返回后会继续存在。闭包就
是就是函数的“堆栈”在函数返回后并不释放,我们也可以理解为这些函数堆栈并不
在栈上分配而是在堆上分配。当在一个函数内定义另外一个函数就会产生闭包。

(2)为什么要用:

  • 匿名自执行函数:我们知道所有的变量,如果不加上 var 关键字,则默认的会添加到全局对象的属性上去,这样的临时变量加入全局对象有很多坏处,比如:别的函数可能误用这些变量;造成全局对象过于庞大,影响访问速度(因为变量的取值是需要从原型链上遍历的)。除了每次使用变量都是用 var 关键字外,我们在实际情况下经常遇到这样一种情况,即有的函数只需要执行一次,其内部变量无需维护,可以用闭包。
  • 结果缓存:我们开发中会碰到很多情况,设想我们有一个处理过程很耗时的函数对象,每次调用都会花费很长时间,那么我们就需要将计算出来的值存储起来,当调用这个函数的时候,首先在缓存中查找,如果找不到,则进行计算,然后更新缓存并返回值,如果找到了,直接返回查找到的值即可。闭包正是可以做到这一点,因为它不会释放外部的引用,从而函数内部的值可以得以保留。
  • 封装:实现类和继承等。

引用类型常见的对象

参考回答:
Object、Array、RegExp、Date、Function、特殊的基本包装类型(String、Number、
Boolean)以及单体内置对象(Global、Math)等

es6 的常用

参考回答:
promise,await/async,let、const、块级作用域、箭头函数

class

参考回答:
ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。
通过 class 关键字,可以定义类。

说说var、let、const区别

常见的作用域: 全局作用域、函数作用域、块状作用域、动态作用域。

块级作用域:大部分包含{}的都可以构成块级作用域,但是函数和对象不构成块级作用域。

作用域链:内层作用域->外层作用域->…->全局作用域

对象类型
global/window全局作用域
function函数作用域(局部作用域)
{ }块级作用域(ES6新增)
this动态作用域

var、let、const区别

varlet(块级作用域)const
ES5ES6
重复声明

允许 

var a = 1;

var a = 2;

console.log(a); // 2

不允许

let a = 1;

let a = 2;

console.log(a); // error

不允许

const a = 1;

const a = 2;

console.log(a); // error

变量提升var会提升变量的声明到作用域的顶部
暂时性死区

声明前不可用,函数内部和外部同时声明一个变量,以内部为准

作用域函数作用域块级作用域

widow对象的属性和方法

(全局作用域中)

全局作用域中,var声明的变量和函数,自动变window对象的变量全局作用域中,let和const声明的变量和函数不会自动变为window的变量和对象

1. var声明变量存在变量提升,let和const不存在
变量提升

2. let和const只能在块作用域里访问

3. 同一作用域下let和const不能声明同名变量,而var可以

4. const定义常量,而且不能修改,但是在定义的对象时对象属性值可以改变

总结:

  1. var定义的变量,变量提升,没有块的概念,可以跨块访问。
  2. let定义的变量,只能在块作用域里访问,不能声明同名变量。
  3. const用来定义常量,使用时必须初始化(即必须赋值),不能声明同名变量,只能在块作用域里访问,而且不能修改,但是在定义的对象时对象属性值可以改变。
  4. 他们都不能跨函数访问

我的JavaScript笔记—— 一、var、let、const的区别?_admin_zlj的博客-CSDN博客


let和const区别(值是否可修改)

相同点:

① 只在声明所在的块级作用域内有效。

② 不提升,同时存在暂时性死区,只能在声明的位置后面使用。

③ 不可重复声明。

不同点:

① let声明的变量可以改变,值和类型都可以改变;

        const声明的常量不可以改变,这意味着,const一旦声明,就必须立即初始化,不能以后再赋值。

打个比方说:
let x = 3;
x = 4;
这样子是可以的,因为let声明的变量是可以修改或者重新定义的。

再比如:
const x = 4;
x = 5;
这是错误的,因为const声明的变量是不可修改并且必须马上赋初值。


let和var区别

var与 let的区别:

var:函数作用域;存在变量提升;可重复定义;声明的变量会作为window的属性。

let:块级作用域;不存在变量提升(有暂时性死区);不可重复定义;声明的变量不会作为window的属性。

块级作用域:即在{}花括号内的域,由{ }包括,比如if{}块、for(){}块。

函数作用域:变量在声明它们的函数体以及这个函数体嵌套的任意函数体都是有定义的。

暂时性死区:在代码块中,在声明变量之前,该变量是不可用的。

JS中作用域有:全局作用域、函数作用域,是没有块作用域的。块级作用域是ES6中的。

var是js的,作用域是函数作用域的,let是ES6的,作用域是块级作用域的。

for循环推荐用let,因为

var是函数作用域,let是块作用域。

在函数中声明了var,整个函数内都是有效的,比如说在for循环内定义的一个var变量,实际上其在for循环以外也是可以访问的

而let由于是块作用域,所以如果在块作用域内定义的变量,比如说在for循环内,在其外面是不可被访问的,所以for循环推荐用let


ES5和ES6区别

区别:1、es6新增了箭头函数,es5没有;2、ES6中新增了块级作用域,es5没有;3、ES6引入Class概念,不再像ES5一样使用原型链实现继承;4、ES6中可以设置默认函数参数,es5不行;5、ES6中新增了promise特性。6、es6使用import导入模块,export导出;7、解构赋值。

总结:

  • 解决原有语法上的一些不足,比如let 和 const 的块级作用域
  • 对原有语法进行增强,比如解构、展开、参数默认值、模板字符串
  • 全新的对象、全新的方法、全新的功能,比如promise、proxy、object的assign、is
  • 全新的数据类型和数据结构,比如symbol、set、map

简单讲一讲 ES6 的一些新特性

ES6 在变量的声明和定义方面增加了 let、const 声明变量,有局部变量的概念,赋值
中有比较吸引人的结构赋值,同时 ES6 对字符串、 数组、正则、对象、函数等拓展了
一些方法,如字符串方面的模板字符串、函数方面的默认参数、对象方面属性的简洁
表达方式,ES6 也 引入了新的数据类型 symbol,新的数据结构 set 和 map,symbol 可
以通过 typeof 检测出来,为解决异步回调问题,引入了 promise 和 generator,还有
最为吸引人了实现 Class 和模块,通过 Class 可以更好的面向对象编程,使用模块加
载方便模块化编程,当然考虑到 浏览器兼容性,我们在实际开发中需要使用 babel 进
行编译
重要的特性:
块级作用域:ES5 只有全局作用域和函数作用域,块级作用域的好处是不再需要立即执
行的函数表达式,循环体中的闭包不再有问题
rest 参数:用于获取函数的多余参数,这样就不需要使用 arguments 对象了,
promise:一种异步编程的解决方案,比传统的解决方案回调函数和事件更合理强大
模块化:其模块功能主要有两个命令构成,export 和 import,export 命令用于规定模

块的对外接口,import 命令用于输入其他模块提供的功能

最全的—— ES6有哪些新特性?_es6新特性_admin_zlj的博客-CSDN博客


说一下类的创建和继承

参考回答:
JS中,类的创建方式与函数的创建方式相同,主要区别在于一般我们创建类时,类名的首字母需要大写,同时,我们可以再类的原型上添加公共属性和方法。

(1)类的创建(es5):

new 一个 function,在这个 function 的 prototype 里面增加属性和方法。
下面来创建一个 Animal 类:
// 定义一个动物类
function Animal (name) {
    // 属性
    this.name = name || 'Animal';
    this.age = 'aa';

    // 实例方法
    this.sleep = function(){
        console.log(this.name + '正在睡觉!');
    }
}

// 原型方法
Animal.prototype.eat = function(food) {
    console.log(this.name + '正在吃:' + food);
};
这样就生成了一个 Animal 类,实力化生成对象后,有方法和属性。

(2)类的继承——原型链继承

--原型链继承
function Cat(){ }
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.eat('fish'));
console.log(cat.sleep());
console.log(cat instanceof Animal); //true
console.log(cat instanceof Cat); //true
介绍:在这里我们可以看到 new 了一个空对象,这个空对象指向 Animal 并且 Cat.prototype 指向了这个空对象,这种就是基于原型链的继承。
特点:基于原型链,既是父类的实例,也是子类的实例
缺点:无法实现多继承

(3)构造继承:

使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)
function Cat(name) {
    Animal.call(this);
    this.name = name || 'Tom';
}
// 创建实例并传参
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
  •  instanceof 是 Java 的保留关键字。
  •  作用是:测试它左边的对象是否是它右边的类的实例,返回 boolean 的数据类型

特点:可以实现多继承

缺点:只能继承父类实例的属性和方法,不能继承原型上的属性和方法。

(4)实例继承和拷贝继承

实例继承:为父类实例添加新特性,作为子类实例返回
拷贝继承:拷贝父类元素上的属性和方法
上述两个实用性不强,不一一举例。

(5)组合继承:

相当于构造继承和原型链继承的组合体。通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
function Cat(name) {
    Animal.call(this);
    this.name = name || 'Tom';
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true

特点:可以继承实例属性/方法,也可以继承原型属性/方法

缺点:调用了两次父类构造函数,生成了两份实例,浪费内存

(6)寄生组合继承:

通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性
function Cat(name) {
    Animal.call(this);
    this.name = name || 'Tom';
}(function() {
// 创建一个没有实例方法的类
var Super = function(){};
Super.prototype = Animal.prototype;
//将实例作为子类的原型
Cat.prototype = new Super();
})();
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true

较为推荐


面向对象的继承方式

参考回答:

原型链继承

核心: 将父类的实例作为子类的原型
特点:
非常纯粹的继承关系,实例是子类的实例,也是父类的实例
父类新增原型方法/原型属性,子类都能访问到
简单,易于实现
缺点:
要想为子类新增属性和方法,不能放到构造器中
无法实现多继承
来自原型对象的所有属性被所有实例共享
创建子类实例时,无法向父类构造函数传参

构造继承

核心:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没
用到原型)
特点:
解决了子类实例共享父类引用属性的问题
创建子类实例时,可以向父类传递参数
可以实现多继承(call 多个父类对象)
缺点:
实例并不是父类的实例,只是子类的实例
只能继承父类的实例属性和方法,不能继承原型属性/方法
无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
实例继承
核心:为父类实例添加新特性,作为子类实例返回
特点:
不限制调用方式,不管是 new 子类()还是子类(),返回的对象具有相同的效果
缺点:
实例是父类的实例,不是子类的实例
不支持多继承

拷贝继承

特点:
支持多继承 缺点:
效率较低,内存占用高(因为要拷贝父类的属性)
组合继承
核心:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例
作为子类原型,实现函数复用
特点:
可以继承实例属性/方法,也可以继承原型属性/方法
既是子类的实例,也是父类的实例
不存在引用属性共享问题
可传参
函数可复用

寄生组合继承

核心:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例
作为子类原型,实现函数复用
参考 https://www.cnblogs.com/humin/p/4556820.html

JS 中继承实现的几种方式,

参考回答:
1、原型链继承,将父类的实例作为子类的原型,他的特点是实例是子类的实例也是父
类的实例,父类新增的原型方法/属性,子类都能够访问,并且原型链继承简单易于实
现,缺点是来自原型对象的所有属性被所有实例共享,无法实现多继承,无法向父类
构造函数传参。
2、构造继承,使用父类的构造函数来增强子类实例,即复制父类的实例属性给子类,
构造继承可以向父类传递参数,可以实现多继承,通过 call 多个父类对象。但是构造
继承只能继承父类的实例属性和方法,不能继承原型属性和方法,无法实现函数服
用,每个子类都有父类实例函数的副本,影响性能 3、实例继承,为父类实例添加新特性,作为子类实例返回,实例继承的特点是不限制
调用方法,不管是 new 子类()还是子类()返回的对象具有相同的效果,缺点是实
例是父类的实例,不是子类的实例,不支持多继承
4、拷贝继承:特点:支持多继承,缺点:效率较低,内存占用高(因为要拷贝父类的
属性)无法获取父类不可枚举的方法(不可枚举方法,不能使用 for in 访问到)
5、组合继承:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父
类实例作为子类原型,实现函数复用
6、寄生组合继承:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构
造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点

说说前端中的事件流

参考回答:
HTML 中与 javascript 交互是通过事件驱动来实现的,例如鼠标点击事件 onclick、页
面的滚动事件 onscroll 等等,可以向文档或者文档中的元素添加事件侦听器来预订事
件。想要知道这些事件是在什么时候进行调用的,就需要了解一下“事件流”的概
念。
什么是事件流:事件流描述的是从页面中接收事件的顺序,DOM2 级事件流包括下面几个
阶段。
事件捕获阶段
处于目标阶段
事件冒泡阶段
addEventListener:addEventListener 是 DOM2 级事件新增的指定事件处理程序的操
作,这个方法接收 3 个参数:要处理的事件名、作为事件处理程序的函数和一个布尔
值。最后这个布尔值参数如果是 true,表示在捕获阶段调用事件处理程序;如果是
false,表示在冒泡阶段调用事件处理程序。
IE 只支持事件冒泡。

说一下图片的懒加载和预加载

参考回答:
  • 预加载:提前加载图片,当用户需要查看时可直接从本地缓存中渲染。
  • 懒加载:懒加载的主要目的是作为服务器前端的优化,减少请求数或延迟请求数。
  • 两种技术的本质:两者的行为是相反的,一个是提前加载,一个是迟缓甚至不加载。
  • 懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。

mouseover 和 mouseenter 的区别

参考回答:
  • mouseover:当鼠标移入元素或其子元素都会触发事件,所以有一个重复触发,冒泡的过程。对应的移除事件是 mouseout
  • mouseenter:当鼠标移除元素本身(不包含元素的子元素)会触发事件,也就是不会冒泡,对应的移除事件是 mouseleave

JS 的 new 操作符做了哪些事情

参考回答:
new 操作符新建了一个空对象,这个对象原型指向构造函数的 prototype,执行构造函
数后返回这个对象。

JS 的各种位置,比如 clientHeight,scrollHeight,offsetHeight ,以及scrollTop,offsetTop,clientTop 的区别?

参考回答:
clientHeight:表示的是可视区域的高度,不包含 border 和滚动条
offsetHeight:表示可视区域的高度,包含了 border 和滚动条
scrollHeight:表示了所有区域的高度,包含了因为滚动被隐藏的部分。
clientTop:表示边框 border 的厚度,在未指定的情况下一般为 0
scrollTop:滚动后被隐藏的高度,获取对象相对于由 offsetParent 属性指定的父坐标(css 定位的元素或 body 元素)距离顶端的高度。

JS 拖拽功能的实现

参考回答:
首先是三个事件,分别是 mousedown,mousemove,mouseup
当鼠标点击按下的时候,需要一个 tag 标识此时已经按下,可以执行 mousemove 里面的具体方法。
clientX,clientY 标识的是鼠标的坐标,分别标识横坐标和纵坐标,并且我们用offsetX 和 offsetY 来表示元素的元素的初始坐标,移动的举例应该是:
鼠标移动时候的坐标-鼠标按下去时候的坐标。
也就是说定位信息为:
鼠标移动时候的坐标-鼠标按下去时候的坐标+元素初始情况下的 offetLeft.
还有一点也是原理性的东西,也就是拖拽的同时是绝对定位,我们改变的是绝对定位条件下的 left
以及 top 等等值。
补充:也可以通过 html5 的拖放(Drag 和 drop)来实现

异步加载 JS 的方法

参考回答: defer:只支持 IE 如果您的脚本不会改变文档的内容,可将 defer 属性加入到<script>标签中,以便加快处理文档的速度。因为浏览器知道它将能够安全地读取文档的剩余部分而不用执行脚本,它将推迟对脚本的解释,直到文档已经显示给用户为止。
async,HTML5 属性仅适用于外部脚本,并且如果在 IE 中,同时存在 defer 和 async,那么 defer 的优先级比较高,脚本将在页面完成时执行。
创建 script 标签,插入到 DOM 中

Ajax 解决浏览器缓存问题

参考回答:

1.浏览器缓存的表现:

       在项目中一般提交请求都会通过ajax来提交,但是发现,每次提交后得到的数据都是一样的,每次清除缓存后,就又可以得到一个新的数据。

2.浏览器缓存原因:

        ajax能提高页面载入的速度主要的原因是ajax能实现局部刷新,通过局部刷新机制减少了重复数据的载入,也就是说在载入数据的同时将数据缓存到内存中,一旦数据被加载其中,只要没有刷新页面,这些数据就会一直被缓存在内存中,当我们提交 的URL与历史的URL一致时,就不需要提交给服务器,也就是不需要从服务器上面去获取数据。那么,我们得到还是最开始缓存在浏览器中的数据。虽然降低了服务器的负载提高了用户的体验,但是我们不能获取最新的数据。为了保证我们读取的信息都是最新的,我们就需要禁止他的缓存功能。

3.解决方法:

(1)在ajax发送请求前加上 anyAjaxObj.setRequestHeader("If-Modified-Since","0")。

          原理:If-Modified-Since:0 故意让缓存过期

(2)在ajax发送请求前加上 anyAjaxObj.setRequestHeader("Cache-Control","no-cache")。 

         原理:直接禁用缓存机制

(3)在URL后面加上一个随机数: "fresh=" + Math.random();。 

         原理:强行让每次的请求地址不同

(4)在URL后面加上时间搓:"nowtime=" + new Date().getTime();。

         原理:强行让每次的请求地址不同

(5)如果是使用jQuery,直接这样就可以了$.ajaxSetup({cache:false})。

        这样页面的所有 ajax 都会执行这条语句就是不需要保存缓存记录。

         原理:不设置ajax缓存


JS 的节流和防抖

在前端日常开发中会遇到一些频繁的事件触发,比如:

1. window 的 resize、scroll
2. mousedown、mousemove
3. input、keyup、keydown

这非常影响性能,所以我们需要控制它们触发的频率,方法就是防抖与节流。

throttle(节流):

连续触发事件,但是在n 秒中只执行一次函数

n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效(游戏中技能CD)

debounce(防抖):

触发事件后,在N秒后函数才会执行

n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时(游戏中的回城)

代码实现

节流(scroll事件监听)

这里添加了一个节流阀flag,在进入时判断flag是否为真,进入后立马将阀门关闭(flag=false)然后通过定时器控制在500ms后开起阀门,实现在500ms内的事件只触发一次

function throttle(fn,delay=100) {
     let flag = true;
     return function() {
       if (flag) {
           flag = false
       }
       setTimeout(() => {
         fn.apply(this);
         flag = true;
       }, delay);
    }
}

//ES6写法
function throttle(fn,wait){
    let timer;
    return (...args)=>{
        if(!timer){
            timer = setTimeout(()=>{
                fn.apply(this,args)
                timer = null
            },wait)
        }
    }
}

防抖(表单验证,提交按钮,窗口resize)

定义一个timer变量在每次执行回调函数时先清空定时器,然后通过timer接收定时器,时间设置在500ms。(时间的话可以自己定)

也就是说在500ms内触发的定时器都被清除,只触发最后一次。

当间隔时间超过500ms时才触发函数。    

  function debounce(fn, delay=500) {
    let timer;
    return function () {
      clearTimeout(timer);
      timer = setTimeout(() => {
        fn.apply(this);
      }, delay);
    };
  }

//ES6写法
function debounce(fn,wait){
    let timer;
    return (...args)=>{
        timer && clearTimeout(timer)
        timer = setTimeout(()=>{
            fn.apply(this,args)
            timer = null
        },wait)
    }
}

区别


相同点:

●都可以通过使用 setTimeout 实现
●目的都是,降低回调执行频率。节省计算资源

原理:函数节流与函数防抖的原理非常简单,巧妙地使用 setTimeout 来存放待执行的函数,这样可以很方便的利用 clearTimeout 在合适的时机来清除待执行的函数。

不同点:

节流(thorttle防抖(debounce
触发时一定时间内只触发一次函数延迟n秒后再执行函数
写法变量timerclearTimeout
多次执行时多次执行变成每隔一段时间执行多次执行变成最后一次执行
侧重点减少执行次数用节流只执行一次用防抖
n秒内再次触发只有一次生效重新计时
应用场景scroll、search联想、submitinput、resize
用户效果
  • 购物节抢购,由于有无数人快速点击按钮,如果每次点击都发送请求,就会给服务器造成巨大压力,进行节流后,就会大大减少请求次数。
  • 例如滚动条滚动时函数的处理,可以通过节流适当减少响应次数。
  • 防止高频点击提交,防止表单重复提交
  • DOM元素的拖拽功能实现
  • 用户输入有操作时,暂时不执行动作,等待没有新操作时,进行相应响应,例如用户名输入校验的情况,可以等待用户输入完成后再发送请求去校验。
  • 例如搜索框输入关键字过程中实时请求服务器匹配搜索结果,如果不进行处理,那么就是输入框内容一直变化,导致一直发送请求,如果进行防抖处理,结果就是当我们输入内容完成后,一定时间(比如1000ms)没有再输入内容,这时再触发请求。
  • 手机号、邮箱验证输入检测。
  • 调整窗口大小,只需窗口调整完成后,计算窗口大小,防止重复渲染。
例如,都设置时间频率为500ms,在2秒时间内,频繁触发函数,节流,每隔 500ms 就执行一次。防抖,则不管调动多少次方法,在2s后,只会执行一次

应用场景

节流在间隔一段时间执行一次回调的场景有:

●滚动加载,加载更多或滚到底部监听。
●搜索框,search搜索联想功能。

●高频点击、表单重复提交。

防抖在连续的事件,只需触发一次回调的场景有:

●搜索框搜索输入。只需用户最后一次输入完,再发送请求
●手机号、邮箱验证输入检测
●窗口大小resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。

前端高薪必会的JavaScript重难点知识:防抖与节流详解 - 知乎

手写系列之防抖和节流 - 知乎

前端常见面试题之js函数的防抖和节流_函数防抖和节流面试_俗人844的博客-CSDN博客

完成节流可以使用时间戳与定时器的写法

https://xiaolulu.blog.csdn.net/article/details/126168043?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-126168043-blog-128390324.235%5Ev32%5Epc_relevant_increate_t0_download_v2&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-126168043-blog-128390324.235%5Ev32%5Epc_relevant_increate_t0_download_v2&utm_relevant_index=2


JS 中的垃圾回收机制

GC算法(Garbage Collection)
GC是什么?

垃圾回收机制的简写。可以找到内存中的垃圾,并释放和回收垃圾。

GC中的垃圾是什么?

程序中不再需要使用的对象
程序中不能再访问到的对象

GC算法是什么?

GC是一种机制,垃圾回收器完成具体的工作。

工作的内容就是查找垃圾释放空间、回收空间。
算法就是工作时查找和回收所遵循的规则。

什么是垃圾回收?

在说这个东西之前,先要解释什么是内存泄漏,因为内存泄漏了,所以引擎才会去回收这些没有用的变量,这一过程就叫垃圾回收

什么是内存泄漏?

程序的运行需要占用内存,当这些程序没有用到时,还不释放内存,就会引起内存泄漏。

回收的是什么?

回收内存。清理变量,释放内存空间

什么是内存管理

内存管理:开发者主动申请空间、使用空间、释放空间

管理流程:申请 -> 使用 -> 释放

js的内存空间在定义变量时自动分配,程序猿无法指定明确大小

js内存生命周期

申请内存空间: let obj = {};
使用内存空间: (读写操作) obj.name = 'sunny';
释放内存空间: (js中并没有相应的释放api) obj = null;

js中的垃圾回收

js中的内存管理是自动的,每当我们创建函数、对象、数组的时候会自动的分配相应的内存空间;

  • 对象不再被引用的时候是垃圾;
  • 对象不能从根上访问到时也是垃圾;
参考回答:
必要性:由于字符串、对象和数组没有固定大小,所有当他们的大小已知时,才能对
他们进行动态的存储分配。JavaScript 程序每次创建字符串、数组或对象时,解释器
都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些
内存以便他们能够被再用,否则,JavaScript 的解释器将会消耗完系统中所有可用的
内存,造成系统崩溃。 这段话解释了为什么需要系统需要垃圾回收,JS 不像 C/C++,他有自己的一套垃圾回 收机制(Garbage Collection)。JavaScript 的解释器可以检测到何时程序不再使用
一个对象了,当他确定了一个对象是无用的时候,他就知道不再需要这个对象,可以
把它所占用的内存释放掉了。例如:
var a="hello world";
var b="world";
var a=b;
//这时,会释放掉"hello world",释放内存以便再引用
垃圾回收的方法:标记清除、计数引用。

标记清除法

  • 核心:

    1. 标记清除算法将“不再使用的对象”定义为“无法达到的对象”。

    2. 就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。 凡是能从根部到达的对象,都是还需要使用的。

    3. 那些无法由根部出发触及到的对象被标记为不再使用,稍后进 行回收。

这是最常见的垃圾回收方式,当变量进入环境时,就标记这个变量为”进入环境“,从
逻辑上讲,永远不能释放进入环境的变量所占的内存,永远不能释放进入环境变量所
占用的内存,只要执行流程进入相应的环境,就可能用到他们。当离开环境时,就标
记为离开环境。
垃圾回收器在运行的时候会给存储在内存中的变量都加上标记(所有都加),然后去
掉环境变量中的变量,以及被环境变量中的变量所引用的变量(条件性去除标记),
删除所有被标记的变量,删除的变量无法在环境变量中被访问所以会被删除,最后垃
圾回收器,完成了内存的清除工作,并回收他们所占用的内存。

引用计数法

另一种不太常见的方法就是引用计数法,引用计数法的意思就是每个值没引用的次
数,当声明了一个变量,并用一个引用类型的值赋值给改变量,则这个值的引用次数
为 1,;相反的,如果包含了对这个值引用的变量又取得了另外一个值,则原先的引用
值引用次数就减 1,当这个值的引用次数为 0 的时候,说明没有办法再访问这个值了,
因此就把所占的内存给回收进来,这样垃圾收集器再次运行的时候,就会释放引用次
数为 0 的这些值。
用引用计数法会存在内存泄露,下面来看原因:
function problem() {
var objA = new Object();
var objB = new Object();
objA.someOtherObject = objB;
objB.anotherObject = objA;
}
在这个例子里面,objA 和 objB 通过各自的属性相互引用,这样的话,两个对象的引用
次数都为 2,在采用引用计数的策略中,由于函数执行之后,这两个对象都离开了作用
域,函数执行完成之后,因为计数不为 0,这样的相互引用如果大量存在就会导致内存
泄露。
特别是在 DOM 对象中,也容易存在这种问题:
var element=document.getElementById(’‘);
var myObj=new Object();
myObj.element=element;
element.someObject=myObj;
这样就不会有垃圾回收的过程。

eval 是做什么的

参考回答:
它的功能是将对应的字符串解析成 JS 并执行,应该避免使用 JS,因为非常消耗性能
2 次,一次解析成 JS,一次执行)

如何理解前端模块化

参考回答:
前端模块化就是复杂的文件编程一个一个独立的模块,比如 JS 文件等等,分成独立的
模块有利于重用(复用性)和维护(版本迭代),这样会引来模块之间相互依赖的问
题,所以有了 commonJS 规范,AMD,CMD 规范等等,以及用于 JS 打包(编译等处理)
的工具 webpack

说一下 CommonJS、AMD 和 CMD

参考回答:
一个模块是能实现特定功能的文件,有了模块就可以方便的使用别人的代码,想要什
么功能就能加载什么模块。
CommonJS:开始于服务器端的模块化,同步定义的模块化,每个模块都是一个单独的
作用域,模块输出,modules.exports,模块加载 require()引入模块。
AMD:中文名异步模块定义的意思。
requireJS 实现了 AMD 规范,主要用于解决下述两个问题。
1.多个文件有依赖关系,被依赖的文件需要早于依赖它的文件加载到浏览器
2.加载的时候浏览器会停止页面渲染,加载文件越多,页面失去响应的时间越长。
语法:requireJS 定义了一个函数 define,它是全局变量,用来定义模块。
requireJS 的例子:
//定义模块
define(['dependency'], function() {
    var name = 'Byron';
    function printName(){
        console.log(name);
    }
    return {
        printName: printName
    };
});

//加载模块
require(['myModule'], function (my) {
    my.printName();
}

RequireJS 定义了一个函数 define,它是全局变量,用来定义模块:

define(id?dependencies?,factory)

在页面上使用模块加载函数:

require([dependencies],factory);

总结 AMD 规范:require()函数在加载依赖函数的时候是异步加载的,这样浏览器不会失去响应,它指定的回调函数,只有前面的模块加载成功,才会去执行。

因为网页在加载 JS 的时候会停止渲染,因此我们可以通过异步的方式去加载 JS,而如果需要依赖某些,也是异步去依赖,依赖后再执行某些方法。

  • 总结

  • CommonJS规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD CMD解决方案。
  • AMD规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅。
  • CMD规范与AMD规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在Node.js中运行。不过,依赖SPM 打包,模块的加载逻辑偏重
  • ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案

AMD,CMD,CommonJs到底是何方神圣?以及三者之间的区别_amd commonjs-CSDN博客


对象深度克隆的简单实现

参考回答:
对象的地址指针存放于栈中,而对象实际的数据存放于堆中。
浅度克隆:
因此当我们简单地执行复制操作时,实际是把地址指针进行了复制操作,因此在对象的实际数据改变之后,新旧对象都会受到影响。
深度克隆:
JS中的深度克隆,指的是原对象改变了,克隆出来的新对象也不会改变,原对象与新对象是完全独立的关系。
function deepClone(obj) {
    var newObj= obj instanceof Array ? []:{};
    for(var item in obj){
        var temple= typeof obj[item] == 'object' ? deepClone(obj[item]) : obj[item];
        newObj[item] = temple;
    }
    return newObj;
}

ES5 的常用的对象克隆的一种方式。注意数组是对象,但是跟对象又有一定区别,所以

我们一开始判断了一些类型,决定 newObj 是对象还是数组。

实现一个 once 函数,传入函数参数只执行一次

参考回答:
once 的实现并不复杂,只要利用闭包,用封闭的环境保存一个缓存的返回值,以及一个是否执行过的状态,就能控制函数的执行走向。
function once(func) {
    var tag=true;
    return function(){
        if(tag==true){
            func.apply(null,arguments);
            tag=false;
        }
    return undefined
    }
}

百度安全验证


JS 监听对象属性的改变

参考回答:
我们假设这里有一个 user 对象,
(1)在 ES5 中可以通过 Object.defineProperty 来实现已有属性的监听
Object.defineProperty(user,'name',{
    set:function(key,value){

    }
})
当需要设置对象中多个属性时,使用defineProperties()进行监听
Object.defineProperties(obj,{
      a : {
            configurable: true, // 设置属性可以更改,默认为false
            set : function(value){}
      },      
       b : {
           configurable: true, // 设置属性可以更改,默认为false
            set : function(value){}
      }
    } 
})

(2)在 ES6 中可以通过 Proxy 来实现

var user = new Proxy({},{
    set:function(target,key,value,receiver){

    }
})

这样即使有属性在 user 中不存在,通过 user.id 来定义也同样可以这样监听这个属性的变化哦。

如何使用ES5、ES6实现监听对象属性的改变? - 知乎


如何实现一个私有变量,用 getName 方法可以访问,不能直接访问

参考回答:
(1)通过 defineProperty 来实现
obj={
    name:yuxiaoliang,
    getName:function(){
        return this.name
   }
}
object.defineProperty(obj,"name",{
    //不可枚举不可配置
});

(2)通过函数的创建形式

function product(){
    var name='yuxiaoliang';
    this.getName=function(){
        return name;
    }
}
var obj=new product();

前端面试题解析 getName - 哔哩哔哩


==和===、以及 Object.is 的区别

1. == :等于,两边值类型不同的时候,先进行类型转换,再比较;

2. === :严格等于,只有当类型和值都相等时,才相等;

3. Object.is() :与 === 的作用基本一样,但有些许不同。

== 和 === 的区别

== 和 === 的最大区别就是前者不限定类型而后者限定类型。如下例,如果想要实现严格相等(===),两者类型必须相同。

1 == "1";     // true
1 === "1";    // false

0 == false;   // true
0 === false;  // false

对于严格相等,有以下规则,如果  x === y,那么:

  a、如果x的类型和y的类型不一样,返回false;

  b、如果x的类型是数字,那么:

    (1):如果x是NaN,返回false;

    (2):如果y是NaN,返回false;

    (3):如果x和y是同一个数字值,返回true;

    (4):如果x是+0,y是-0,返回true;

    (5):如果x是-0,y是+0,返回true;

    (6):其余返回false。

  c、如果x和y的类型都为undefined或者null,返回true;

  d、如果x和y的类型都为字符串类型,那么如果x和y是完全相同的字符编码序列,返回true,否则返回false;

  e、如果x和y的类型都为布尔类型,那么如果x和y同为true或者false,返回true,否则返回false;

  f、如果x和y是同一个对象值,返回true,否则返回false。

=== 和 Object.is() 的区别

Object.is() 的行为与 === 基本一致,但有两处不同:

  a、+0 不等于 -0;

  b、NaN 等于自身,即NaN==NaN

https://www.cnblogs.com/zzh0318/p/12046530.html


setTimeout、setInterval 和 requestAnimationFrame 之间的区别

定义:

与 setTimeout 和 setInterval 不同,requestAnimationFrame 不需要设置时间间隔, requestAnimationFrame() 方法告诉浏览器您希望执行动画并请求浏览器在下一次重绘之前调用指定的函数来更新动画。该方法使用一个回调函数作为参数,这个回调函数会在浏览器重绘之前调用。

相比 setTimeout 等 API 的优势之一是减少 DOM 重绘的次数。

使用

//控制台输出1和0
var timer = requestAnimationFrame(function(){
    console.log(0);
}); 
console.log(timer);//1

cancelAnimationFrame方法用于取消定时器

//控制台什么都不输出
var timer = requestAnimationFrame(function(){
    console.log(0);
}); 
cancelAnimationFrame(timer);

也可以直接使用返回值进行取消

var timer = requestAnimationFrame(function(){
    console.log(0);
}); 
cancelAnimationFrame(1);

兼容

  IE9-浏览器不支持该方法,可以使用setTimeout来兼容

【简单兼容】

if (!window.requestAnimationFrame) {
    requestAnimationFrame = function(fn) {
        setTimeout(fn, 17);
    };    
}

特点

  【1】requestAnimationFrame会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率

  【2】在隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流,这当然就意味着更少的CPU、GPU和内存使用量

  【3】requestAnimationFrame是由浏览器专门为动画提供的API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了CPU开销

总结:setTimeout 与 requestAnimationFrame 的区别:

引擎层面
​​setTimeout 属于 JS 引擎,存在事件轮询,存在事件队列。​​

​​requestAnimationFrame 属于 GUI 引擎,发生在渲染过程的中重绘重排部分,与电脑分辨路保持一致。​​

性能层面

https://www.cnblogs.com/xiaohuochai/p/5777186.html


setTimeout(fn,100);100 毫秒是如何权衡的

参考回答:
setTimeout()函数只是将事件插入了任务列表,必须等到当前代码执行完,主线程才会去执行它指定的回调函数,有可能要等很久,所以没有办法保证回调函数一定会在 setTimeout 指定的时间内执行,100 毫秒是插入队列的时间+等待的时间


用 setTimeout 来实现 setInterval

setInterval的缺点:

1、使用setInterval时,某些间隔会被跳过(如果上一次执行代码没有执行,那么这次的执行代码将不会被放入队列,会被跳过)

2、可能多个定时器会连续执行(上一次代码在队列中等待还没有开始执行,然后定时器又添加第二次代码,第一次代码等待时间和执行时间刚好等于第二次代码执行)

setTimeout实现setInterval功能

先来看一下两者的简介:
setTimeout在指定的毫秒数后调用函数或计算表达式。
setInterval按照指定的周期(以毫秒计)来调用函数或计算表达式。
简单理解为setTimeout只执行一次,而setInterval可以按周期一直执行下去。

使用递归函数实现,不断地去执行 setTimeout 从而达到 setInterval 的效果。这样我们就确保了只有一个事件结束了,我们才会触发下一个定时器事件,解决了 setInterval 的问题。

请看代码:

function newIntervel() {
  setTimeout(function() {
    console.log(123)
    newIntervel()//调用自身
  }, 1000)
}
newIntervel()//控制台每过一秒一直打印 123

 

 函数newIntervel内部有一个setTimeout,每过一秒再次调用newIntervel,这样就实现了基本的setInterval功能。

封装

setInterval方法我们一般是以函数作为第一个参数,时间作为第二个参数,就像这样setInterval(function() {}, 1000),所以我们用setTimeout来封装一个可以传两个参数的newInterval:

function newInterval(callback, time) {
  setTimeout(function () {
    callback()
    newInterval(callback, time)
  }, time)
}

使用方法:newInterval(function () { }, 1000),这样就实现了比较完整的setInterval

用setTimeout实现setInterval功能 - 简书


JS 怎么控制一次加载一张图片,加载完后再加载下一张

只要能监控到图片是否加载完成 就能实现了
要把图片当成是图片对象才行;

方法一、

借助Image对象的onload事件,该事件在图片对象加载完成时触发

<div id="mypic">onloading……</div>

var obj=new Image(); // <img> 标签每出现一次,一个 Image 对象就会被创建。
obj.src="http://www.phpernote.com/uploadfiles/editor/201107240502201179.jpg";
// onload当图像装载完毕时调用的事件句柄。
obj.onload=function(){ // 这个就是加载完成,当加载完成之后可以执行一个函数比如
alert('图片的宽度为:bai'+obj.width+';图片的高度为:'+obj.height);
document.getElementById("mypic").innnerHTML="<img src='"+this.src+"' />";
}

方法二、

同理,借助图片对象身上的complete属性可以查看图片是否加载完成

<div id="mypic">onloading……</div>

let obj = new Image()  
obj.src = '..'
obj.onreadystatechange = function () {
  if(this.readyState === 'complate') {
    document.getElementById('mypic').innerHTML = '<img src=" '+this.src +'">'
  }
}

onreadystatechange:属性指向一个回调函数。当页面的加载状态发生改变的时候readyState属性就会跟随发生变化,而这时readystatechange属性所对应的回调函数就会自动被调用

 readyState:是一个只读属性,用一个整数和对应的常量来表示XMLHttpRequest请求当前所处的状态一般会在onreadystatechange事件的回调函数中,通过判断readyState属性的值,进而执行不同状态对应的函数。
​               语法:

xhr.onreadystatechange = function() {
    if(xhr.readyState == n){
    ​  // 执行对应的函数
    }
}

onload和onreadstatechange_onreadystatechange和onload-CSDN博客


代码的执行顺序

 js代码的执行顺序_js代码执行的先后顺序_半个开心果的博客-CSDN博客

宏任务主要包含:script( 整体代码)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 环境)
微任务主要包含:Promise、MutaionObserver、process.nextTick(Node.js 环境)

process.nextTick 中的回调是在当前tick执行完之后,下一个宏任务执行之前调用的。

理解 process.nextTick() - 简书

setTimeout(function(){console.log(1)},0);

new Promise(function(resolve,reject){
   console.log(2);
   resolve();
}).then(function(){console.log(3)
}).then(function(){console.log(4)});

process.nextTick(function(){console.log(5)});

console.log(6);
//输出2,6,5,3,4,1

注意:定义promise的时候,promise构造部分是同步执行的

首先分析Job queue的执行顺序:

script(主程序代码)—>process.nextTick—>Promises...——>setTimeout——>setInterval——>setImmediate——> I/O——>UI rendering

I) 主体部分: 定义promise的构造部分是同步的,
因此先输出2 ,主体部分再输出6(同步情况下,就是严格按照定义的先后顺序)

II)process.nextTick: 输出5

III)promise: 这里的promise部分,严格的说其实是promise.then部分,输出的是3,4

IV) setTimeout : 最后输出1

综合的执行顺序就是: 2——>6——>5——>3——>4——>1


从promise、process.nextTick、setTimeout出发,谈谈Event Loop中的Job queue · Issue #5 · forthealllight/blog · GitHub


Eventloop

参考回答:
任务队列中,在每一次事件循环中,macrotask 只会提取一个执行,而 microtask 会一直提取,直到 microsoft 队列为空为止。
也就是说如果某个 microtask 任务被推入到执行中,那么当主线程任务执行完成后,会循环调用该队列任务中的下一个任务来执行,直到该任务队列到最后一个任务为止。而事件循环每次只会入栈一个 macrotask,主线程执行完成该任务后又会检查
  • microtasks 队列并完成里面的所有任务后再执行 macrotask 的任务。
  • macrotasks: setTimeout, setInterval, setImmediate, I/O, UI rendering
  • microtasks: process.nextTick, Promise, MutationObserver

setTimeout 和 Promise 的执行顺序

参考回答:
首先我们来看这样一道题:
setTimeout(function () {
	console.log(1)
}, 0);
new Promise(function(resolve, reject) {
	console.log(2)
	for (var i = 0; i < 10000; i++) {
		if(i === 10) {console.log(10)}
			i == 9999 && resolve();
		}
	console.log(3)
}).then(function() {
	console.log(4)
})
console.log(5);

输出答案为 2 10 3 5 4 1

要先弄清楚 settimeout(fun,0)何时执行,promise 何时执行,then 何时执行settimeout 这种异步操作的回调,只有主线程中没有执行任何同步代码的前提下,才会执行异步回调,而 settimeout(fun,0)表示立刻执行,也就是用来改变任务的执行顺序,要求浏览器尽可能快的进行回调
promise 何时执行,由上图可知 promise 新建后立即执行,所以 promise 构造函数里代码同步执行的, then 方法指向的回调将在当前脚本所有同步任务执行完成后执行, 那么 then 为什么比settimeout 执行的早呢,因为 settimeout(fun,0)不是真的立即执行,
经过测试得出结论:执行顺序为:同步执行的代码->promise.then->settimeout

如何实现 sleep 的效果(es5 或者 es6)

简要介绍:在多线程编程中,sleep的作用是起到挂起的作用,使线程休眠,而js是单线程的,我们如何在js中模拟sleep的效果呢~ 也就是如何用同步的方式来处理异步。

序:为什么不能用setTimeout来实现sleep的效果

因为sleep要实现的是同步进程或者说同步程序的挂起,而setTimeout本身是异步的

1、while循环的方式

执行sleep()之后,休眠了1000ms之后输出了1111。以下循环的方式缺点很明显,容易造成死循环。

function sleep(ms){
   var start=Date.now(),expire=start+ms;
   while(Date.now()<expire);
   console.log('');
   return;
}

2、通过promise实现

function sleep(ms){
  var temple=new Promise(
  (resolve)=>{
  console.log(111);setTimeout(resolve,ms)
  });
  return temple
}
sleep(500).then(function(){
   //console.log(222)
})

//先输出了111,延迟500ms后输出222

3、通过async封装

function sleep(ms){
  return new Promise((resolve)=>setTimeout(resolve,ms));
}
async function test(){
  var temple=await sleep(1000);
  console.log(1111)
  return temple
}
test();
//延迟1000ms输出了1111

4、通过generate实现

function* sleep(ms){
   yield new Promise(function(resolve,reject){
             console.log(111);
             setTimeout(resolve,ms);
        })  
}
sleep(500).next().value.then(function(){console.log(2222)})

es5、promise、async以及generator中实现sleep的方法_es6 线程休眠_小小小小小亮的博客-CSDN博客


介绍一下 promise,及其底层如何实现

参考回答:

Promise 是js中的一个原生对象,保存着未来将要结束的事件,解决回调地狱的问题,她有两个特征:

1、对象的状态不受外部影响,Promise 对象代表一个异步操作,有三种状态,pending 进行中,fulfilled 已成功,rejected 已失败,只有异步操作的结果,才可以决定当前是哪一种状态,任何其他操作都无法改变这个状态,这也就是 promise 名字的由来
2、一旦状态改变,就不会再变,promise 对象状态改变只有两种可能,从 pending 改到 fulfilled 或者从 pending 改到 rejected,只要这两种情况发生,状态就凝固了, 不会再改变,这个时候就称为定型 resolved。

Promise 构造函数的基本用法

let promise = new Promise((resolve, reject) => {
	setTimeout(() => {
		if (true) {
			resolve('ok!')
		} else {
			reject('失败了!')
		}
	}, 1000);
})
promise.then(result => console.log(result))
	.catch(error => console.log(error))

最简单代码实现 promise

class PromiseM {
	constructor (process) {
		this.status = 'pending'
		this.msg = ''
		process(this.resolve.bind(this), this.reject.bind(this))
	    return this
	}
	resolve (val) {
		this.status = 'fulfilled'
		this.msg = val
	}
	reject (err) {
		this.status = 'rejected'
		this.msg = err
	}
	then (fufilled, reject) {
		if(this.status === 'fulfilled') {
			fufilled(this.msg)
		}
		if(this.status === 'rejected') {
			reject(this.msg)
		}
	}
}
//测试代码
var mm=new PromiseM(function(resolve,reject){
	resolve('123');
});
mm.then(function(success){
	console.log(success);
},function(){
	console.log('fail!');
});

 一文告诉你什么是回调地狱,如何解决回调地狱?_混子前端的博客-CSDN博客


如何解决异步回调地狱

promise、generator、async/await

Generator 生成器

  • 函数前添加 *,生成一个生成器
  • 一般配合 yield 关键字使用
  • 最大特点,惰性执行,调 next 才会往下执行
  • 主要用来解决异步回调过深的问题

promise+Generator+Async 的使用

promise对象和Async异步函数前后文都有,这里讲generator函数

Generator 函数:

1、分段执行,可以暂停

2、可以控制阶段和每个阶段的返回值

3、可以知道是否执行到结尾,value 表示返回值,done 表示遍历是否结束,false没结束,true结束了

{
  function* gen() {
    yield 'hello'
    yield 'world'
    return 'ending'
  }

  let it = gen()

  it.next()   // {value: "hello", done: false}
  it.next()   // {value: "world", done: false}
  it.next()   // {value: "ending", done: true}
  it.next()   // {value: undefined, done: true}
}

 Generator函数_沉默的小刘的博客-CSDN博客

26:promise+Generator+Async 的使用 - 简书


简单的实现一个 promise

我们先分析一下promise 都有哪些功能:

1、promise 是一个类,所以 我们可以使用class关键字声明

2、promise 有三种状态 pending (等待执行) fulfilled (执行成功) rejected (执行失败)

 - 状态只能有两种转换方式:

· pending --> fulfilled

· pending --> rejected

- 只要状态发生了改变,这一次promise就算执行完成了。

- 不管结果是成功还是失败,都不会再变成另一种。

3、promise 接收的是一个匿名函数,我们称之为执行器

4、执行器函数有两个回调方法,一个叫做 resolve reject。

- 这两个方法是从 promise 内部调用的并且需要传一个结果出来,所以应该是promise类内部的方法。

- resolve 在执行之后,promise的状态就变成了fulfilled

- rejecte 在执行之后,promise的状态就变成了rejected

5、resolve,reject都有一个参数 来表示成功的结果和失败的的原因,我们将这两个值保存在promise内部,因为在then的时候还会使用到他们

6、promise有一个then方法,它有两个参数,一个是成功的回调方法,一个是失败的回调方法,每个方法都能接收到对应状态的返回结果。

function myPromise(constructor){
    let self=this;
    self.status="pending" //定义状态改变前的初始状态
    self.value=undefined;//定义状态为resolved的时候的状态
    self.reason=undefined;//定义状态为rejected的时候的状态
    function resolve(value){
        //两个==="pending",保证了状态的改变是不可逆的
       if(self.status==="pending"){
          self.value=value;
          self.status="resolved";
       }
    }
    function reject(reason){
        //两个==="pending",保证了状态的改变是不可逆的
       if(self.status==="pending"){
          self.reason=reason;
          self.status="rejected";
       }
    }
    //捕获构造异常
    try{
       constructor(resolve,reject);
    }catch(e){
       reject(e);
    }
}

//在myPromise的原型上定义链式调用的then方法:
myPromise.prototype.then=function(onFullfilled,onRejected){
   let self=this;
   switch(self.status){
      case "resolved":
        onFullfilled(self.value);
        break;
      case "rejected":
        onRejected(self.reason);
        break;
      default:       
   }
}

//上述就是一个初始版本的myPromise,在myPromise里发生状态改变,
//然后在相应的then方法里面根据不同的状态可以执行不同的操作。
var p=new myPromise(function(resolve,reject){
    resolve(1)
});
p.then(function(x){
    console.log(x) //输出1
})


//但是这里myPromise无法处理异步的resolve.比如:
var p=new myPromise(function(resolve,reject){
    setTimeout(function(){
        resolve(1)
    },1000)
});

p.then(function(x){
    console.log(x) //无输出
})
参考回答:
首先明确什么是 promiseA+规范
一般不会问的很详细,只要能写出上述文章中的 v1.0 版本的简单 promise 即可。

异步函数async/await

异步函数可以将异步代码写成同步的形式,让代码不再有回调函数的嵌套

const fn = async () => {}
async function fn () {}

async

  • 默认返回promise对象
  • 在异步函数内部使用return返回结果,结果会被包裹在promise对象中,return关键字代替resolve方法
  • 在异步函数内部使用throw抛出程序异常
  • 链式调用then方法获取执行结果,catch获取错误信息

await

  • await只能出现异步函数中
  • await后面只能写promise对象,不可以写其他类型的api
  • await的作用是暂停异步函数向下执行,直到promise返回结果

异步的本质

async/await是语法糖,异步的本质还是回调函数,代码执行规则依然按照Event Loop进行。

async function async1(){
    console.log('async1 start') // 顺序2
    // await 的后面,都可以看作是callback里的内容,即异步
    await async2()
    // Promise.resolve.then(()=>{console.log('async1 end'}) 
    console.log('async1 end')  // 顺序5
}

async function async2(){
    console.log('async2')  // 顺序3
}

console.log('script start')  // 顺序1
async1()
console.log('script end')  // 顺序4

原型和原型链

作用:被用于复制现有实例来生成新实例的函数,可以很好的实现继承

总结

1、

__proto__:对象拥有的隐式原型(对象原型)

prototype:函数拥有的显式原型(原型对象)

2、

对象只有 __proto__

函数  __proto__ 和 prototype 两者都有

3、总结 __proto__:

  • 通常被称作隐式原型,每个对象都拥有该属性。
  • 对象的[[prototype]]其实就是__proto__。

4、constructor构造函数
对象原型(proto_)和构造函数(prototype)原型对象里面都有一个属性contructor属性。

  • contructor我们称为构造函数,因为她指回构造函数本身。
  • contructor主要用于记录该对象引用于哪个构造函数,他可以让原型对象重新指向原来的构造函数。
  • 给prototype追加方法,不会覆盖contructor。
  • 给prototype赋值,会覆盖掉contructor。
function Person() {}

var person = new Person();

得到三个等式:

Object.getPrototypeOf(person) === Person.prototype

Person===Person.prototype.constructor

person.constructor === Person.prototype.constructor

原型链

查找属性的过程形成的一条线索就叫做原型链,大家可以把原型链拆开来理解:原型和链。

  • 原型就是我们的prototype
  • 链就是__proto__,它让整个链路连接起来。

想要理解原型链,我们还得理解__proto__指向哪儿,也就是说它指向那个构造函数,比如上面的obj对象的__proto__指向的就是Person构造函数,所以我们继续往Person上查找。

一文搞懂原型和原型链! - 知乎

https://www.cnblogs.com/loveyaxin/p/11151586.html


Function._proto_(getPrototypeOf)是什么?

参考回答:
Function.__proto__(左右两边各两个“_”,或者是Object.getPrototypeOf,这两者均是获取原型)
在 chrome 中可以通过_proto_的形式,或者在 ES6 中可以通过Object.getPrototypeOf 的形式。
那么 Function.proto 是什么么?也就是说 Function 由什么对象继承而来,我们来做
如下判别。
Function.__proto__==Object.prototype //false
Function.__proto__==Function.prototype//true

我们发现 Function 的原型也是 Function。

 轻松理解 JS 中的面向对象,顺便搞懂 prototype

  1. JS中的函数可以作为函数使用,也可以作为类使用

  2. 作为类使用的函数实例化时需要使用new

  3. 为了让函数具有类的功能,函数都具有prototype属性。

  4. 为了让实例化出来的对象能够访问到prototype上的属性和方法,实例对象的__proto__指向了类的prototype。所以prototype是函数的属性,不是对象的。对象拥有的是__proto__,是用来查找prototype的。

  5. prototype.constructor指向的是构造函数,也就是类函数本身。改变这个指针并不能改变构造函数。

  6. 对象本身并没有constructor属性,你访问到的是原型链上的prototype.constructor

  7. 函数本身也是对象,也具有__proto__,他指向的是JS内置对象Function的原型Function.prototype。所以你才能调用func.call,func.apply这些方法,你调用的其实是Function.prototype.callFunction.prototype.apply

  8. prototype本身也是对象,所以他也有__proto__,指向了他父级的prototype__proto__prototype的这种链式指向构成了JS的原型链。原型链的最终指向是ObjectObject上面原型链是null,即Object.__proto__ === null


JS 原型链,原型链的顶端是什么?Object 的原型是什么?Object 的原型的原型是什么?在数组原型链上实现删除数组重复数据的方法

参考回答
                                                  蓝色的线即是原型链

  • 原型链顶端是Object.prototype
  •  Object的原型是Object.prototype
  • Object的原型的原型是null,即Object.prototype没有原型,Object.prototype.__proto__ 不存在
Object.prototype.__proto__ === null,//返回true

  • 构造函数

典型的OOP的语言中,都存在类的概念,类就是对象的模板,对象就是类的实例化;

创建一个构造函数就是创建一个类。

类就是抽离出对象的公共属性和方法。

创建一个构造函数

    function Person(name, height) {
          this.name = name;
          this.height = height;
          this.bark = function(x) {
              return x
          }
     }
  
      var boy = new Person('Keith', 180);
      console.log(boy);  //Person {name: 'Keith', height: 180, bark: ƒ}
      console.log(boy.constructor);  //f Person(){}  //整个构造函数原型
      console.log(boy.bark(8));  //8

1、首字母大写,为了和普通函数区分

2、通过new创建多个实例对象

3、里面的属性和方法前必须加this,this就表示当前运行时的对象

4、ECMAScript提供了多个内置构造函数,如 Object、Array、String、Boolean、Number、Date…等等。var obj = new Object(); var arr = new Array();

ECMAScript也允许自定义构造函数

构造函数的原理:

在这里插入图片描述

构造函数的弊端:

  • 缺点1: 如果构造函数中有很多的方法那么就会开辟很多的空间,浪费内存资源
  • 缺点2: 如果在全局情况下声明函数,虽然解决了内存资源浪费的问题,但是又会出现全局变量污染的问题
  • 缺点3: 可以重新声明一个对象专门存放这些方法,但是新的问题时,如果有很多个构造函数,就要声明很多个这样的对象
  • 总结: 为解决创建构造函数的弊端的问题,我们可以直接使用原型对象

原型对象----构造函数每次在声明的时候,系统会自动的帮我们创建一个与之对应的对象
prototype---- 属于构造函数,指向原型对象
proto----属于构造函数的实例化对象,指向原型对象
constructor----属于原型对象,指向该原型对象与之对应的构造函数


 ES6创建类和对象

class Star {    //    1、首字母大写,类名不要加()
            constructor(name,age){    //无需加function
                this.name = name;
                this.age = age
            }
            listen(music){
                console.log('姓名:'+this.name+'年龄:'+this.age+'爱听的音乐:'+ music);
            }
        }
        // 2、利用类创建对象 new
        var a = new Star('Nan',10);  //Star {name: "Nan", age: 10}
        var b = new Star('Chen',20);
         a.listen('One Day');
        console.log(a);
        console.log(b);

 注意:

1、通过class创建类,类名最好首字母大写

2、constructor(){},用来接收传递的参数,并且返回实例对象

3、constructor(){} 一旦生成new实例对象时,就会自动调用这个函数,如果不写这个函数,其类也会生成这个函数

4、生成实例 new 不能省略

5、语法规范:创建类后的类名不要加小括号 ,生成实例对象的类名需要加(),构造函数无需加function

6、类的方法和constructor函数同级书写,不写在其里面


面向对象的特性

什么是面向对象?

对比来看,

面向过程:按照分析好的步骤解决问题

面向对象:以对象功能来划分问题

三大基本特性:封装,继承,多态


减少前端代码耦合

什么是代码耦合?代码耦合的表现是改了一点毛发而牵动了全身,或者是想要改点东西,需要在一堆代码里面找半天。由于前端需要组织js/css/html,耦合的问题可能会更加明显


实现 JS 中所有对象的深度克隆(包装对象,Date 对象,正则对象)

不管是浅拷贝还是深拷贝都用到for in 遍历

1、浅拷贝(内存地址一样,会改变原对象)

(1)手写for in 浅拷贝

(2)Object.assign() 方法

(3)函数库lodash的_.clone方法

(4)使用数组 API,如 concat 或者 slice 以及拓展运算符...

2、深拷贝(开辟新内存,不改变原对象,新旧各自独立)

(1)JSON.parse(JSON.stringify())  (不能处理函数和正则,因为函数会消失,正则变成空)

(2)函数库lodash的_.cloneDeep方法

(3)jQuery.extend()方法

(4)递归函数实现(只能深复制数组和对象)

①简单的深拷贝

function deepClone(obj) {
  var newObj = obj instanceof Array ? [] : {}
 
  for (const i in obj) {
    newObj[i] = typeof obj[i] == 'object' ? deepClone(obj[i]) : obj[i]
  }
 
  return newObj
}

②考虑到包装对象,Date 对象,正则对象的深拷贝

递归方法实现深度克隆原理:

在for in 里调用自身

遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。

// 在深拷贝的基础上,对(包装对象Number,String,Boolean;Date 对象,RegExp正则对象)进行深拷贝
function deepClone(obj) {
  let newObj = obj instanceof Array ? [] : {}
  for (let i in obj) {
    // for...in 会遍历原型上的属性,此处只拷贝obj对象自身的属性
    if (obj.hasOwnProperty(i)) {
      let type = Object.prototype.toString.call(obj[i])
      if (typeof obj[i] == 'object') {
        // 拷贝的值为对象,则需要深拷贝
        if (type == '[object Date]') {
          newObj[i] = new Date(obj[i].valueOf())
        } else if (type == '[object RegExp]') {
          // 正则对象
          let pattern = obj[i].valueOf()
          let flags = ''
          flags += pattern.global ? 'g' : ''
          flags += pattern.ignoreCase ? 'i' : ''
          flags += pattern.multiline ? 'm' : ''
          newObj[i] = new RegExp(pattern.source, flags)
        } else if (type == '[object Array]' || type == '[object Object]') {
          // 数组或对象
          newObj[i] = deepClone(obj[i])
        } else {
          // 包装对象Number,String,Boolean
          newObj[i] = obj[i].valueOf()
        }
      } else if (typeof obj[i] == 'function') {
        // 函数
        newObj[i] = new Function('return ' + obj[i].toString())()
      } else {
        // 拷贝的值为原始值,则直接复制该值
        newObj[i] = obj[i]
      }
    }
  }
  return newObj
}

前端深拷贝与浅拷贝(附实现方法)_godlike-icy的博客-CSDN博客

实现 js 中所有对象的深拷贝(包装对象,Date 对象,正则对象)_js 复制date对象__L...的博客-CSDN博客

javascript - 如何写出一个惊艳面试官的深拷贝? - code秘密花园 - SegmentFault 思否


包装对象

定义

对象是 JavaScript 语言最主要的数据类型,三种原始类型的值——数值、字符串、布尔值——在一定条件下,也会自动转为对象,也就是原始类型的“包装对象”(wrapper)。

所谓“包装对象”,指的是与数值、字符串、布尔值分别相对应的NumberStringBoolean三个原生对象。这三个原生对象可以把原始类型的值变成(包装成)对象。

var v1 = new Number(123);
var v2 = new String('abc');
var v3 = new Boolean(true);
 
typeof v1 // "object"
typeof v2 // "object"
typeof v3 // "object"
 
v1 === 123 // false
v2 === 'abc' // false
v3 === true // false

new Map()

Map数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。 

1、let mapObj = new Map();
2、let mapObj = new Map([[key,value],[key,value]]); //默认带初始化参数的定义


递归函数

一个函数在内部可以调用自身,就是递归函数

let num = 1;
function fn() {
	console.log(num);
	if(num == 6) return
	num++;
	fn();
}
fn(); 

简单实现 Node 的 Events 模块

实例1:

简介:观察者模式或者说订阅模式,它定义了对象间的一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,所有依赖于它的对象都将得到通知。

node中的Events模块就是通过观察者模式来实现的:

var events=require('events');
var eventEmitter=new events.EventEmitter();
eventEmitter.on('say',function(name){
console.log('Hello',name);
})
eventEmitter.emit('say','Jony yu');

这样,eventEmitter 发出 say 事件,通过 On 接收,并且输出结果,这就是一个订阅模式的实现,下面我们来简单的实现一个 Events 模块的 EventEmitter。

(1)实现简单的 Event 模块的 emit 和 on 方法
 
function Events(){
    this.on=function(eventName,callBack){
        if(!this.handles){
            this.handles={};
        }
        if(!this.handles[eventName]){
            this.handles[eventName]=[];
        }
        this.handles[eventName].push(callBack);
    }
    this.emit=function(eventName,obj){
        if(this.handles[eventName]){
            for(var i=0;o<this.handles[eventName].length;i++){
                this.handles[eventName][i](obj);
            }
        }
    }
    return this;
}

这样我们就定义了 Events,现在我们可以开始来调用:

var events=new Events();
events.on('say',function(name){
    console.log('Hello',nama)
});
events.emit('say','Jony yu');
//结果就是通过 emit 调用之后,输出了 Jony yu

(2)每个对象是独立的

因为是通过 new 的方式,每次生成的对象都是不相同的,因此:
var event1=new Events();
var event2=new Events();
event1.on('say',function(){
    console.log('Jony event1');
});
event2.on('say',function(){
    console.log('Jony event2');
})
event1.emit('say');
event2.emit('say');

event1、event2 之间的事件监听互相不影响

输出结果为'Jony event1' 'Jony event2'

https://www.cnblogs.com/fangtoushi/p/16013134.html


箭头函数中 this 指向举例

参考回答:
定义时绑定。
var a=11;
function test2(){
    this.a=22;
    let b=()=>{
        console.log(this.a)
    }
    b();
}
var x=new test2();//输出 22

JS 判断类型

参考回答:
判断方法:typeof(),instanceof,Object.prototype.toString.call()等

 数组常用方法

参考回答:

1、增 

        前面三种都会改变原数组,最后一项不改变原数组:
  • push(),在数组末尾添加,返回数组长度
  • unshift(),在数组开头添加,返回数组长度
  • splice(),传入三个参数,splice(开始位置,0删除0项,插入的元素),返回空数组
  • concat(),创建副本,在副本末尾添加参数,返回此副本,不改变原数组

2、删

        前面三种都会改变原数组,最后一项不改变原数组:

  • pop(),删除数组最后一项,返回被删除的项
  • shift(),删除数组第一项,返回被删除的项
  • splice(),传入2个参数,splice(0开始位置,1删除1项),返回包含删除元素的数组
  • slice(),从数组中截取指定的字段,返回出来,创建副本,slice(1)等同于slice(1, 1),截取索引为1的项到最后一项,slice(1,4)截取索引为1的项到索引为3的项,不改变原数组

3、改

即修改原来数组的内容,常用splice

  • splice(),传入三个参数,splice(开始位置,1删除1项,插入的元素),返回修改后的数组,改变原数组

4、查

即查找元素,返回元素坐标或者元素值

  • indexOf(),返回要查找的元素在数组中的索引,如果没找到则返回 -1
  • includes(),返回要查找的元素在数组中的索引,找到返回true,否则false
  • find(),返回第一个匹配的元素
  •  findIndex(),找到符合条件的项的下标,并且返回第一个

5、排序方法

数组有两个方法可以用来对元素重新排序:

  • reverse(),反转数组
  • sort(),接收一个比较函数,判断哪个值应该排在前面

6、转换方法

  • join() ,用指定的分隔符,将数组分割成字符串。返回值:返回一个新的数组
  • split(),用指定的分隔符,将字符串分割成数组。返回值:返回一个新的数组

7、求和方法

  • reduce(),用于数组求和,扁平数组
//其中,前两项参数是必须的
arr.reduce(function(prev,cur,index,arr){
...
}, init);
  • prev: 可指定初始值init,不指定默认为数组的第一项arr[0]
  • cur: 正在处理的元素,若prev制定了初始值init,则是数组第一项arr[0],否则默认是第二项arr[1]
  • index:正在处理的元素的索引。若指定了初始值 init,则起始索引号为 0,否则从索引 1 起始。
  • arr:用于遍历的数组。

求和案例:

①数组求和

var arr = [1,5,8,10,15,66,65,25,48,55]
 
//  reduce
 var sum = arr.reduce(function(prev,cur){
  return prev+cur;
 });
 
console.log(sum); //298

②求最大值

var arr = [1,5,8,10,15,66,65,25,48,55]
var max = arr.reduce(function (prev, cur) {
    return Math.max(prev,cur);
});
 
console.log(max)  //66

③扁平一个二维数组

var arr = [[1, 2, 8], [3, 4, 9], [5, 6, 10]];
var res = arr.reduce((prev, cur) => prev.concat(cur), []);
console.log(res)   // [1,2,8,3,4,9,5,6,10]

JS中的reduce()函数介绍_reduce函数_潮汐未见潮落的博客-CSDN博客

8、迭代方法 / 批处理方法

常用来遍历数组的方法(都不改变原数组)有如下:

遍历就是把数组中的每个元素从头到尾都访问一次,以下方法都是接收三个参数,例如:

[ ].forEach(function(item, index, arr) { }

  • some(),数组中有一项符合条件就返回true
  • every(),数组的每一项都符合条件才返回true
  • forEach(), 没有返回值
  • filter(),有返回值, 过滤出符合条件的元素
  • map(),映射,有返回值,返回值是生成的新数组

5、不改变原数组的

concat(),slice(),join() ,split(),filter(),indexOf(),reduce(),遍历数组的所有方法


 foreach和map的不同点:

(1)map()方法会得到一个新的数组并返回,forEach()会修改原来的数组。

map遍历的后的数组通常都是生成一个新的数组,新的数组的值发生变化,当前遍历的数组值不会变化。

(2)forEach()允许callback更改原始数组的元素。map()返回新的数组。

forEach遍历通常都是直接引入当前遍历数组的内存地址,生成的数组的值发生变化,当前遍历的数组对应的值也会发生变化。

foreach和map的共同点:

(1)都是循环遍历数组中的每一项。

(2)每次执行匿名函数都支持三个参数,参数分别为item(当前每一项),index(索引值),arr(原数组)。

(3)匿名函数中的this都是指向window。

(4)只能遍历数组。

vue使用场景

  • forEach() 适合你并不打算改变数据的时候,而只是想用数据做一些事情—比如存入数据库或者打印出来。
  • map() 适合你要改变数据值的时候,不仅仅在于它更快,而是返回一个新的数组;这样的优点在于你可以使用复合(composition) (map() , filter() , reduce() 等组合使用)来玩出更多的花样。

数组去重

参考回答:

原数组

const arr = [1, 1, '1', 17, true, true, false, false, 'true', 'a', {}, {}];

1.对象属性(indexof

利用对象属性key排除重复项

遍历数组,每次判断新数组中是否存在该属性,不存在就存储在新数组中

并把数组元素作为key,最后返回新数组

这个方法的优点是效率高,缺点是使用了额外空间

var newArr = [];
arr.forEach((key,index)=>{
    if(newArr.indexOf(key) === -1){
        newArr.push(key)
  }        })
console.log(newArr);// [1, '1', 17, true, false, 'true', 'a', {}, {}]

2.new Set(数组)

作用:Set() 用来存储键,是键的集合;即用来管理一组键(key)

Set是一系列无序、没有重复值的数据集合,传入一个需要去重的数组,Set会自动删除重复的元素

再将Set转数组返回。此方法效率高,代码清晰,缺点是存在兼容性问题

const newArr = [...new Set(arr)];
console.log(newArr);// [1, '1', 17, true, false, 'true', 'a', {}, {}] 

3.new Map()

       作用: Map() 用来存储键值对,是键值对的集合;即用来管理键值对(key:value)

  利用Map的键值对同名覆盖,再将Map转数组

const m = new Map();
for (let i = 0; i < arr.length; i++) {
    m.set(arr[i], arr[i]);
}
const newArr = []
m.forEach(function (value, key) {
    newArr .push(value)
})
console.log(newArr );//[1, '1', 17, true, false, 'true', 'a', {}, {}]

4.filter() + indexof

filter把接收的函数依次作用于每一个数组项,然后根据返回值 true or false 决定是否保留该值

优点在于可在去重时插入对元素的操作,可拓展性强

const newArr= arr.filter(function(item,index,self){
    return self.indexOf(item) === index;
})
console.log(newArr);//  [1, '1', 17, true, false, 'true', 'a', {}, {}]

5.reduce() + includes

reduce()把结果继续和序列的下一个元素做累加计算

利用reduce遍历和传入一个空数组作为去重后的新数组,然后内部判断新数组中是否存在当前遍历的元素,不存在就插入新数组

缺点在于时间消耗多,内存空间也额外占用

const newArray = arr.reduce((newArr, element) => {
  if (!newArr.includes(element)) {
    newArr.push(element);
  }
   return newArr;
}, []);

6、Object键值对去重

把数组的值存成 Object 的 key 值,比如Object[value1] = true,在判断另一个值的时候,如果 Object[value2]存在的话,就说明该值是重复的。

注意点:

在数据量较低时,以上五个方法无较为明显的区别(10000条)

高于10000条时,前两种方法时间消耗最少,后三种时间消耗依次增加

第一种方法空间占用多,当下很多项目不再考虑低版本游览器兼容性问题

推荐使用Set()去重方法


如何让事件先冒泡后捕获

参考回答: 在 DOM 标准事件模型中,是先捕获后冒泡。但是如果要实现先冒泡后捕获的效果,对
于同一个事件,监听捕获和冒泡,分别对应相应的处理函数,监听到捕获事件,先暂缓执行,直到冒泡事件被捕获后再执行捕获事件。


说一下事件委托

参考回答:
简介:事件委托,不是每个子节点单独设置事件监听器,而是事件监听器设置在其父节点上,
然后利用冒泡原理影响设置每个子节点。
举例:最经典的就是 ul 和 li 标签的事件监听,比如我们在添加事件时候,采用事件委托机制,不会在 li 标签上直接添加,而是在 ul 父元素上添加。
好处:比较合适动态元素的绑定,新添加的子元素也会有监听函数,也可以有事件触发机制。

事件委托以及冒泡原理。

参考回答:
  • 事件委托就是利用冒泡原理,把事件加到父元素或者祖先元素上,触发来执行效果
  • 其优点为:可以显著的提高事件的处理速度,减少内存的占用
  • 事件冒泡的原理:事件按照从最特定的目标到最不特定的目标的顺序触发,也就指事件向上传导,当后代元素上的事件被触发时,其祖先的相同事件也会被触发,而事件就是从dom树层层往上传递,直至传递到dom的根节点

事件代理在捕获阶段的实际应用

参考回答:
1、是什么?
  • 事件代理:把一个元素响应事件的函数委托到另一个元素上,也叫事件委托;
  • 事件流的三个阶段:1、捕获阶段----->2、目标阶段----->3、冒泡阶段;
  • 事件代理(事件委托)主要就是利用了冒泡的特点,是在冒泡阶段完成的;

2、好处: 只操作一次DOM,提高性能,减少事件绑定,从而减少内存占用。

3、原理

不是每个子节点单独设置事件监听器,而是事件监听器设置在其父节点上,
然后利用冒泡原理影响设置每个子节点

4、详细解释:

事件委托,会把一个或者一组元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素,而不是目标元素。

当事件响应到目标元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后再外层元素上去执行函数。

5、在捕获阶段的实际应用

可以在父元素层面阻止事件向子元素传播,也可代替子元素执行某些操作。

去除字符串首尾空格

参考回答:
使用正则(^\s*)|(\s*$)即可

 


 能来讲讲 JS 的语言特性吗

参考回答:

定义:

JavaScript是一门 函数为先的轻量级解释型或者即时编译型的编程语言,或者说JavaScript 是一种基于 原型编程、多范式的动态脚本语言,并且支持面向对象、命令式和声明式(如函数式编程)风格。

 

 区别:

HTML:标记语言

CSS:标记语言

JavaScript:动态脚本语言

JavaScript特性:

  1. 面向对象语言,是基于原型的面向对象;
  2. 脚本语言(动态语言)
  3. 事件驱动,运行在客户端浏览器上;
  4. 解释性语言,不用预编译,直接解析执行代码;
  5. 动态性,程序在运行时可以改变其结构;
  6. 是弱类型语言(&松散类型),较为灵活;
  7. 单线程与异步处理共存,回调函数事件;
  8. 与操作系统无关,跨平台的语言。

js语言的特点_A了LONE的博客-CSDN博客


说说 C++,Java,JavaScript 这三种语言的区别

参考回答:

1、从静态类型还是动态类型来看

静态类型,编译的时候就能够知道每个变量的类型,编程的时候也需要给定类型,如Java 中的整型 int,浮点型 float 等。C、C++、Java 都属于静态类型语言。
动态类型,运行的时候才知道每个变量的类型,编程的时候无需显示指定类型,如JavaScript 中的 var、PHP 中的$。JavaScript、Ruby、Python 都属于动态类型语言。
静态类型还是动态类型对语言的性能有很大影响。
  • 对于静态类型,在编译后会大量利用已知类型的优势,如 int 类型,占用 4 个字节,编译后的代码就可以用内存地址加偏移量的方法存取变量,而地址加偏移量的算法汇编很容易实现。
  • 对于动态类型,会当做字符串通通存下来,之后存取就用字符串匹配。

2、从编译型还是解释型来看

编译型语言
  • 像 C、C++,需要编译器编译成本地可执行程序后才能运行,由开发人员在编写完成后手动实施。用户只使用这些编译好的本地代码,这些本地代码由系统加载器执行,由操作系统的 CPU 直接执行,无需其他额外的虚拟机等。
  • 源代码=》抽象语法树=》中间表示=》本地代码
解释性语言
  • 像 JavaScript、Python,开发语言写好后直接将代码交给用户,用户使用脚本解释器将脚本文件解释执行。对于脚本语言,没有开发人员的编译过程,当然,也不绝对。
  • 源代码=》抽象语法树=》解释器解释执行。
  • 对于 JavaScript,随着 Java 虚拟机 JIT 技术的引入,工作方式也发生了改变。可以将抽象语法树转成中间表示(字节码),再转成本地代码,如 JavaScriptCore,这样可以大大提高执行效率。也可以从抽象语法树直接转成本地代码,如 V8。
  • Java 语言,分为两个阶段。首先像 C++语言一样,经过编译器编译。和 C++的不同,C++编译生成本地代码,Java 编译后,生成字节码,字节码与平台无关。第二阶段,由Java 的运行环境也就是 Java 虚拟机运行字节码,使用解释器执行这些代码。一般情况下,Java 虚拟机都引入了 JIT 技术,将字节码转换成本地代码来提高执行效率。
  • 注意,在上述情况中,编译器的编译过程没有时间要求,所以编译器可以做大量的代码优化措施。

3、对于 JavaScript 与 Java 它们还有的不同:

  • 对于 Java,Java 语言将源代码编译成字节码,这个同执行阶段是分开的。也就是从源代码到抽象语法树到字节码这段时间的长短是无所谓的。
  • 对于 JavaScript,这些都是在网页和 JavaScript 文件下载后同执行阶段一起在网页的加载和渲染过程中实施的,所以对于它们的处理时间有严格要求。

如何判断一个数组

参考回答:

1、坑 typeof

typeof 用来检测数据类型的运算符
typeof在判断null、array、object以及函数实例(new + 函数)时,得到的都是object。
数组是属于object类型的,也就是引用类型,所以不能使用 typeof 来判断其具体类型。

2、判断方法

(1)instanceof 运算符

let arr = [1,2,3];
console.log(arr instanceof Array); //true

(2)constructor 构造函数

let arr = [1,2,3];
console.log(arr.constructor == Array); //true

(3)isArray

(ES5新增数组方法,判断数组是不是数组)

let arr = [1,2,3];
console.log(Array.isArray(arr));  //true

(4)Object.prototype.toString.call()

(把对象转化成字符串和一个已知的对象对比)

let arr = [1,2,3];
console.log(Object.prototype.toString.call(arr) == '[object Array]'); //true

(5)Array原型链上的isPrototypeOf 

(Array.prototype表示Array的构造函数的原型isPrototypeOf()方法可以判断当前对象是否是另一个对象的原型,或者说一个对象是否被包含在另一个对象的原型链中)

let arr = [1,2,3];
console.log(Array.prototype.isPrototypeOf(arr)); //true

(6)Object.getPrototypeOf() 

(Object.getPrototypeOf()方法返回指定对象(arr)的原型,然后和Array的原型对比)

let arr = [1,2,3];
console.log(Object.getPrototypeOf(arr) == Array.prototype); //true

https://www.cnblogs.com/srqsl/p/16895063.html


你说到 typeof,能不能加一个限制条件达到判断条件

参考回答:
typeof 只能判断是 object,可以判断一下是否拥有数组的方法
// 1、剔除特殊的null
function TypeOf (obj) {
	if (typeof obj === 'object' && obj !== null) {
		return 'object';
	} else if (obj === null) {
		return 'null';
	}
	return typeof obj;
}
// 2、判断object中有数组
function hasArray (obj) {
	if (TypeOf(obj) === 'object') {
		for (let i in obj) {
			if (obj[i] instanceof Array) return true;
		}
	}
	return false;
}
let arr = [1, 2, 3]

hasArray(arr)

ES5之数据类型 - 简书


JS 实现跨域

跨域的原理
参考回答:
跨域,是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript 实施的安全限制,那么只要协议、域名、端口有任何一个不同,都被当作是不同的域。跨域原理,即是通过各种方式,避开浏览器的安全限制。

1、广义的跨域

跨域是指一个域下的文档或脚本试图去请求另一个域下的资源,这里跨域是广义的。

1.) 资源跳转: A链接、重定向、表单提交
2.) 资源嵌入: <link>、<script>、<img>、<frame>等dom标签,还有样式中background:url()、@font-face()等文件外链
3.) 脚本请求: js发起的ajax请求、dom和js对象的跨域操作等

2、同源策略

所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。

同源策略限制以下几种行为:

1.) Cookie、LocalStorage 和 IndexDB 无法读取
2.) DOM 和 Js对象无法获得
3.) AJAX 请求不能发送

3、JS 实现跨域

(1)JSONP跨域

原理:script标签引入js文件不受跨域影响。不仅如此,带src属性的标签都不受同源策略的影响。通过动态创建 script,再请求一个带参网址实现跨域通信。

(2)document.domain+ iframe 跨域

此方案仅限主域相同,子域不同的应用场景。两个页面都通过 js 强制设置 document.domain 为基础主域,就实现了同域。

(3)location.hash + iframe 跨域

a 欲与 b 跨域相互通信,通过中间页 c 来实现。 三个页面,不同域之间利用 iframe 的 location.hash 传值,相同域之间直接 js 访问来通信。

(4)window.name + iframe 跨域

通过 iframe 的 src 属性由外域转向本地域,跨域数据即由iframe 的 window.name 从外域传递到本地域。

(5)postMessage 跨域

可以跨域操作的 window 属性之一。

(6) CORS 跨域资源共享

服务端设置 Access-Control-Allow-Origin 即可,前端无须设置,若要带cookie 请求,前后端都需要设置。

(7)nginx代理跨域

启一个代理服务器,实现数据的转发

(8)Web Sockets跨域

web sockets原理:在JS创建了web socket之后,会有一个HTTP请求发送到浏览器以发起连接。取得服务器响应后,建立的连接会使用HTTP升级从HTTP协议交换为web sockt协议

以上,参考nginx - 前端常见跨域解决方案(全) - 个人文章 - SegmentFault 思否

js前端解决跨域的八种实现方案_javascript技巧_脚本之家


JS数据类型

八种,按照类型来分有原始数据类型和引用数据类型:

栈:原始数据类型:String、Number、Boolean、Null、Undefined、Symbol、BigInt

堆:引用数据类型:Object【Object是个大类,function函数、array数组、date日期...等都归属于Object】

其中 Symbol 和 BigInt 是ES6 中新增的数据类型:

Symbol是原始数据类型, 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。

// Symbol(字符串) 返回一个具有唯一的标识符,标识符是原始值
  const sym1 = Symbol('hello')
  const sym2 = Symbol('hello')
  console.log(sym1===sym2);//false

// const sym3 = new Symbol('hello')//用new调用Symbol会报错

const sym4 = Symbol() 
const sym5 = Symbol()
const obj = {
      x:1,
      y:2,
      [sym4]:'hello',
      [sym5]:'你好'
    }
 console.log(obj[sym4]); //hello
 console.log(obj[sym5]); //你好
简单介绍一下 symbol
参考回答:
Symbol 是 ES6 的新增属性,代表用给定名称作为唯一标识,这种类型的值可以这样创
建,let id=symbol(“id”)
Symbl 确保唯一,即使采用相同的名称,也会产生不同的值,我们创建一个字段,仅为
知道对应 symbol 的人能访问,使用 symbol 很有用,symbol 并不是 100%隐藏,有内置
方法 Object.getOwnPropertySymbols(obj)可以获得所有的 symbol。
也有一个方法 Reflect.ownKeys(obj)返回对象所有的键,包括 symbol。
所以并不是真正隐藏。但大多数库内置方法和语法结构遵循通用约定他们是隐藏的。

BigInt 是一种原始数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。

//字面量法
const bi1 = 100000n

//内置函数法
const bi2 = BigInt(100000) //100000n
const bi2 = BigInt('100000') //100000n


【图解系列】JS的数据类型,它们的区别? - 哔哩哔哩


内置对象与原生对象

内置(Build-in)对象与原生(Naitve)对象的区别在于:前者总是在引擎初始化阶段就被创建好的对象,是后者的一个子集;而后者包括了一些在运行过程中动态创建的对象。

原生对象(New后的对象)

内置对象(不需要New)

JS数据类型:null 和 undefined 有什么区别?

Null 只有一个值,是 null。不存在的对象。
Undefined 只有一个值,是undefined。没有初始化。undefined 是从 null 中派生出来的。
简单理解就是:undefined 是没有定义的,null 是定义了但是为空。

JS数据类型:null 不存在的原因是什么?如何解决?

不存在的原因是:
1、方法不存在
2、对象不存在
3、字符串变量不存在
4、接口类型对象没初始化
解决方法:
做判断处理的时候,放在设定值的最前面

null == undefined 为什么

要比较相等性之前,不能将 null 和 undefined 转换成其他任何值,但 null == undefined 会返回 true 。ECMAScript 规范中是这样定义的。

JS数据类型:== 和 === 有什么区别,什么场景下使用?

  1. JavaScript中存在隐式转换。等于操作符(==)在比较中会先进行类型转换,再确定操作数是否相等,全等运算符不会做类型转换
  2. null 和 undefined 比较,相等操作符(==)为true,全等为false
  3. 除了在比较对象属性为null或者undefined的情况下,我们可以使用相等操作符(==),其他情况建议一律使用全等操作符(===)

JS中的八大数据类型 - 简书


重排和重绘,讲讲看

参考回答:

重绘(repaint 或 redraw):

当盒子的位置、大小以及其他属性,例如颜色、字体大小等都确定下来之后,浏览器便把这些原色都按照各自的特性绘制一遍,将内容呈现在页面上。重绘是指一个元素外观的改变所触发的浏览器行为,浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。

触发重绘的条件:改变元素外观属性。如:color,background-color ,opacity等。

注意:table 及其内部元素可能需要多次计算才能确定好其在渲染树中节点的属性值,比同等元素要多花两倍时间,这就是我们尽量避免使用 table 布局页面的原因之一。

重排(重构/回流/reflow):

当渲染树中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建, 这就称为回流(reflow)。每个页面至少需要一次回流,就是在页面第一次加载的时候。

重绘和重排的关系:在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘。所以,

重绘不一定重排,但重排必然会重绘。

触发重排

  • 页面初始化、页面第一次渲染时
  • DOM树发生变化,增删结点时
  • Render树发生变化,比如padding的值发生改变时
  • 浏览器窗口的resize发生变化时
  • 获取元素的某些属性时
  • 字体大小改变

触发重绘

  • 触发重排一定能触发重绘,重绘也可以单独被触发
  • 背景色、颜色、透明度发生变化
  • 字体大小变化不止会触发重绘还会触发重排

如何减少重绘重排次数?

  • 用transform做形变和位移可以减少重排
  • 避免逐个修改结点样式,尽量一起修改,将逐步渲染转变为整体渲染
  • 使用DocumentFragment将需要多次修改的DOM元素缓存,最后一次性的append到真实的DOM树中进行渲染
  • 可以将需要多次修改的DOM元素设置display:none;等其他操作结束后再显示。

display:none;和visibility:hidden;的区别

display:none;会触发重绘和重排,隐藏后不占空间,产生了位置变化

visibility:hidden;只会触发重绘,不会触发重排,隐藏后占据空间,没有产生位置变化

【前端面试题】页面绘制-重绘和重排_页面重绘_二琳爱吃肉的博客-CSDN博客


不同数据类型的值的比较,是怎么转换的,有什么规则

链接: 不同数据类型的值的比较,是怎么转换的,有什么规则__牛客网
来源:牛客网

1) 数字和字符串、布尔类型、数组进行比较时,字符串(或布尔类型、或数组)先转换为数字(Number),再进行比较;

2) 字符串和布尔类型比较,进行比较的两个数据同时经过Number()的洗礼后再进行比较。数组和布尔类型的比较也如此。

3) undefined 除了和null进行非全等比较返回true,其它均返回false。null 除了和 undefined进行非全等比较返回true,其它均返回false。

null==undefined  //true

null===undefined //false

null 和 undefined 和其它值永远不相等

NaN 和其它值永不相等

4) 数组(或对象)和字符串进行比较时,数组(或对象)会转换成字符串再进行比较。


有一个游戏叫做 Flappy Bird,就是一只小鸟在飞,前面是无尽的沙漠,上下不断有钢管生成,你要躲避钢管。然后小明在玩这个游戏时候老是卡顿甚至崩溃,说出原因(3-5 个)以及解决办法(3-5 个)

参考回答:
原因可能是:
1.内存溢出问题。
2.资源过大问题。
3.资源加载问题。
4.canvas 绘制频率问题
解决办法:
1.针对内存溢出问题,我们应该在钢管离开可视区域后,销毁钢管,让垃圾收集器回
收钢管,因为不断生成的钢管不及时清理容易导致内存溢出游戏崩溃。
2.针对资源过大问题,我们应该选择图片文件大小更小的图片格式,比如使用 webp、
png 格式的图片,因为绘制图片需要较大计算量。
3.针对资源加载问题,我们应该在可视区域之前就预加载好资源,如果在可视区域生
成钢管的话,用户的体验就认为钢管是卡顿后才生成的,不流畅。
4.针对 canvas 绘制频率问题,我们应该需要知道大部分显示器刷新频率为 60 次/s,因
此游戏的每一帧绘制间隔时间需要小于 1000/60=16.7ms,才能让用户觉得不卡顿。
(注意因为这是单机游戏,所以回答与网络无关)

编写代码,满足以下条件:

(1)Hero("37er");
        执行结果为 Hi! This is 37er
(2)Hero("37er").kill(1).recover(30);
        执行结果为 Hi! This is 37er Kill 1 bug Recover 30 bloods
(3) Hero("37er").sleep(10).kill(2)
        执行结果为 Hi! This is 37er
        //等 待 10s 后 Kill 2 bugs //注意为 bugs (双斜线后的为提示信息,不需要打印)

参考答案:

function Hero(name){
    let o=new Object();
    o.name=name;
    o.time=0;
    console.log("Hi! This is "+o.name);
    o.kill=function(bugs) {
        if(bugs==1){
            console.log("Kill "+(bugs)+" bug");
        }else {
            setTimeout(function () {
                console.log("Kill " + (bugs) + " bugs");
            }, 1000 * this.time);
        }
    return o;
    };
    o.recover=function (bloods) {
        console.log("Recover "+(bloods)+" bloods");
        return o;
    }
    o.sleep=function (sleepTime) {
        o.time=sleepTime;
        return o;
    }
    return o;
}

Hero("37er");
Hero("37er").kill(1).recover(30);
Hero("37er").sleep(10).kill(2);

什么是按需加载

参考回答:
当用户触发了动作时才加载对应的功能。触发的动作,是要看具体的业务场景而言, 包括但不限于以下几个情况:鼠标点击、输入文字、拉动滚动条,鼠标移动、窗口大小更改等。加载的文件,可以是 JS、图片、CSS、HTML 等。

说一下什么是 virtual dom

参考回答:

Virtual DOM——虚拟节点

1、以前更新ui的方法——遍历查询 dom 树的方式

虚拟 dom 是相对于浏览器所渲染出来的真实 dom 的,在react,vue等技术出现之前,我们要改变页面展示的内容只能通过遍历查询 dom 树的方式找到需要修改的 dom 然后修改样式行为或者结构,来达到更新 ui 的目的。

这种方式相当消耗计算资源,因为每次查询 dom 几乎都需要遍历整颗 dom 树,如果建立一个与 dom 树对应的虚拟 dom 对象( js 对象),以对象嵌套的方式来表示 dom 树,那么每次 dom 的更改就变成了 js 对象的属性的更改,这样一来就能查找 js 对象的属性变化要比查询 dom 树的性能开销小。

2、现在更新ui的方法——前端框架中Virtual DOM的方式

Virtual DOM是根据dom结构生成对应的js对象,Js对象结构就是虚拟dom树。当有节点元素发生改变时,虚拟dom会先跟着变。新旧dom树对比,记录差异,把记录的差异更新到真正的dom'树中。

本质:虚拟dom树就是在js和dom之间做了一个缓存。


Vue 的 Virtual DOM 解决了什么问题


Vue 的 Virtual DOM 解决了以下问题:

提高渲染性能:Vue 通过在内存中创建虚拟 DOM 树,将修改操作批量处理并最小化 DOM 操作次数,从而减少了浏览器重排和重绘的开销,提高了性能。

简化代码复杂度:Vue 的 Virtual DOM 抽象了底层 DOM 的实现细节,让开发者可以更专注于业务逻辑和数据层面的编写和管理,从而简化了代码复杂度。

支持跨平台开发:由于 Virtual DOM 是一个独立于浏览器的抽象层,因此 Vue 可以通过不同的渲染引擎(如浏览器、Node.js、Weex 等)来渲染组件,支持跨多个平台的开发。

virtual Dom 全面理解虚拟Dom_虚拟dom解决了什么问题_叶落风尘的博客-CSDN博客


写一个函数,第一秒打印 1,第二秒打印 2

参考回答:

两个方法,第一个是用 let 块级作用域

for(let i=0;i<5;i++){
    setTimeout(function(){
        console.log(i)
    },1000*i)
}

第二个方法闭包+立即执行函数

for(var i=0;i<5;i++){
    (function(i){
        setTimeout(function(){
            console.log(i)
        },1000*i)
    })(i)
}

写个函数,可以转化短横线命名到驼峰命名

参考回答:

一、短横线命名到驼峰命名

方法1:采用数组的方法

  1. 通过split()方法 将字符串转化为一个数组
  2. toUpperCase()转化成大写
  3. slice()只有一个参数的时候,则会截取至数组的最后一个单元
  4. join()把数组中的所有元素放入一个字符串
function getCamelCase1(str) {
    var arr=str.split('-') // arr = ['user', 'name']
    return arr.map(function(item,index){
        if(index===0){
            return item
        }else{
            return item[0].toUpperCase()+item.slice(1)
        }
    }).join('') //把arr中的元素放入字符串
}
console.log( getCamelCase1( 'user-name' ) ); //userName

方法2:采用正则表达式

1、replace(regxp/substr, replacement),用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串

2、/表达式/[switch]

g:全局匹配

i:忽略大小写

gi:全局匹配+忽略大小写

/-([a-z])/g,匹配以-开头小写a-z任意字母结尾的正则,以下例子意思是将"-n"替换成"-N"

function getCamelCase2(str) {
    return str.replace(/-([a-z])/g,function(keb,item){
        return item.toUpperCase();
    } )
}
console.log( getCamelCase2( 'user-name' ) ); //userName

二、驼峰命名到短横线命名

方法1:采用数组的方法
item.toUpperCase()===item 把数组每一项字符串的每个字母转换成大写与原字符串比较,相等时就找到了"N"
function getKebabCase1(str){
    var arr=str.split('') // arr = ['user', 'Name']
    str=arr.map(function(item){
        if(item.toUpperCase()===item){ //找到"N"的位置
            return '-'+item.toLowerCase();
        }else{
            return item;
        }
    })
    return str.join( '' )
}
console.log(getKebabCase1('userName')) //user-name
方法2:采用正则表达式
将"N"替换成"-n"
function getKebabCase2(str){
    return str.replace(/[A-Z]/g, function(item) {
        return '-'+item.toLowerCase()
    })
}
console.log( getKebabCase2( 'userName' ) ) //user-name

JS 中 string 的 startwith 和 indexof 两种方法的区别

参考回答:

 startwith 函数

str.startsWith(searchString[, position])返回布尔值用于检测字符串是否以指定的前缀开始。

searchString要搜索的子字符串。
position(可选) 在 str 中搜索 searchString 的开始位置,默认值为 0,也就是真正的字符串开头处。
let str2 = "hello world";
let result = str2.startsWith("hello");
let result1 = str2.startsWith("h", 1);
let result2 = str2.indexOf("h");
console.log(result); // true
console.log(result1); // false
console.log(result2); // 0

Indexof 函数

indexof函数,返回索引值, 可返回某个指定字符串在字符串中首次出现的位置。

JS 字符串转数字的方法

参考回答:
通过函数 parseInt(),可解析一个字符串,并返回一个整数,语法为 parseInt (string ,radix)
string:被解析的字符串
radix:表示要解析的数字的基数,默认是十进制,如果 radix<2 或>36,则返回 NaN
 parseInt("1234blue"); // 1234
 parseInt("0xA"); // 10
 parseInt("22.5"); // 22
 parseInt("blue"); // NaN

JS 字符串转换成数字的三种方法_js字符串转数字_ljr 加油吖!的博客-CSDN博客


有了解过事件模型吗,DOM0 级和 DOM2 级有什么区别,DOM 的分级是什

首先,W3C DOM标准(DOM分级)先后有三个版本:

1级DOM、2级DOM、3级DOM

DOM事件模型有三个:

0级DOM中定义的DOM0级模型、2级DOM定义的DOM2级模型、IE事件模型

1. 0级DOM

0级DOM包含了一些基本事件和 “原始事件模型” ,在该模型中,事件不会传播,即没有事件流。这种方式所有浏览器都兼容,但是逻辑与显示并没有分离。

2. 1级DOM

前面已经说了,这个标准中,并没有定义出新的事件模型,仅仅是定义了HTML和XML文档的底层结构,不是我们讨论的重点,直接跳过。

3. 2级DOM

在这个标准中,添加了许多新的事件,定义了2级DOM事件模型,该模型指明了新的添加、移除事件监听的方式,还增添了事件流

2级DOM把新增的事件分为了5种类型:ui事件、鼠标事件、键盘事件、HTML事件、变动事件。

2级DOM事件模型将事件传播分为了捕获阶段,目标阶段,冒泡阶段三个阶段。

4. 3级DOM

再次增加了一些事件,提升交互能力。

IE事件模型

在该模型中,事件的传播没有捕获阶段,只有命中与冒泡阶段。

注意!在DOM事件模型中,事件类型没有on,而在IE事件模型中,事件类型需要加上on!


事件流

  • 捕获阶段:事件由最外层document向内层传播,一直传递到对应触发事件的元素
  • 命中事件:执行对应监听回调
  • 冒泡阶段:从命中元素向外层元素传播,一直传递到document

DOM 标准事件流的触发的先后顺序

先捕获再冒泡,点击 DOM 节点时,事件传播顺序:事件捕获阶段,从上往下传播,然后到达事件目标节点,最后是冒泡阶段,从下往上传播

 事件委托中addEventListener

addEventListener(事件名称,执行函数,事件类型)

第三个参数不写或者写false,表示默认使用事件冒泡,写true表示使用事件捕获

element.addEventListener("mousedown", func);  //事件冒泡

element.addEventListener("mousedown", func, true);  //事件捕获

第三个参数可以是对象类型或布尔值: 

element.addEventListener("mousedown", func, { passive: true }); 
element.addEventListener("mousedown", func, { capture: false }); 
  • object类型(options)包括三个布尔值选项:

    • capture: 默认值为false(即 使用事件冒泡).,true---使用事件捕获;
    • once: 默认值为false,是否只调用一次,true---会在调用后自动销毁listener
    • passive:不同浏览器默认值不同。true---listener永远不远调用preventDefault方法。根据规范,默认值为false. 但是chrome, Firefox等浏览器为了保证滚动时的性能,在Window,、Document、 Document.body上针对  touchstart  和  touchmove  事件将passive默认值改为了true, 保证了在页面滚动时不会因为自定义事件中调用了preventDefault而阻塞页面渲染。
  • bool类型(useCapture): 默认值为false(即 使用事件冒泡),与capture用法相同。

中参数 capture 可以指定该监听是添加在事件捕获阶段还是事件冒泡阶段,为 false 是事件冒泡,为 true 是事件捕获,并非所有的事件都支持冒泡,比如 focus,blur 等等,我们可以通过 event.bubbles 来判断

事件模型有三个常用方法:
  • event.stopPropagation:阻止捕获和冒泡阶段中,当前事件的进一步传播,
  • event.stopImmediatePropagetion,阻止调用相同事件的其他侦听器,
  • event.preventDefault,取消该事件(假如事件是可取消的)而不停止事件的进一步传播。
最后,对于执行顺序的问题,如果 DOM 节点同时绑定了两个事件监听函数,一个用于捕获,一个用于冒泡,那么两个事件的执行顺序真的是先捕获在冒泡吗,答案是否定的,绑定在被点击元素的事件是按照代码添加顺序执行的,其他函数是先捕获再冒泡。

target 和currentTarget 和 this

事件代理的核心,一是事件交给父元素处理的思想,还有一点就是对 e.target 的使用。

e.target 与 e.currentTarget 的区别:

e.target:返回真正的触发了事件的标签对象。这个会随着我们点击位置的不同,返回不同的标签对象,这个就和事件流有关
e.currentTarget:currentTarget = this,返回绑定事件的标签对象。显然这个是不会变化的,只要事件绑定好了,自然返回的就总是同一个标签


写一个 newBind 函数,完成 bind 的功能

Function.prototype是什么?

Function.prototype位于所有函数的原型链上,Function.prototype又通过_proto_指向Object.prototype,所以所有的函数既是Function的实例又是Object的实例,并分别从这两个内置对象继承了很多属性和方法。

bind()的原生实现分步解析

1:通过call,把类数组转换成一个真正的数组,让this能够指向arguments

call() 方法的第二个参数表示传递给slice的参数即截取数组的起始位置

以下两种结果是一样的

Array.prototype.slice.call(arguments)是用来将参数由类数组转换为真正的数组;
[].slice.call(arguments)是用来将参数由类数组转换为真正的数组;
slice()截取数组,
没参数默认从key0到最后一项,浅拷贝,不影响原数组

Array.prototype.slice.call(arguments,1)就是能够将具有length属性(这一点需要注意,必须包含length属性)的对象转换为数组,并使用数组的slice方法

    Function.prototype.myBind = function () {
      if (typeof this !== 'function') throw 'caller must be a function'
      let self = this // 这里是关键 用来和new出来的比较原型来判断是否为new出来的  当前函数对象
      let context = arguments[0]
      let args = Array.prototype.slice.call(arguments, 1) // 旧:参数
      let fn = function () {
        let fnArgs = Array.prototype.slice.call(arguments) // 新:参数
        // bind 函数的参数 + 延迟函数的参数
        // 用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
        self.apply(this instanceof self ? this : context, args.concat(fnArgs))
      }
      fn.prototype = Object.create(self.prototype) // 维护原型
      return fn
    }

js中bind()使用详情_js bind_曾皙的博客-CSDN博客

Array.prototype.slice.call(arguments,1)_Jqy_111的博客-CSDN博客


 • 自己实现一个 bind 函数

参考回答:
原理:通过 apply 或者 call 方法来实现。
(1)初始版本
Function.prototype.bind=function(obj,arg){
    var arg=Array.prototype.slice.call(arguments,1);
    var context=this;
    return function(newArg){
    arg=arg.concat(Array.prototype.slice.call(newArg));
    return context.apply(obj,arg);
    }
}

(2) 考虑到原型链

为什么要考虑?因为在 new 一个 bind 过生成的新函数的时候,必须的条件是要继承原
函数的原型
Function.prototype.bind=function(obj,arg){
    var arg=Array.prototype.slice.call(arguments,1);
    var context=this;
    var bound=function(newArg){
        arg=arg.concat(Array.prototype.slice.call(newArg));
        return context.apply(obj,arg);
    }
    var F=function(){}
    //这里需要一个寄生组合继承
    F.prototype=context.prototype;
    bound.prototype=new F();
    return bound;
}

 • 改变函数内部 this 指针的指向函数(bind,apply,call 的区别)

参考回答:

apply:调用一个对象的一个方法,用另一个对象替换当前对象。例如:B.apply(A,arguments);即 A 对象应用 B 对象的方法。
call:调用一个对象的一个方法,用另一个对象替换当前对象。例如:B.call(A, args1,args2);即 A 对象调用 B 对象的方法。
bind 除了返回是函数以外,这个函数不会马上执行,它的参数和 call 一样,例如:B.bind(A, args1,args2)() ;即 A 对象调用 B 对象的方法。

call 和 apply 是用来做什么?

参考回答:
Call 和 apply 的作用是一模一样的,只是传参的形式有区别而已
1、改变 this 的指向
2、借用别的对象的方法,
3、调用函数,因为 apply,call 方法会使函数立即执行


this 的指向 哪几种

参考回答:
  • 默认绑定:全局环境中,this 默认绑定到 window。
  • 隐式绑定:一般地,被直接对象所包含的函数调用时,也称为方法调用,this 隐式绑定到该直接对象。
  • 隐式丢失:隐式丢失是指被隐式绑定的函数丢失绑定对象,从而默认绑定到 window。
  • 显式绑定:通过 call()、apply()、bind()方法把对象绑定到 this 上,叫做显式绑定。
  • new 绑定:如果函数或者方法调用之前带有关键字 new,它就构成构造函数调用。对于this 绑定来说,称为 new 绑定。
  • 【1】构造函数通常不使用 return 关键字,它们通常初始化新对象,当构造函数的函数体执行完毕时,它会显式返回。在这种情况下,构造函数调用表达式的计算结果就是这个新对象的值。
  • 【2】如果构造函数使用 return 语句但没有指定返回值,或者返回一个原始值,那么这时将忽略返回值,同时使用这个新对象作为调用结果。
  • 【3】如果构造函数显式地使用 return 语句返回一个对象,那么调用表达式的值就是这个对象。

this的详细分析加案例_曾皙的博客-CSDN博客


怎么获得对象上的属性

1、只能获取可枚举属性的:

for...in循环:只遍历对象自身的和继承的可枚举的属性。

const obj1 = {name: 'nick', age: 13, sex: '男'}
for (const key in obj1) {
	if (Object.hasOwnProperty.call(obj1, key)) {
		const element = obj1[key];
		console.log(element); //nick 13 男
	}
}

Object.keys():返回对象自身的所有可枚举的属性的键名。

const obj1 = { name: 'nick', age: 13, sex: '男' };
Object.keys(obj1); // ['name', 'age', 'sex']

JSON.stringify():只串行化对象自身的可枚举的属性。

const obj1 = { name: 'nick', age: 13, sex: '男' };
JSON.stringify(obj1) // '{"name":"nick","age":13,"sex":"男"}'

Object.assign(): 忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性。

const obj1 = { name: 'nick', age: 13, sex: '男' };
Object.assign(obj1) // {name: 'nick', age: 13, sex: '男'}

Object.values():返回 对象 自身及其原型链上可枚举的属性值的数组

2、包括可枚举和不可枚举的属性

Object.getOwnPropertyNames:主要用于返回对象的自有属性

const obj1 = { name: 'nick', age: 13, sex: '男' };
Object.getOwnPropertyNames(obj1) //  ['name', 'age', 'sex']

Reflect.ownKeys():返回自身所有属性键名,包括不可枚举类型,不包括继承的属性

3、只包括不可枚举属性

Object.getOwnPropertySymbols():获取指定对象的所有Symbol属性名

Object 的各类方法 - JavaShuo

JS获取对象属性API汇总(可/不可枚举、symbol)(建议收藏)_js 如何读取对象中不可枚举的属性_码上十七的博客-CSDN博客


  • 枚举属性的作用

什么是枚举?枚举是指对象中的属性是否可以遍历出来,再简单点说就是属性是否可以被列举出来。

 枚举属性主要会影响几个方法

ES5中:
    for...in                         //只遍历对象自身的和继承的可枚举的属性
    Object.keys()             //返回对象自身的所有可枚举的属性的键名
    JSON.stringify           //JSON.stringify() 方法用于将 JavaScript 值转换为 JSON 字符串。
 
ES6中:
    Object.assign()          //会忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性。

    可以看出来这些都是可以遍历对象的方法,而这四个操作中只有for...in中会返回继承的属性


给出以下代码,输出的结果是什么?原因?

 
for(var i=0;i<5;i++){ 
    setTimeout(function(){ 
        console.log(i);  // 5 5 5 5 5
    },1000); 
}
console.log(i) //5
参考回答:
在一秒后输出 5 个 5。

因为在主程序中遇到异步事件的时候,会将异步事件放到事件队列里面去,等到主程序执行完之后,在去执行事件队列里面的事件,此时主程序已经执行完,i变为5,由于每次输出都是i的值,所以是5个5。


给两个构造函数 A 和 B,如何实现 A 继承 B?

寄生组合式继承

function Foo(name) {
    this.name = name;
}
Foo.prototype.age = function() {
    console.log('父类:' + this.name);
};
Foo.prototype.obj = function() {
    console.log('hello world');
};
function fn (name) {
    Foo.call(this,name);  
}
fn.prototype = Object.create(Foo.prototype);
fn.prototype.constructor = fn;
fn.prototype.age = function() {
    console.log('子类:' + this.name);
};
fn.prototype.set = function() {
    console.log('set si es5');
};
var a = new fn('lisi');
a.age();
a.obj();
a.set();

https://www.cnblogs.com/boses/p/9588088.html


如果已经有三个 promise,A、B 和 C,想串行执行,该怎么写?

// promise
A.then(B).then(C).catch(...)

// async/await
(async ()=>{
    await a();
    await b();
    await c();
})()

知道 private 和 public 吗

参考回答:
public:public 表明该数据成员、成员函数是对所有用户开放的,所有用户都可以直
接进行调用
private:private 表示私有,私有的意思就是除了 class 自己之外,任何人都不可以
直接使用

基础的 js

Function.prototype.a = 1;
Object.prototype.b = 2;
function A() {}
var a = new A();
console.log(a.a, a.b); // undefined, 2
console.log(A.a, A.b); // 1, 2

JS 加载过程阻塞,解决方法。

参考回答:
<script src=""  async>
<script src=""  defer>

指定 script 标签的 async 属性。

async是异步执行,defer是延迟加载,这两个只在外联script中起作用。
async:脚本相对于页面的其余部分异步地执行(当页面继续进行解析时,脚本将被执行)
defer:脚本将在页面完成解析时执行

JavaScript 中的轮播实现原理?假如一个页面上有两个轮播,你会怎么实现?

参考回答:
图片轮播的原理就是图片排成一行,然后准备一个只有一张图片大小的容器,对这个容器设置超出部分隐藏,在控制定时器来让这些图片整体左移或右移,这样呈现出来的效果就是图片在轮播了。
如果有两个轮播,可封装一个轮播组件,供两处调用。

怎么实现一个计算一年中有多少周?

首先你得知道是不是闰年,也就是一年是 365 还是 366. 其次你得知道当年 1 月 1 号是周几。假如是周五,一年 365 天把 1 号 2 号 3 号减去,
也就是把第一个不到一周的天数减去等于 362
还得知道最后一天是周几,加入是周五,需要把周一到周五减去,也就是 362-5=357.
正常情况 357 这个数计算出来是 7 的倍数。357/7=51 。即为周数。

箭头函数和 function 有什么区别

参考回答:
箭头函数根本就没有绑定自己的 this,在箭头函数中调用 this 时,仅仅是简单的沿着作用域链向上寻找,找到最近的一个 this 拿来使用
function fnnn () {
	console.log(this);  // {name: 'chel'}
	return () => {
		console.log(this);  // {name: 'chel'}
	}
}

const aaa = { name: 'chel' }
const resFn = fnnn.call(aaa)
resFn()

箭头函数面试题

let objss = {
	age: 20,
	say: () => {
		console.log(this.age)  // undefined
	}
}
objss.say()

输出结果是undefined,因为此时的this是window


new 操作符原理 

1. 创建一个类的实例:创建一个空对象 obj,然后把这个空对象的__proto__设置为构造函数的 prototype。
2. 初始化实例:构造函数被传入参数并调用,关键字 this 被设定指向该实例 obj。
3. 返回实例 obj。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值