JavaScript面试题汇总
1、ES6新特性
- let,const
- 变量解构赋值
- 模板字符串 (‘${}’)
- 箭头函数(=>)
- 扩展运算符(…)
- 模块化(import/export)
- 类(class/extends)
- Promise,Proxy,Map,Set,Generator,Reflect。。。
2、对象的创建方式
let obj = { };// 1、对象字面量
// 2、通过构造函数创建
let obj = new Object(); // Object构造函数
let arrObj = new Array(); // 其他的内置对象构造函数
function People(name, age) {
this.name = name;
this.age = age;
}
let obj = new People('张三', 18); // 自定义构造函数
// 3、使用Object.create(protoObj, propertiesObject),需要指定原型对象
let people1 = {name: 'myName', age: 18}; // 原型对象,可为null
let obj = Object.create(people1, {
sex: {
configurable: true,
enumerable: true,
writable: true,
value: '男'
}
});
console.log(obj, obj.name, obj.age);
console.log(Object.keys(obj));
// 4、使用Object.defineProperties(obj, propertiesObject)
let obj = {};// 创建一个空对象
let newObj = Object.defineProperties(obj, {
name: {
enumerable: true,
value: 'myName',
},
getName: {
enumerable: true,
value: function() {
return this.name;
}
},
})
console.log(newObj);
3、怎么获取一个对象的键,或值,或键值对。
// 对象本身是不可迭代的(没有默认的[symbol.iterator]()方法)
// 所以不能直接使用for of获得对象各属性的值。
let obj = {a: 1, b: '2'};
let keys = Object.keys(obj);
let values = Object.values(obj);
let entries = Object.entries(obj);
console.log(keys, values, entries);
注意:这三个方法都返回一个数组,Object.entries()返回一个二维数组。
4、为什么typeof null是Object
因为在JavaScript
中,不同的对象都是使用二进制存储的,如果二进制前三位都是0的话,系统会判断为是Object
类型,而null的二进制全是0,自然也就判断为Object
其他五种标识位:
000 对象
001 整型
010 双精度类型
100 字符串
110 布尔类型
5、JS数据类型
分为基本数据类型和引用数据类型。
基本数据类型有7种:Null,Undefined,String,Number,Boolean,Symbol,BigInt。
引用数据类型:Object。
6、null和undefined的区别
undefined 代表的含义是未定义,null 代表的含义是空对象。一般变量声明了但未赋值的时候会返回undefined,null主要用于赋值给一些可能会返回对象的变量,作为初始值。
使用双等号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。
7、原型、原型链
原型是指每个构造函数都有自己对应的原型对象,通过构造函数.prototype
表示该原型对象,例如
Object.prototype; // Object的原型对象
String.prototype; // String的原型对象
。。。
注意:所有构造函数(创建对象的包装器)的原型为Function.prototype(而非它们对应的原型对象):
// 传入的构造函数会被看成一个函数对象,该对象的原型自然就是Function.prototype。
Object.getPrototypeOf(Object) === Function.prototype
Object.getPrototypeOf(String) === Function.prototype
Object.getPrototypeOf(Fn) === Function.prototype
。。。
原型链由一系列构造函数的原型对象构成,原型链的终点为null,null下来就是Object.prototype。
例如:
function Fn() {}
let obj = new Fn();
console.log(Object.getPrototypeOf(obj) === Fn.prototype);// true
console.log(Object.getPrototypeOf(Fn.prototype) === Object.prototype);// true
console.log(Object.getPrototypeOf(Object.prototype) === null);// true
8、判断数据类型的方法
1、typeof
typeof
原理:不同的对象在底层都表示为二进制,在Javascript中二进制低三位存储其类型信息。
- 000: 对象
- 001: 整数
- 010: 浮点数
- 100:字符串
- 110: 布尔
typeof 可判断出基本数据类型(除了null),几乎所有引用类型的对象都返回object
:
typeof 123;// number
typeof '123';// string
typeof true;// boolean
typeof undefined;// undefined
typeof Symbol('a');// symbol
typeof 123n;// bigint
typeof null;// object,因为null的二进制表示全为0
typeof function() {};// function,不是object,是个例外
typeof new Date();// object
typeof new RegExp();// object
...
2、instanceof
instanceof
沿着原型链往上查找,判断一个对象是否是原型链上某个构造函数的实例,是则返回true,否则返回false。
function Fn() {}
let instance = new Fn();
instance instanceof Fn;// true
Fn instanceof Function;// true
Function instanceof Object;// true
String instanceof Object;// true
3、Object.prototype.toString.call()
Object.prototype.toString.call()是判断类型的最可靠方式。可区分各种引用数据类型,返回string类型:
Object.prototype.toString.call([]);// [object Array]
Object.prototype.toString.call({});// [object Object]
Object.prototype.toString.call(new Date());// [object Date]
Object.prototype.toString.call(new Map());// [object Map]
Object.prototype.toString.call(new Set());// [object Set]
Object.prototype.toString.call(new Error());// [object Error]
Object.prototype.toString.call('123');// [object String]
Object.prototype.toString.call(123);// [object Number]
...
let gen = function* GeneratorFn() {}
typeof gen;// function
Object.prototype.toString.call(gen);// [object GeneratorFunction]
let asyncFn = async function() {}
typeof asyncFn;// function
Object.prototype.toString.call(asyncFn);// [object AsyncFunction]
9、ES6新增Symbol类型
symbol实例表示独一无二的值,最大的用途就是用来定义对象唯一的属性名,能保证不会出现同名的属性。
定义及使用symbol实例:
let symbol1 = Symbol('desc');
typeof symbol1;// symbol
let obj = {
[symbol1]: 'unique';// 对象的唯一键
}
// 可迭代对象
let iterableObj = {
[Symbol.iterator]: function() {
return {
next() {
return {
value: 1,
done: false
}
}
}
}
}
注意:
1、symbol属性名不能被枚举。
2、Symbol 函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述。
3、Symbol()和Symbol.for()的区别:
Symbol.for()
与Symbol()
这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局注册表中供搜索,后者不会。Symbol.for()
不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key
是否已经存在,如果不存在才会新建一个值。
10、箭头函数的特性
1、箭头函数没有自己的this,会继承父作用域的this值。
2、箭头函数没有arguments参数对象。
3、不能用作构造函数。
11、Promise对象
Promise是一种异步编程解决方案。
Promise 是一个构造函数,接收一个函数作为参数,返回一个 Promise 实例。
let promise = new Promise((resolve, reject) => {
resolve('value');
// or
reject('error');
})
promise.then((value) => {
console.log(value);// 'value'
})
// or
promise.catch((error) => {
console.log(error);// 'error'
})
一个 Promise 实例有三种状态,分别是 pending(待定)、fulfilled(已兑现) 和 rejected(已拒绝),状态的改变是通过 resolve() 和 reject() 函数来实现的,并且状态一经改变,就无法再被改变了。
常用实例方法:then(),catch(),finally()
常用静态方法:
Promise.resolve(),Promise.reject();
需传入可迭代对象的方法(4个):
Promise.all(),Promise.allSettled(), Promise.any(),Promise.race()。
12、async function 和 await
async function声明一个异步函数,可配合await使用。
await后跟一个Promise对象,await表达式返回一个Promise已兑现或已拒绝的值,如果await后不是Promise对象,则表达式直接返回该值:
async function asyncFn() {
let res = await Promise.resolve(10);
let res1 = await 20;
console.log(res);// 10
console.log(res1);// 20
}
async…await的作用:简化promise的链式调用,看起来更简洁易懂。
13、Proxy对象
Proxy 对象用于创建一个目标对象的代理对象,从而实现基本操作的拦截和自定义。
在通过 Proxy 构造函数生成实例对象时,需要提供 target和handler 这两个参数。 target是目标对象, handler 是一个包含一些特定方法的对象,声明了代理 target 的指定行为。
handler对象的方法:
handler.getPrototypeOf();
handler.setPrototypeOf();
handler.defineProperty();
handler.get();
handler.set();
handler.ownKeys();
...
14、new操作符做了什么
new
关键字会进行如下的操作:
- 创建一个空的JavaScript对象
{}
; - 为步骤1新创建的对象添加属性
__proto__
,将该属性链接至构造函数的原型对象 ; - 将步骤1新创建的对象作为构造函数
this
的值,执行构造函数中的代码。 - 如果该函数没有返回对象,则返回
this
。
function People(name, age) {
this.name = name;
this.age = age;
}
let newObj = new People('zhang', 20);
// 在内部执行了如下:
let obj = {};
obj.__proto__ = People.prototype;
People.call(obj);// 给创建的空对象添加属性。
return obj;
15、JS实现继承的方式
1、原型链继承
将父类创建的实例作为子类构造函数对应的原型对象,这样子类创建的实例就可以访问父类构造函数的属性和原型对象的方法了。
function Parent() {
this.prop = 'value';
this.sharedProp = {
a: 1,
b: '2'
}
}
Parent.prototype.method = function() {
return this.prop;
}
function Child() {}
Child.prototype = new Parent();// 子类构造函数对应的原型对象被赋值为父类创建的实例
let child1 = new Child();
let child2 = new Child();
console.log(child1.prop);// 'value'
console.log(child2.sharedProp);// {a:1, b:'2'}
child1.sharedProp.c = 'newProp';
console.log(child2.sharedProp);// {a:1, b:'2', c:'newProp'}
缺点:子类的所有实例会共享父类的引用类型属性,且不能在创建实例时传参。
2、构造函数继承
在子类构造函数中调用父类构造函数,并在调用时指定 this 值为子类构造函数,这样子类创建的对象就会拥有父类构造函数中的属性了。
function Parent(name) {
this.info = {
name,
age: 20
}
}
function Child(name) {
Parent.call(this, name);// 调用父类构造函数并指定this
}
let child1 = new Child('zhang');
let child2 = new Child('li');
console.log(child1.info);
console.log(child2.info);
child2.info.newProp = 'newValue';
console.log(child1.info);
注意:构造函数继承,创建实例时可传参,子类实例间不会共享父类的引用类型属性,因为每次创建实例时都会调用父类构造函数并返回新的引用类型数据(新的对象属性)。
缺点:子类不能访问父类原型对象上的方法。
3、组合继承
组合继承综合了原型链继承和构造函数继承:
function Parent(name) {
this.name = name;
this.infoArr = [22, 180, 120];
}
Parent.prototype.sayName = function () {
return this.name;
}
function Child(name) {
Parent.call(this, name);// 构造函数继承
}
Child.prototype = new Parent();// 原型链继承
let child1 = new Child('Red');
let child2 = new Child('black');
console.log(child1);
console.log(child2.sayName());
child1.infoArr.push('newValue');
console.log(child1.infoArr);
console.log(child2.infoArr);
4、类继承
ES6新增类继承:
class Parent{
constructor(name) {
this.name = name;
this.info = {
a: '1',
b: 2
}
}
sayName() {
return this.name;
}
}
class Child extends Parent{
constructor(name, age) {
super(name);
this.age = age;
}
showAge() {
return this.age;
}
}
let child1 = new Child('blue', 20);
let child2 = new Child('pink', 22);
console.log(child1.sayName());// blue
console.log(child2.showAge());// 22
child1.info.newProp = 'newValue';
console.log(child1.info);
console.log(child2.info);
16、JS是单线程的,为什么可以有异步任务?
JS是单线程的,但是浏览器是多线程的,JS代码通常是在浏览器中运行的。
浏览器通常有以下几个线程:
- 渲染引擎线程:该线程负责页面的渲染
- JS引擎线程:负责JS的解析和执行
- 定时器线程:处理定时事件,比如setTimeout, setInterval
- 事件触发线程:处理DOM事件
- 异步http请求线程:处理http请求
17、图片懒加载和图片预加载
1、图片懒加载
核心逻辑:判断当前图片是否到了可视区域。
2、图片预加载
采用异步的方式加载图片,在后面偷偷加载。
18、cookies、sessionStorage、localStorage的区别
三者都是用于浏览器存储。
cookie由服务器设置,在客户端存储,然后每次发起同源请求时,发送给服务器端。cookie 最多能存储 4 k 数据。
localStorage、sessionStorage均能存储5M左右数据。
localStorage:除非手动删除,否则不会失效。
sessionStorage:关闭当前窗口则失效。
19、数组常用方法(28个)
// 1、静态方法
Array.from(iterable);// 将可迭代对象转换为一个新数组
Array.isArray(obj);// 判断对象是否是一个数组,是则返回true,否则返回false
Array.of(element1, element2, ...);// 返回一个包含传入元素的数组
// 2、实例方法
// 传入元素的方法
includes(element); // 判断传入元素是否存在于数组中,是则返回true,否则返回false
indexOf(element); // 返回匹配传入元素的第一个元素的索引,没有则返回-1。
push(element1, element2, ...);// 向数组末尾添加元素,返回数组长度。
unshift(element1, element2, ...);// 向数组开头添加元素,返回数组长度
// 传入数组的方法
arr1.concat(arr2, arrN, ...);// 合并两个或多个数组,返回合并后的新数组
// 不需传参的方法
pop();// 删除数组末尾元素
shift();// 删除数组第一个元素
reverse(); //反转原数组,该方法改变原数组,返回反转后的数组
// 返回Array Iterator的三个方法
entries();
keys();
values();
// 返回字符串的方法
join('分隔符');// 用指定分隔符分隔数组的每一个元素形成一个字符串,并返回该字符串
toString();// 返回一个表示指定的数组及其元素的字符串。
// 需传入测试函数的方法
every((element) => {});// 当数组的所有元素通过测试函数时返回true,空数组调用时返回true。
some((element) => {});// 只要数组有一个元素通过测试函数就会返回true,空数组返回false。
filter((element) => {}); // 返回一个包含通过测试函数的元素的新数组。
find((element) => {}); // 返回通过测试函数的第一个元素值,没有则返回undefined。
findIndex((element) => {}); // 返回通过测试函数的第一个元素的索引,没有则返回-1。
// 需传入一个所有元素都会执行的函数的方法
forEach((element, index, array) => {}); // 返回undefined
map((element, index, array) => {}); // 返回一个新数组
// 其他特殊方法
1、flat(int/infinity);// 扁平化一个数组,可指定层数
2、reduce((result, currentEle, index, array) => {}, initialValue);// 将数组每个元素按指定方式汇总为一个值并返回,可选的initialValue作为第一次调用回调时的result值。
3、slice(begin, end);// 返回一个新数组,该数组是原数组特定索引区间的浅拷贝(如果原数组元素是对象引用,则该对象的改变会影响到所有相关数组),end为可选参数,且结果不包含end索引元素
4、splice(begin, deleteCount, element1, element2, ...);// 此方法改变原数组,从指定索引处删除元素、添加元素,返回被删除元素组成的数组。
5、sort([compareFunction]);// 按给定函数排序数组元素,若不传递函数参数,则默认按元素字符串形式的unicode值排序
20、字符串常用方法(15个)
// 实例方法
// 字符串替换
replace(regexp|str, newStr); //将第一个参数匹配到的第一个字符替换为指定的新字符,如果第一个参数为正则表达式并带有g标志,则替换所有匹配的字符。返回一个新的字符串。
// 需传入正则表达式的方法
match(regexp);// 返回一个包含匹配结果字符的数组
// 传入字符串的方法
includes(str);// 判断传入的字符串是否是调用者的子字符串,是则返回true,否则返回false。
indexOf(str);// 返回参数字符串第一次出现的索引,没有则返回-1。不传参时默认参数为'undefined'。
startsWith(str, [position]);// 判断当前字符串是否以给定的子字符串开头,是则返回true,否则返回false。可指定开始查找的位置position。
// 截取子字符串
slice(begin, end);//返回一个新的字符串(不包括end索引处)
substring(begin, end);// 同上
// 返回数组的方法
split('分隔符');// 返回一个包含子字符串的数组,数组元素由分隔符决定。若参数为空字符串'',则结果数组的每个元素即为一个字符,若不传参,则结果数组只包含一个元素,该元素即为原字符串
// 不需传参的方法
toLowerCase();// 返回一个新的全为小写的字符串。
toUpperCase();// 返回一个新的全为大写的字符串。
trim();// 返回一个新的已去除两端空白字符的字符串。
trimEnd();// 返回一个新的已去除末端空白字符的字符串。
trimStart();// 返回一个新的已去除开头空白字符的字符串。
// 其他方法
repeat(int);// 返回一个新的字符串,该字符串是原字符串重复指定次数后的字符串。
charAt(index);// 返回一个在指定索引处的字符。若index大于 字符串长度-1 ,则返回空字符串'',不提供参数则使用0。
21、对象常用方法(26个)
// 1、静态方法(21)
// 配置对象的属性(2)
Object.defineProperty(obj, prop, descriptor);// 在一个对象上定义一个新属性或修改一个现有属性,并返回该对象
Object.defineProperties(obj, propsObj);// 在一个对象上定义新的属性或修改现有属性,并返回该对象
// 返回数组的方法(3)
Object.entries(obj);// 返回一个二维数组
Object.keys(obj);
Object.values(obj);
// 使对象变得不可扩展(3)
Object.preventExtensions(obj);// 使对象不能再添加新属性
Object.freeze(obj);// 冻结对象,使对象及对象的原型都不能被改变(添加、删除、属性的配置、修改值),返回该对象。
Object.seal(obj);// 密封对象,使对象不能添加新属性,已有属性变得不可配置,返回该对象。
// 与对象自有属性相关(4)
Object.getOwnPropertyDescriptor(obj, prop);// 返回对象一个自有属性对应的属性描述符对象。
Object.getOwnPropertyDescriptors(obj);// 返回一个对象,该对象包含指定对象的所有自身属性及其描述符。
Object.getOwnPropertyNames(obj);// 返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组。
Object.getOwnPropertySymbols(obj);// 返回一个包含给定对象自身的所有 Symbol 属性的数组。
// 与原型对象相关(3)
Object.getPrototypeOf(obj);// 返回给定对象的原型对象
Object.setPrototypeOf(obj, prototype);// 设置一个指定对象的原型对象
Object.create(prototype, propsObj);// 用指定的原型对象和属性创建一个新对象。
// 返回布尔值的方法(4)
Object.is(value1, value2);// 判断两个值是否相等,不进行隐式类型转换,与===类似,区别在于:此方法NaN与NaN返回true,+0和-0返回false。
Object.isExtensible(obj);// 判断一个对象是否是可扩展的,对象默认是可扩展的。
Object.isFrozen(obj);// 判断一个对象是否已被冻结,一个不可扩展的空对象同时也是一个已冻结对象
Object.isSealed(obj);// 判断一个对象是否已被密封,一个不可扩展的空对象同时也是一个已密封对象
// 其他方法(2)
Object.assign(target, obj1, obj2, ...);// 将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。
Object.fromEntries(iterable);// 把键值对列表(Map对象或二维数组)转换为新对象,返回该新对象
// 2、实例方法(5)
// 返回布尔值的方法(3)
hasOwnProperty(prop);// 判断对象自身属性中是否具有指定的属性
isPrototypeOf(obj);// 判断调用者是否存在于指定对象的原型链上
propertyIsEnumerable(prop);// 判断指定的属性是否可枚举
// 不需传参的方法(2)
toString();// 返回对象的字符串表示,常用来区分各种引用类型
valueOf();// 返回对象的原始值
22、作用域、作用域链
**作用域:**变量和函数的可使用范围。
js一般分为全局作用域和局部作用域,函数会形成局部作用域。
每一个作用域都有与其相对应的一个变量对象,该变量对象包含该作用域定义的变量,在局部作用域中(函数),该变量对象包含的是函数的参数、arguments对象以及定义在该函数内的变量。
作用域链就是由与每个作用域相关联的变量对象组成的,当访问一个变量时,会从当前作用域的变量对象开始往外部作用域变量对象查找,直到全局作用域变量对象为止。
23、防抖和节流
防抖和节流都是优化性能的手段,主要是通过定时器实现的。
- 防抖:就是一定时间内重复触发事件时,只会执行最后一次任务;
- 节流:就是一定时间间隔内重复触发事件时,只执行一次 ;
防抖的应用场景,最常见的就是页面滚动条监听的例子。
节流应用的实际场景是,根据文本框中输入的内容自动请求后台数据。
24、为什么 0.1 + 0.2 !== 0.3?
因为0.1和0.2对应的二进制浮点数存储时会造成精度缺失的问题,所以计算结果存在误差。
25、垃圾回收机制
垃圾回收算法主要有两种:
1、引用计数
如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
限制:循环引用问题。
2、标记清除
代码回收规则如下:
1.全局变量不会被回收。
2.局部变量会被回收,也就是函数一旦运行完以后,函数内部的东西都会被销毁。
3.只要被另外一个作用域所引用就不会被回收
26、事件循环
浏览器事件循环
1、js引擎将所有代码放入执行栈,并依次弹出并执行,这些任务有的是同步有的是异步(宏任务或微任务)。
2、如果在执行栈中代码时发现宏任务则交给浏览器相应的线程去处理,浏览器线程在正确的时机(比如定时器最短延迟时间)将宏任务的消息(或称之为回调函数)推入宏任务队列。而宏任务队列中的任务只有执行栈为空时才会执行。
3、如果执行栈中的代码时发现微任务则推入微任务队列,和宏任务队列一样,微任务队列的任务也在执行栈为空时才会执行,但是微任务始终比宏任务先执行。
4、当执行栈为空时,eventLoop转到微任务队列处,依次弹出首个任务放入执行栈并执行,如果在执行的过程中又有微任务产生则推入队列末尾,这样循环直到微任务队列为空。
5、当执行栈和微任务队列都为空时,eventLoop转到宏任务队列,并取出队首的任务放入执行栈执行。需要注意的是宏任务每次循环只执行一个。
6、重复1-5过。直到栈和队列都为空时,代码执行结束。引擎休眠等待直至下次任务出现。
例子:
console.log(1);
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(3)
});
});
new Promise((resolve, reject) => {
console.log(4)
resolve(5)
}).then((data) => {
console.log(data);
Promise.resolve()
.then(() => {
console.log(6)
})
.then(() => {
console.log(7)
setTimeout(() => {
console.log(8)
}, 0);
});
})
setTimeout(() => {
console.log(9)
}, 0);
console.log(10)
// 输出结果
// 1 4 10 5 6 7 2 3 9 8
注意:
1、宏任务每次只取一个,执行之后马上执行微任务。
2、微任务会依次执行,直到微任务队列为空。
Node事件循环
NodeJS中执行宏队列的回调任务有6个阶段,按如下方式依次执行:
1、timers阶段:这个阶段执行setTimeout和setInterval预定的callback。
2、callback阶段:执行除了close事件的callbacks、被timers设定的callbacks、setImmediate()设定的callbacks这些之外的callbacks。
3、idle, prepare阶段:仅node内部使用。
4、poll(轮询)阶段:获取新的I/O事件,适当的条件下node将阻塞在这里。
5、check阶段:执行setImmediate()设定的callbacks。
6、close callbacks阶段:执行socket.on(‘close’, …)这些callbacks。
宏队列有4个,各种类型的任务主要集中在以下四个队列之中:
- Timers Queue
- IO Callbacks Queue
- Check Queue
- Close Callbacks Queue
微队列主要有2个,不同的微任务放在不同的微队列中:
- Next Tick Queue:是放置process.nextTick(callback)的回调任务的
- Other Micro Queue:放置其他microtask,比如Promise等
Node的 EventLoop的具体流程:
1、执行全局Script的同步代码。
2、执行microtask微任务,先执行所有Next Tick Queue中的所有任务,再执行Other Microtask Queue中的所有任务。
3、执行macrotask宏任务,共6个阶段,从第1个阶段开始执行相应每一个阶段macrotask中的所有任务,注意,这里是所有每个阶段宏任务队列的所有任务,在浏览器的Event Loop中是只取宏队列的第一个任务出来执行,每一个阶段的macrotask任务执行完毕后,开始执行微任务,也就是步骤2。
4、Timers Queue -> 步骤2 -> I/O Queue -> 步骤2 -> Check Queue -> 步骤2 -> Close Callback Queue -> 步骤2 -> Timers Queue …
5、重复1 - 4过程。
例子:
console.log(0);
setTimeout(() => { // callback1
console.log(1);
setTimeout(() => { // callback2
console.log(2);
}, 0);
setImmediate(() => { // callback3
console.log(3);
})
process.nextTick(() => { // callback4
console.log(4);
})
}, 0);
setImmediate(() => { // callback5
console.log(5);
process.nextTick(() => { // callback6
console.log(6);
})
})
setTimeout(() => { // callback7
console.log(7);
process.nextTick(() => { // callback8
console.log(8);
})
}, 0);
process.nextTick(() => { // callback9
console.log(9);
})
console.log(10);
// 输出
// 0 10 9 1 4 7 8 5 6 3 2
总结
1、事件循环是 浏览器 和 Node 执行JS代码的核心机制,但浏览器 和 NodeJS事件循环的实现机制有些不同。
2、浏览器事件循环有一个宏队列,一个微队列,且微队列在执行过程中一个接一个执行一直到队列为空,宏队列只取队首的一个任务放入执行栈执行,执行过后接着执行微队列,并构成循环。
3、NodeJS事件循环有四个宏队列,两个微队列,微队列执行方式和浏览器的类似,先执行Next Tick Queue所有任务,再执行Other Microtask Queue所有任务。 但宏队列执行时会依次执行队列中的每个任务直至队为空才开始再次执行微队列任务。
4、MacroTask包括: setTimeout、setInterval、 setImmediate(Node)、requestAnimation(浏览器)、IO、UI rendering
5、Microtask包括: process.nextTick(Node)、Promise、Object.observe、MutationObserver
27、对象的深浅拷贝
1、浅拷贝
概念:将一个对象的属性及其对应值拷贝给另一个对象
// 1、直接赋值
let a;
let b = {prop: value};
a = b;
// 2、Object.assign({}, obj)
let b = {
c: {
prop: "value"
}
};
let a = Object.assign({}, b);
// 3、展开运算符...
let obj = {
a: {
prop: 'value'
}
}
newObj = {...obj};
2、深拷贝
概念:将一个对象的属性及其值(引用类型除外)拷贝给另一个对象,对于引用类型的值,拷贝时会创建新的栈地址。
// 1、JSON.parse(JSON.stringify(obj))
let obj = {
a: {
prop: 'value'
}
};
let newObj = JSON.parse(JSON.stringify(obj));
// 2、递归
function deep(obj) {
let newObj = {};
for(let key in obj) {
if(typeof obj[key] === 'object'){
newObj[key] = deep(obj[key]);
}
else{
newObj[key] = obj[key];
}
}
return newObj;
}
使用JSON对象进行深拷贝的缺陷:JSON对象实现拷贝时忽略function类型属性,…