Skip to main content

JS全部面试题69题

1、JavaScript有哪些数据类型,它们的区别?

JavaScript共有八种数据类型,分别是 Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt。

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

  • Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
  • BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。

这些数据可以分为原始数据类型和引用数据类型:

  • 栈:原始数据类型(Undefined、Null、Boolean、Number、String)
  • 堆:引用数据类型(对象、数组和函数)

两种类型的区别在于存储位置的不同:

  • 原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;
  • 引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

堆和栈的概念存在于数据结构和操作系统内存中,在数据结构中:

  • 在数据结构中,栈中数据的存取方式为先进后出。
  • 堆是一个优先队列,是按优先级来进行排序的,优先级可以按照大小来规定。

在操作系统中,内存被分为栈区和堆区:

  • 栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
  • 堆区内存一般由开发着分配释放,若开发者不释放,程序结束时可能由垃圾回收机制回收。

2、数据类型检测的方式有哪些

(1)typeof

console.log(typeof 2);               // number
console.log(typeof true); // boolean
console.log(typeof 'str'); // string
console.log(typeof []); // object
console.log(typeof function(){}); // function
console.log(typeof {}); // object
console.log(typeof undefined); // undefined
console.log(typeof null); // object

其中数组、对象、null都会被判断为object,其他判断都正确。

(2)instanceof

instanceof****可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型

console.log(2 instanceof Number);                    // false
console.log(true instanceof Boolean); // false
console.log('str' instanceof String); // false

console.log([] instanceof Array); // true
console.log(function(){} instanceof Function); // true
console.log({} instanceof Object); // true

可以看到,instanceof只能正确判断引用数据类型**,而不能判断基本数据类型。**instanceof 运算符可以用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。

(3) constructor

console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true

constructor****有两个作用,一是判断数据的类型,二是对象实例通过 constrcutor 对象访问它的构造函数。需要注意,如果创建一个对象来改变它的原型,constructor就不能用来判断数据类型了:

function Fn(){};

Fn.prototype = new Array();

var f = new Fn();

console.log(f.constructor===Fn); // false
console.log(f.constructor===Array); // true

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

Object.prototype.toString.call() 使用 Object 对象的原型方法 toString 来判断数据类型:

var a = Object.prototype.toString;

console.log(a.call(2));
console.log(a.call(true));
console.log(a.call('str'));
console.log(a.call([]));
console.log(a.call(function(){}));
console.log(a.call({}));
console.log(a.call(undefined));
console.log(a.call(null));

同样是检测对象obj调用toString方法,obj.toString()的结果和Object.prototype.toString.call(obj)的结果不一样,这是为什么?

这是因为toString是Object的原型方法,而Array、function等类型作为Object的实例,都重写了toString方法。不同的对象类型调用toString方法时,根据原型链的知识,调用的是对应的重写之后的toString方法(function类型返回内容为函数体的字符串,Array类型返回元素组成的字符串…),而不会去调用Object上原型toString方法(返回对象的具体类型),所以采用obj.toString()不能得到其对象类型,只能将obj转换为字符串类型;因此,在想要得到对象的具体类型时,应该调用Object原型上的toString方法。

3、 判断数组的方式有哪些

1、通过Object.prototype.toString.call()
Object.prototype.toString.call(obj).slice(8,-1) === 'Array'
2、通过ES6Array.isArray()来判断
Array.isArray(obj)
3、通过原型链做判断
obj.__proto__ ===Array.prototype
4、通过instanceof来判断
obj instanceof Array
5、通过Array.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(obj)

4、 null和undefined区别

首先 Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。

undefined 代表的含义是未定义,null 代表的含义是空对象。一般变量声明了但还没有定义的时候会返回 undefined,null主要用于赋值给一些可能会返回对象的变量,作为初始化。

undefined 在 JavaScript 中不是一个保留字,这意味着可以使用 undefined 来作为一个变量名,但是这样的做法是非常危险的,它会影响对 undefined 值的判断。我们可以通过一些方法获得安全的 undefined 值,比如说 void 0。

当对这两种类型使用 typeof 进行判断时,Null 类型化会返回 “object”,这是一个历史遗留的问题。当使用双等号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。

5、typeof null 的结果是什么,为什么?

typeof null 为"object", 原因是因为 不同的对象在底层都表示为二进制,在Javascript中二进制前(低)三位都为0的话会被判断为Object类型,null的二进制表示全为0,自然前三位也是0,所以执行typeof时会返回"object"。 一个不恰当的例子,假设所有的Javascript对象都是16位的,也就是有16个0或1组成的序列,猜想如下:

Array: 1000100010001000
null: 0000000000000000

typeof [] // "object"
typeof null // "object"

因为Array和null的前三位都是000。为什么Array的前三位不是100?因为二进制中的“前”一般代表低位, 比如二进制00000011对应十进制数是3,它的前三位是011。

还有一种情况

function foo() {};
typeof foo; // 'function'

这样看来,function 也是JavaScript的一个内置类型。然而查阅规范,就会知道,它实际上是 object 的一个"子类型"。具体来说,函数是“可调用对象”,它有一个内部属性[[call]],该属性使其可以被调用。typeof 可以用来区分函数其他对象。

6. intanceof 操作符的实现原理及实现

关键词:递归、proto、 prototype 例如: L instanceof R instanceof运算时,会递归L的__proto__原型链,查看是否存在右侧的prototype原型。

function myInstanceOf(left,right){
if (typeof left !== 'object' || left === null || typeof right !== 'function') {
return false;
};
let proto = Object.getPrototypeOf(left) // 获取左边对象的原型
let prototype = right.prototype
// 判断构造函数的 prototype 对象是否在对象的原型链上
while(true){
if(!proto) return false
if(proto===prototype) return true
proto = Object.getPrototypeOf(proto)
}
}

typeof与instanceof 都是判断数据类型的方法,区别如下:

● typeof会返回一个运算数的基本类型,instanceof 返回的是布尔值

● instanceof 可以准确判断引用数据类型,但是不能正确判断原始数据类型

● typeof虽然可以判断原始数据类型(null 除外),但是无法判断引用数据类型(function 除外)

7. 为什么0.1+0.2 ! == 0.3,如何让其相等

js的小数位是2的53次方,一共是52+符号位,一共53位,而0.1和0.2的二进制是无限循环小数,所以在进制转化的过程中精度损失了,并且在计算的过程中对介运算也精度损失了,所以

精度损失可能出现在进制转化和对阶运算过程中

精度损失可能出现在进制转化和对阶运算过程中

精度损失可能出现在进制转化和对阶运算过程中

只要在这两步中产生了精度损失,计算结果就会出现偏差

问:怎么解决精度问题?

  1. 将数字转成整数
function add(num1, num2) {
const num1Digits = (num1.toString().split('.')[1] || '').length;
const num2Digits = (num2.toString().split('.')[1] || '').length;
const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
return (num1 * baseNum + num2 * baseNum) / baseNum;
}
但是这种方法对大数支持的依然不好
  1. 第三方库
  2. tofixed
  3. 对于这个问题,一个直接的解决方法就是设置一个误差范围,通常称为“机器精度”。对JavaScript来说,这个值通常为2-52,在ES6中,提供了 Number.EPSILON****属性,而它的值就是2-52,只要判断 0.1+0.2-0.3****是否小于 Number.EPSILON**,如果小于,就可以判断为0.1+0.2 ===0.3**
function numberepsilon(arg1,arg2){
return Math.abs(arg1 - arg2) < Number.EPSILON;
}

console.log(numberepsilon(0.1 + 0.2, 0.3)); // true

8. typeof NaN 的结果是什么?

number

9. isNaN 和 Number.isNaN 函数的区别?

函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。 ●函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。

10. == 操作符的强制类型转换规则?

对于 == 来说,如果对比双方的类型不一样,就会进行类型转换。假如对比 x 和 y 是否相同,就会进行如下判断流程:

1.首先会判断两者类型是否相同,相同的话就比较两者的大小; 2.类型不相同的话,就会进行类型转换; 3.会先判断是否在对比 null 和 undefined,是的话就会返回 true 4.判断两者类型是否为 string 和 number,是的话就会将字符串转换为 number

5.判断其中一方是否为 boolean,是的话就会把 boolean 转为 number 再进行判断

6.判断其中一方是否为 object 且另一方为 string、number 或者 symbol,是的话就会把 object 转为原始类型再进行判断

img

11. 其他值到字符串的转换规则?

Null 和 Undefined 类型 ,null 转换为 "null",undefined 转换为 "undefined",

  • Boolean 类型,true 转换为 "true",false 转换为 "false"。
  • Number 类型的值直接转换,不过那些极小和极大的数字会使用指数形式。
  • Symbol 类型的值直接转换,但是只允许显式强制类型转换,使用隐式强制类型转换会产生错误。
  • 对普通对象来说,除非自行定义 toString() 方法,否则会调用 toString()(Object.prototype.toString())来返回内部属性 [[Class]] 的值,如"[object Object]"。如果对象有自己的 toString() 方法,字符串化时就会调用该方法并使用其返回值。

12. 其他值到数字值的转换规则?

  • Undefined 类型的值转换为 NaN。
  • Null 类型的值转换为 0。
  • Boolean 类型的值,true 转换为 1,false 转换为 0。
  • String 类型的值转换如同使用 Number() 函数进行转换,如果包含非数字值则转换为 NaN,空字符串为 0。
  • Symbol 类型的值不能转换为数字,会报错。
  • 对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。

为了将值转换为相应的基本类型值,抽象操作 ToPrimitive 会首先(通过内部操作 DefaultValue)检查该值是否有valueOf()方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString() 的返回值(如果存在)来进行强制类型转换。

如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误。

13. 其他值到布尔类型的值的转换规则?

以下这些是假值:

• undefined

• null

• false

• +0、-0 和 NaN

• ""

假值的布尔强制类型转换结果为 false。从逻辑上说,假值列表以外的都应该是真值。

14. Object.is() 与比较操作符 “===”、“==” 的区别?

  • 使用双等号(==)进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较。
  • 使用三等号(===)进行相等判断时,如果两边的类型不一致时,不会做强制类型准换,直接返回 false。
  • 使用 Object.is 来进行相等判断时,一般情况下和三等号的判断相同,它处理了一些特殊的情况,比如 -0 和 +0 不再相等,两个 NaN 是相等的。

15. JavaScript 中如何进行隐式类型转换?

首先要介绍ToPrimitive方法,这是 JavaScript 中每个值隐含的自带的方法,用来将值 (无论是基本类型值还是对象)转换为基本类型值。如果值为基本类型,则直接返回值本身;如果值为对象,其看起来大概是这样:

/**
* @obj 需要转换的对象
* @type 期望的结果类型
*/
ToPrimitive(obj,type)

type的值为number或者string。

(1)当typenumber时规则如下:

  • 调用obj的valueOf方法,如果为原始值,则返回,否则下一步;
  • 调用obj的toString方法,后续同上;
  • 抛出TypeError 异常。

(2)当typestring时规则如下:

  • 调用obj的toString方法,如果为原始值,则返回,否则下一步;
  • 调用obj的valueOf方法,后续同上;
  • 抛出TypeError 异常。

可以看出两者的主要区别在于调用toString和valueOf的先后顺序。默认情况下:

  • 如果对象为 Date 对象,则type默认为string;
  • 其他情况下,type默认为number。

总结上面的规则,对于 Date 以外的对象,转换为基本类型的大概规则可以概括为一个函数:

var objToNumber = value => Number(value.valueOf().toString())
objToNumber([]) === 0
objToNumber({}) === NaN


JavaScript 中的隐式类型转换主要发生在+-*/以及==><这些运算符之间。而这些运算符只能操作基本类型值,所以在进行这些运算前的第一步就是将两边的值用ToPrimitive转换成基本类型,再进行操作。

以下是基本类型的值在不同操作符的情况下隐式转换的规则 (对于对象,其会被ToPrimitive转换成基本类型,所以最终还是要应用基本类型转换规则):

1+操作符+操作符的两边有至少一个string类型变量时,两边的变量都会被隐式转换为字符串;其他情况下两边的变量都会被转换为数字。


1 + '23' // '123'
1 + false // 1
1 + Symbol() // Uncaught TypeError: Cannot convert a Symbol value to a number
'1' + false // '1false'
false + true // 1

2、*、\操作符NaN也是一个数字

1 * '23' // 23
1 * false // 0
1 / 'aa' // NaN

3、对于**==**操作符

操作符两边的值都尽量转成number:

4 、对于**<>**比较符

如果两边都是字符串,则比较字母表顺序:

其他情况下,转换为数字再比较:

以上说的是基本类型的隐式转换,而对象会被ToPrimitive转换为基本类型再进行转换:

其对比过程如下:

a.valueOf() // {}, 上面提到过,ToPrimitive默认type为number,所以先valueOf,结果还是个对象,下一步
a.toString() // "[object Object]",现在是一个字符串了
Number(a.toString()) // NaN,根据上面 < 和 > 操作符的规则,要转换成数字
NaN > 2 //false,得出比较结果

又比如:

var a = {name:'Jack'}
var b = {age: 18}
a + b // "[object Object][object Object]"
a.valueOf() // {},上面提到过,ToPrimitive默认type为number,所以先valueOf,结果还是个对象,下一步
a.toString() // "[object Object]"
b.valueOf() // 同理
b.toString() // "[object Object]"
a + b // "[object Object][object Object]"

16. let、const、var的区别

1、块级作用域 块级作用域是指包括的区域存在块级作用域,let和const存在块级作用域,var不存在 2、变量提升 var声明的变量存在变量提升,而const和let不存在 3、暂时性死区 var定义的变量不存在暂时性死区,而const和let存在 4、重复声明 var定义的变量可以重复声明,而let和const不可以 5、全局变量 var定义的变量会被存在window对象上,node则是global对象上,而let和const不会 6、初始值设置 let和var可以不用设置初始值,但是const要设置初始值 7、指针指向 let和const是es6的新增变量的语法,let可以被重新赋值,即改变指针指向,而const不可以改变指针指向

17. const对象的属性可以修改吗

const保证的并不是变量的值不能改动,而是变量指向的那个内存地址不能改动。对于基本类型的数据(数值、字符串、布尔值),其值就保存在变量指向的那个内存地址,因此等同于常量。

但对于引用类型的数据(主要是对象和数组)来说,变量指向数据的内存地址,保存的只是一个指针,const只能保证这个指针是固定不变的,至于它指向的数据结构是不是可变的,就完全不能控制了。

18. 如果new一个箭头函数的会怎么样

箭头函数是ES6中的提出来的,它没有prototype,也没有自己的this指向,更不可以使用arguments参数,所以不能New一个箭头函数。

new操作符的实现步骤如下:

1创建一个对象 2将构造函数的作用域赋给新对象(也就是将对象的proto属性指向构造函数的prototype属性) 3指向构造函数中的代码,构造函数中的this指向该对象(也就是为这个对象添加属性和方法) 4返回新的对象

所以,上面的第二、三步,箭头函数都是没有办法执行的。

19. 箭头函数与普通函数的区别

区别:

1、箭头函数比普通函数更加简洁:

箭头函数返回值只有一句,直接可以省略大括号。如果不需要返回值直接加上void就可以了

2、箭头函数没有自己的this

箭头函数不会创建自己的this,所以它没有this,它只会继承自己作用域的上一层,所以箭头函数在创建的时候this指向就已经定义好了

3、箭头的函数的this指向永远不会被改变

4、call、apply、bind等方法不会改变箭头函数this的指向

5、箭头函数不能作为构造函数使用:因为箭头函数没有自己的this,所以会报错

6、箭头函数没有自己的arguments:箭头函数没有自己的argumnets对象,在箭头函数中访问的实际上获得的是它外层函数的arguments值

7、箭头函数没有prototype

8、箭头函数不能作为Genrator函数,不能使用yeild关键字

20. 箭头函数的this指向哪⾥?

箭头函数不同于传统JavaScript中的函数,箭头函数并没有属于⾃⼰的this,它所谓的this是捕获其所在上下⽂的 this 值,作为⾃⼰的 this 值,并且由于没有属于⾃⼰的this,所以是不会被new调⽤的,这个所谓的this也不会被改变。

可以⽤Babel理解⼀下箭头函数:

// ES6
const obj = {
getArrow() {
return () => {
console.log(this === obj);
};
}
}
// ES5,由 Babel 转译
var obj = {
getArrow: function getArrow() {
var _this = this;
return function () {
console.log(_this === obj);
};
}
};

21. Proxy 可以实现什么功能?

在 Vue3.0 中通过 Proxy 来替换原本的 Object.defineProperty 来实现数据响应式。

Proxy 是 ES6 中新增的功能,它可以用来自定义对象中的操作。

let p = new Proxy(target, handler)

target 代表需要添加代理的对象,handler 用来自定义对象中的操作,比如可以用来自定义 set 或者 get 函数。

下面来通过 Proxy 来实现一个数据响应式:

let onWatch = (obj, setBind, getLogger) => {
let handler = {
get(target, property, receiver) {
getLogger(target, property)
return Reflect.get(target, property, receiver)
},
set(target, property, value, receiver) {
setBind(value, property)
return Reflect.set(target, property, value)
}
}
return new Proxy(obj, handler)
}
let obj = { a: 1 }
let p = onWatch(
obj,
(v, property) => {
console.log(`监听到属性${property}改变为${v}`)
},
(target, property) => {
console.log(`'${property}' = ${target[property]}`)
}
)
p.a = 2 // 监听到属性a改变
p.a // 'a' = 2

在上述代码中,通过自定义 setget 函数的方式,在原本的逻辑中插入了我们的函数逻辑,实现了在对对象任何属性进行读写时发出通知。

当然这是简单版的响应式实现,如果需要实现一个 Vue 中的响应式,需要在 get 中收集依赖,在 set 派发更新,之所以 Vue3.0 要使用 Proxy 替换原本的 API 原因在于 Proxy 无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现有一些数据更新不能监听到,但是 Proxy 可以完美监听到任何方式的数据改变,唯一缺陷就是浏览器的兼容性不好。

22. 对 rest 参数的理解

扩展运算符被用在函数形参上时,它还可以把一个分离的参数序列整合成一个数组

function mutiple(...args) {
let result = 1;
for (var val of args) {
result *= val;
}
return result;
}
mutiple(1, 2, 3, 4) // 24

这里,传入 mutiple 的是四个分离的参数,但是如果在 mutiple 函数里尝试输出 args 的值,会发现它是一个数组:

function mutiple(...args) {
console.log(args)
}
mutiple(1, 2, 3, 4) // [1, 2, 3, 4]

这就是 … rest运算符的又一层威力了,它可以把函数的多个入参收敛进一个数组里。这一点经常用于获取函数的多余参数,或者像上面这样处理函数参数个数不确定的情况。

23. ES6中模板语法与字符串处理

ES6 提出了“模板语法”的概念。在 ES6 以前,拼接字符串是很麻烦的事情:

var name = 'css'
var career = 'coder'
var hobby = ['coding', 'writing']
var finalString = 'my name is ' + name + ', I work as a ' + career + ', I love ' + hobby[0] + ' and ' + hobby[1]

仅仅几个变量,写了这么多加号,还要时刻小心里面的空格和标点符号有没有跟错地方。但是有了模板字符串,拼接难度直线下降:

var name = 'css'
var career = 'coder'
var hobby = ['coding', 'writing']
var finalString = `my name is ${name}, I work as a ${career} I love ${hobby[0]} and ${hobby[1]}`

字符串不仅更容易拼了,也更易读了,代码整体的质量都变高了。这就是模板字符串的第一个优势——允许用$的方式嵌入变量。但这还不是问题的关键,模板字符串的关键优势有两个:

  • 在模板字符串中,空格、缩进、换行都会被保留
  • 模板字符串完全支持“运算”式的表达式,可以在$里完成一些计算

基于第一点,可以在模板字符串里无障碍地直接写 html 代码:

let list = `
<ul>
<li>列表项1</li>
<li>列表项2</li>
</ul>
`;
console.log(message); // 正确输出,不存在报错

基于第二点,可以把一些简单的计算和调用丢进 $ 来做:

function add(a, b) {
const finalString = `${a} + ${b} = ${a+b}`
console.log(finalString)
}
add(1, 2) // 输出 '1 + 2 = 3'

除了模板语法外, ES6中还新增了一系列的字符串方法用于提升开发效率:

  • 存在性判定:在过去,当判断一个字符/字符串是否在某字符串中时,只能用 indexOf > -1 来做。现在 ES6 提供了三个方法:includes、startsWith、endsWith,它们都会返回一个布尔值来告诉你是否存在。
  • includes:判断字符串与子串的包含关系:
const son = 'haha'
const father = 'xixi haha hehe'
father.includes(son) // true
  • startsWith:判断字符串是否以某个/某串字符开头:
const father = 'xixi haha hehe'
father.startsWith('haha') // false
father.startsWith('xixi') // true
  • endsWith:判断字符串是否以某个/某串字符结尾:
const father = 'xixi haha hehe'
father.endsWith('hehe') // true
  • 自动重复:可以使用 repeat 方法来使同一个字符串输出多次(被连续复制多次):
const sourceCode = 'repeat for 3 times;'
const repeated = sourceCode.repeat(3)
console.log(repeated) // repeat for 3 times;repeat for 3 times;repeat for 3 times;

24. new操作符的实现原理

关键词: 构造函数、对象实例、 proto、 prototype、this、返回值 a. JS引擎在内存中创建一个空对象; b. 新对象的__proto__属性指向构造函数的prototype; c. 构造函数内部的this会指向这个新对象; d. 从上到下执行函数体(只有这步是我们能直观看到代码的); e. 返回创造出来的对象(如果构造函数没有返回对象,则默认返回this。在函数体内部的this指向新创建的内存空间,默认返回 this 就相当于默认返回了该内存空间。如果构造函数return 复杂数据类型则返回复杂数据类型,return简单数据类型,没有影响)。

function myNew(constructor, ...args) {
if (typeof constructor !== "function") return
let obj = Object.create(constructor.prototype)
const res = constructor.apply(obj, args)
if (res && (typeof res !== "object" || typeof res === "function")) return res
return obj
}
function Fn(obj) {
this.obj =obj
}
let obj =myNew(Fn,'222')
console.log(obj);

25. map和Object的区别

关键词:键值区别、长度大小获取、原型、顺序、循环

1、Object的键只能是字符串或Symbol,但是Map的键可以是任意值

2、Map的长度大小可以通过size来获取,但是Object则需要通过Object.keys(obj).length

3、Object的键值可能会和原型上的属性重名,我们可以使用Object.create(null)来设置没有原型的对象

4、Object的key的顺序是先判断数字开头的key,在判断字符串,而Map的顺序是在插入时决定的

5、Map结构的数据可以使用for of来循环,而Object则需要手动遍历,用for in

6、Map在频繁增删键值对的场景下表现更好。

综上所诉:如果需要存储不同的键值并且保持插入顺序的话用Map会更加灵活

26、map和weakMap的区别

(1)Map

map本质上就是键值对的集合,但是普通的Object中的键值对中的键只能是字符串。而ES6提供的Map数据结构类似于对象,但是它的键不限制范围,可以是任意类型,是一种更加完善的Hash结构。如果Map的键是一个原始数据类型,只要两个键严格相同,就视为是同一个键。

实际上Map是一个数组,它的每一个数据也都是一个数组,其形式如下:

const map = [
["name","张三"],
["age",18],
]

Map数据结构有以下操作方法:

  • sizemap.size 返回Map结构的成员总数。
  • set(key,value):设置键名key对应的键值value,然后返回整个Map结构,如果key已经有值,则键值会被更新,否则就新生成该键。(因为返回的是当前Map对象,所以可以链式调用)
  • get(key):该方法读取key对应的键值,如果找不到key,返回undefined。
  • has(key):该方法返回一个布尔值,表示某个键是否在当前Map对象中。
  • delete(key):该方法删除某个键,返回true,如果删除失败,返回false。
  • clear():map.clear()清除所有成员,没有返回值。

Map结构原生提供是三个遍历器生成函数和一个遍历方法

  • keys():返回键名的遍历器。
  • values():返回键值的遍历器。
  • entries():返回所有成员的遍历器。
  • forEach():遍历Map的所有成员。
const map = new Map([
["foo",1],
["bar",2],
])
for(let key of map.keys()){
console.log(key); // foo bar
}
for(let value of map.values()){
console.log(value); // 1 2
}
for(let items of map.entries()){
console.log(items); // ["foo",1] ["bar",2]
}
map.forEach( (value,key,map) => {
console.log(key,value); // foo 1 bar 2
})

(2)WeakMap

WeakMap 对象也是一组键值对的集合,其中的键是弱引用的。其键必须是对象,原始数据类型不能作为key值,而值可以是任意的。只接受对象作为键名(null除外),不接受其它类型的值作为键名。

该对象也有以下几种方法:

  • set(key,value):设置键名key对应的键值value,然后返回整个Map结构,如果key已经有值,则键值会被更新,否则就新生成该键。(因为返回的是当前Map对象,所以可以链式调用)
  • get(key):该方法读取key对应的键值,如果找不到key,返回undefined。
  • has(key):该方法返回一个布尔值,表示某个键是否在当前Map对象中。
  • delete(key):该方法删除某个键,返回true,如果删除失败,返回false。

其clear()方法已经被弃用,所以可以通过创建一个空的WeakMap并替换原对象来实现清除。

WeakMap的设计目的在于,有时想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。一旦不再需要这两个对象,就必须手动删除这个引用,否则垃圾回收机制就不会释放对象占用的内存。

而WeakMap的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用

总结:

  • Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
  • WeakMap 结构与 Map 结构类似,也是用于生成键值对的集合。但是 WeakMap 只接受对象作为键名( null 除外),不接受其他类型的值作为键名。而且 WeakMap 的键名所指向的对象,不计入垃圾回收机制。

map的键可以是任何类型,weakmap只接受对象作为键(null除外),不接受其他类型的值作为键。 map的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键;weakmap的键是弱引用,键所指向的对象可以被垃圾回收,此时的键是无效的。 map可以被遍历,weakmap不可以被遍历。

27. JavaScript脚本延迟加载的方式有哪些?

延迟加载就是等页面加载完成之后再加载 JavaScript 文件。 js 延迟加载有助于提高页面加载速度。

一般有以下几种方式:

  • **defer 属性:**给 js 脚本添加 defer 属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了 defer 属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样。
  • **async 属性:**给 js 脚本添加 async 属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js 脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个 async 属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行。
  • **动态创建 DOM 方式:**动态创建 DOM 标签的方式,可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入 js 脚本。
  • **使用 setTimeout 延迟方法:**设置一个定时器来延迟加载js脚本文件
  • 让 JS 最后加载:将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行。

28. JavaScript 类数组对象的定义?

一个拥有 length 属性和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法。常见的类数组对象有 arguments 和 DOM 方法的返回结果,还有一个函数也可以被看作是类数组对象,因为它含有 length 属性值,代表可接收的参数个数。

常见的类数组转换为数组的方法有这样几种:

(1)通过 call 调用数组的 slice 方法来实现转换

Array.prototype.slice.call(arrayLike);

(2)通过 call 调用数组的 splice 方法来实现转换

Array.prototype.splice.call(arrayLike, 0);

(3)通过 apply 调用数组的 concat 方法来实现转换

Array.prototype.concat.apply([], arrayLike);

(4)通过 Array.from 方法来实现转换 Array.from(arrayLike);

29. 为什么函数的 arguments 参数是类数组而不是数组?如何遍历类数组?

`arguments`是一个对象,它的属性是从 0 开始依次递增的数字,还有 `callee``length`等属性,与数组相似;但是它却没有数组常见的方法属性,如 `forEach`, `reduce`等,所以叫它们类数组。

要遍历类数组,有三个方法:

1)将数组的方法应用到类数组上,这时候就可以使用 `call``apply`方法,如:

function foo(){
Array.prototype.forEach.call(arguments, a => console.log(a))
}

2)使用Array.from方法将类数组转化成数组:‌

function foo(){
const arrArgs = Array.from(arguments)
arrArgs.forEach(a => console.log(a))
}

3)使用展开运算符将类数组转化成数组
function foo(){
const arrArgs = [...arguments]
arrArgs.forEach(a => console.log(a))
}

30. 什么是 DOM 和 BOM?

  • DOM 指的是文档对象模型,它指的是把文档当做一个对象,这个对象主要定义了处理网页内容的方法和接口。
  • BOM 指的是浏览器对象模型,它指的是把浏览器当做一个对象来对待,这个对象主要定义了与浏览器进行交互的法和接口。BOM的核心是 window,而 window 对象具有双重角色,它既是通过 js 访问浏览器窗口的一个接口,又是一个 Global(全局)对象。这意味着在网页中定义的任何对象,变量和函数,都作为全局对象的一个属性或者方法存在。window 对象含有 location 对象、navigator 对象、screen 对象等子对象,并且 DOM 的最根本的对象 document 对象也是 BOM 的 window 对象的子对象。

31. 对类数组对象的理解,如何转化为数组

一个拥有 length 属性和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法。常见的类数组对象有 arguments 和 DOM 方法的返回结果,函数参数也可以被看作是类数组对象,因为它含有 length属性值,代表可接收的参数个数。

常见的类数组转换为数组的方法有这样几种:

  • 通过 call 调用数组的 slice 方法来实现转换
Array.prototype.slice.call(arrayLike);
  • 通过 call 调用数组的 splice 方法来实现转换
Array.prototype.splice.call(arrayLike, 0);
  • 通过 apply 调用数组的 concat 方法来实现转换
Array.prototype.concat.apply([], arrayLike);
  • 通过 Array.from 方法来实现转换
Array.from(arrayLike);

32. 对AJAX的理解,实现一个AJAX请求

AJAX是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。

创建AJAX请求的步骤:

  • 创建一个 XMLHttpRequest 对象。
  • 在这个对象上使用 open 方法创建一个 HTTP 请求,open 方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。
  • 在发起请求前,可以为这个对象添加一些信息和监听函数。比如说可以通过 setRequestHeader 方法来为请求添加头信息。还可以为这个对象添加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变化时会触发onreadystatechange 事件,可以通过设置监听函数,来处理请求成功后的结果。当对象的 readyState 变为 4 的时候,代表服务器返回的数据接收完成,这个时候可以通过判断请求的状态,如果状态是 2xx 或者 304 的话则代表返回正常。这个时候就可以通过 response 中的数据来对页面进行更新了。
  • 当对象的属性和监听函数设置完成后,最后调用 sent 方法来向服务器发起请求,可以传入参数作为发送的数据体。
const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", url, true);
// 设置状态监听函数
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;
// 当请求成功时
if (this.status === 200) {
handle(this.response);
} else {
console.error(this.statusText);
}
};
// 设置请求失败时的监听函数
xhr.onerror = function() {
console.error(this.statusText);
};
// 设置请求头信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 发送 Http 请求
xhr.send(null);

使用Promise封装AJAX:

// promise 封装实现:
function getJSON(url) {
// 创建一个 promise 对象
let promise = new Promise(function(resolve, reject) {
let xhr = new XMLHttpRequest();
// 新建一个 http 请求
xhr.open("GET", url, true);
// 设置状态的监听函数
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;
// 当请求成功或失败时,改变 promise 的状态
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
// 设置错误监听函数
xhr.onerror = function() {
reject(new Error(this.statusText));
};
// 设置响应的数据类型
xhr.responseType = "json";
// 设置请求头信息
xhr.setRequestHeader("Accept", "application/json");
// 发送 http 请求
xhr.send(null);
});
return promise;
}

33.JavaScript为什么要进行变量提升,它导致了什么问题?

变量提升的表现是,无论在函数中何处位置声明的变量,好像都被提升到了函数的首部,可以在变量声明前访问到而不会报错。

造成变量声明提升的本质原因是 js 引擎在代码执行前有一个解析的过程,创建了执行上下文,初始化了一些代码执行时需要用到的对象。当访问一个变量时,会到当前执行上下文中的作用域链中去查找,而作用域链的首端指向的是当前执行上下文的变量对象,这个变量对象是执行上下文的一个属性,它包含了函数的形参、所有的函数和变量声明,这个对象的是在代码解析的时候创建的。

首先要知道,JS在拿到一个变量或者一个函数的时候,会有两步操作,即解析和执行。

  • 在解析阶段,JS会检查语法,并对函数进行预编译。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出this、arguments和函数的参数。

    • 全局上下文:变量定义,函数声明
    • 函数上下文:变量定义,函数声明,this,arguments
  • 在执行阶段,就是按照代码的顺序依次执行。

那为什么会进行变量提升呢?主要有以下两个原因:

  • 提高性能
  • 容错性更好

(1)提高性能

在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。这么做就是为了提高性能,如果没有这一步,那么每次执行代码前都必须重新解析一遍该变量(函数),而这是没有必要的,因为变量(函数)的代码并不会改变,解析一遍就够了。

在解析的过程中,还会为函数生成预编译代码。在预编译时,会统计声明了哪些变量、创建了哪些函数,并对函数的代码进行压缩,去除注释、不必要的空白等。这样做的好处就是每次执行函数时都可以直接为该函数分配栈空间(不需要再解析一遍去获取代码中声明了哪些变量,创建了哪些函数),并且因为代码压缩的原因,代码执行也更快了。

(2)容错性更好

变量提升可以在一定程度上提高JS的容错性,看下面的代码:

a = 1;
var a;
console.log(a);

如果没有变量提升,这两行代码就会报错,但是因为有了变量提升,这段代码就可以正常执行。

虽然,在可以开发过程中,可以完全避免这样写,但是有时代码很复杂的时候。可能因为疏忽而先使用后定义了,这样也不会影响正常使用。由于变量提升的存在,而会正常运行。

总结:

  • 解析和预编译过程中的声明提升可以提高性能,让函数可以在执行时预先为变量分配栈空间
  • 声明提升还可以提高JS代码的容错性,使一些不规范的代码也可以正常执行

变量提升虽然有一些优点,但是他也会造成一定的问题,在ES6中提出了let、const来定义变量,它们就没有变量提升的机制。下面看一下变量提升可能会导致的问题:

var tmp = new Date();

function fn(){
console.log(tmp);
if(false){
var tmp = 'hello world';
}
}

fn(); // undefined

在这个函数中,原本是要打印出外层的tmp变量,但是因为变量提升的问题,内层定义的tmp被提到函数内部的最顶部,相当于覆盖了外层的tmp,所以打印结果为undefined。

var tmp = 'hello world';

for (var i = 0; i < tmp.length; i++) {
console.log(tmp[i]);
}

console.log(i); // 11

由于遍历时定义的i会变量提升成为一个全局变量,在函数结束之后不会被销毁,所以打印出来11。

34.ES6模块与CommonJS模块有什么异同?

  • es6module是值的引用,commonjs是也是值的引用,不过commonjs是导出对象的引用

  • es6是编译的时候输出接口,commonjs是运行才加载

  • es6是异步加载,commonjs是同步加载,因为commonjs是运行在node中,文件在本地,而es6是在浏览器运行,所以需要支持异步加载

  • es6支持tree-shaking,commonjs不支持

  • esmodule的数据是同步的,也就是实时绑定

  • 在 ESM 中,导出的变量是只读的,外部不能修改,这意味着更好的安全性

35.常见的DOM操作有哪些

1)DOM 节点的获取

DOM 节点的获取的API及使用:

getElementById // 按照 id 查询
getElementsByTagName // 按照标签名查询
getElementsByClassName // 按照类名查询
querySelectorAll // 按照 css 选择器查询

// 按照 id 查询
var imooc = document.getElementById('imooc') // 查询到 id 为 imooc 的元素
// 按照标签名查询
var pList = document.getElementsByTagName('p') // 查询到标签为 p 的集合
console.log(divList.length)
console.log(divList[0])
// 按照类名查询
var moocList = document.getElementsByClassName('mooc') // 查询到类名为 mooc 的集合
// 按照 css 选择器查询
var pList = document.querySelectorAll('.mooc') // 查询到类名为 mooc 的集合

2)DOM 节点的创建

**创建一个新节点,并把它添加到指定节点的后面。**已知的 HTML 结构如下:

<html>
<head>
<title>DEMO</title>
</head>
<body>
<div id="container">
<h1 id="title">我是标题</h1>
</div>
</body>
</html>

要求添加一个有内容的 span 节点到 id 为 title 的节点后面,做法就是:

// 首先获取父节点
var container = document.getElementById('container')
// 创建新节点
var targetSpan = document.createElement('span')
// 设置 span 节点的内容
targetSpan.innerHTML = 'hello world'
// 把新创建的元素塞进父节点里去
container.appendChild(targetSpan)

3)DOM 节点的删除

**删除指定的 DOM 节点,**已知的 HTML 结构如下:

<html>
<head>
<title>DEMO</title>
</head>
<body>
<div id="container">
<h1 id="title">我是标题</h1>
</div>
</body>
</html>

需要删除 id 为 title 的元素,做法是:

// 获取目标元素的父元素
var container = document.getElementById('container')
// 获取目标元素
var targetNode = document.getElementById('title')
// 删除目标元素
container.removeChild(targetNode)

或者通过子节点数组来完成删除:

// 获取目标元素的父元素
var container = document.getElementById('container')
// 获取目标元素
var targetNode = container.childNodes[1]
// 删除目标元素
container.removeChild(targetNode)

4)修改 DOM 元素

修改 DOM 元素这个动作可以分很多维度,比如说移动 DOM 元素的位置,修改 DOM 元素的属性等。

**将指定的两个 DOM 元素交换位置,**已知的 HTML 结构如下:

<html>
<head>
<title>DEMO</title>
</head>
<body>
<div id="container">
<h1 id="title">我是标题</h1>
<p id="content">我是内容</p>
</div>
</body>
</html>

现在需要调换 title 和 content 的位置,可以考虑 insertBefore 或者 appendChild:

// 获取父元素
var container = document.getElementById('container')

// 获取两个需要被交换的元素
var title = document.getElementById('title')
var content = document.getElementById('content')
// 交换两个元素,把 content 置于 title 前面
container.insertBefore(content, title)

36.for...in和for...of的区别d

for…of 是ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构(数组、对象等)并且返回各项的值,和ES3中的for…in的区别如下

  • for…of 遍历获取的是对象的键值,for…in 获取的是对象的键名;
  • for… in 会遍历对象的整个原型链,性能非常差不推荐使用,而 for … of 只遍历当前对象不会遍历原型链;
  • 对于数组的遍历,for…in 会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for…of 只返回数组的下标对应的属性值;

**总结:**for...in 循环主要是为了遍历对象而生,不适用于遍历数组;for...of 循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象。

37.如何使用for...of遍历对象

for…of是作为ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构(数组、对象等)并且返回各项的值,普通的对象用for..of遍历是会报错的。

如果需要遍历的对象是类数组对象,用Array.from转成数组即可。

var obj = {
0:'one',
1:'two',
length: 2
};
obj = Array.from(obj);
for(var k of obj){
console.log(k)
}

如果不是类数组对象,就给对象添加一个[Symbol.iterator]属性,并指向一个迭代器即可。

//方法一:
var obj = {
a:1,
b:2,
c:3
};

obj[Symbol.iterator] = function(){
var keys = Object.keys(this);
var count = 0;
return {
next(){
if(count<keys.length){
return {value: obj[keys[count++]],done:false};
}else{
return {value:undefined,done:true};
}
}
}
};

for(var k of obj){
console.log(k);
}


// 方法二
var obj = {
a:1,
b:2,
c:3
};
obj[Symbol.iterator] = function*(){
var keys = Object.keys(obj);
for(var k of keys){
yield [k,obj[k]]
}
};

for(var [k,v] of obj){
console.log(k,v);
}

38.ajax、axios、fetch的区别

(1)AJAX

Ajax 即“AsynchronousJavascriptAndXML”(异步 JavaScript 和 XML),是指一种创建交互式网页应用的网页开发技术。它是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。通过在后台与服务器进行少量数据交换,Ajax 可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。传统的网页(不使用 Ajax)如果需要更新内容,必须重载整个网页页面。其缺点如下:

  • 本身是针对MVC编程,不符合前端MVVM的浪潮
  • 基于原生XHR开发,XHR本身的架构不清晰
  • 不符合关注分离(Separation of Concerns)的原则
  • 配置和调用方式非常混乱,而且基于事件的异步模型不友好。

(2)Fetch

fetch号称是AJAX的替代品,是在ES6出现的,使用了ES6中的promise对象。Fetch是基于promise设计的。Fetch的代码结构比起ajax简单多。fetch不是ajax的进一步封装,而是原生js,没有使用XMLHttpRequest对象

fetch的优点:

- 语法简洁,更加语义化
- 基于标准 Promise 实现,支持 async/await
- 更加底层,提供的API丰富(request, response)
- 脱离了XHR,是ES规范里新的实现方式

fetch的缺点:

- fetch只对网络请求报错,对400,500都当做成功的请求,服务器返回 400,500 错误码时并不会 reject,只有网络错误这些导致请求不能完成时,fetch 才会被 reject。
- fetch默认不会带cookie,需要添加配置项: fetch(url, {credentials: 'include'})
- fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费
- fetch没有办法原生监测请求的进度,而XHR可以

(3)Axios

Axios 是一种基于Promise封装的HTTP客户端,其特点如下:

  • 浏览器端发起XMLHttpRequests请求
  • node端发起http请求
  • 支持Promise API
  • 监听请求和返回
  • 对请求和返回进行转化
  • 取消请求
  • 自动转换json数据
  • 客户端支持抵御XSRF攻击

39.数组的遍历方法有哪些

40.forEach和map方法有什么区别

这方法都是用来遍历数组的,两者区别如下:

  • forEach()方法会针对每一个元素执行提供的函数,第三个参数传该数组会改变原数组,该方法没有返回值;
  • map()方法不会改变原数组的值,返回一个新数组,新数组中的值为原数组调用函数处理之后的值;

41.对原型、原型链的理解

JavaScript中的每个函数都有一个[[Prototype]]属性,也称为原型(prototype),它指向另一个对象或者null。当我们访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript会沿着它的原型链一层层向上查找,直到找到该属性或方法或者原型链到达顶端(即Object.prototype,它是所有对象的祖先)为止。

通过原型链,我们可以实现继承、属性的共享和方法的复用。当我们创建一个对象时,可以通过指定它的原型来继承原型上的属性和方法。在查找属性或方法时,如果对象本身没有这个属性或方法,就可以在其原型上查找。这样可以避免在每个对象上都复制一份相同的属性和方法,从而节省内存。

需要注意的是,如果在原型链上的某个属性被修改了,那么所有继承自该原型的对象都会受到影响。因此,应该谨慎地修改原型上的属性和方法。另外,通过对象的constructor属性,可以访问到其构造函数,从而可以判断一个对象的类型。

42.原型修改、重写

function Person(name) {
this.name = name
}
// 修改原型
Person.prototype.getName = function() {}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // true
// 重写原型
Person.prototype = {
getName: function() {}
}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // false

可以看到重写原型的时候p的构造函数不是指向Person了,因为直接给Person的原型对象直接用对象赋值时,它的构造函数指向的了根构造函数Object,所以这时候 `p.constructor === Object` ,而不是 `p.constructor === Person`。要想成立,就要用constructor指回来:

Person.prototype = {
getName: function() {}
}
var p = new Person('hello')
p.constructor = Person
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // true

43.原型链指向

p.__proto__  // Person.prototype
Person.prototype.__proto__ // Object.prototype
p.__proto__.__proto__ //Object.prototype
p.__proto__.constructor.prototype.__proto__ // Object.prototype
Person.prototype.constructor.prototype.__proto__ // Object.prototype
p1.__proto__.constructor // Person
Person.prototype.constructor // Person

44.原型链的终点是什么?如何打印出原型链的终点?

由于 Object是构造函数,原型链终点是 Object.prototype.__proto__,而 Object.prototype.__proto__=== null // true,所以,原型链的终点是 null。原型链上的所有原型都是对象,所有的对象最终都是由 Object构造的,而 Object.prototype的下一级是 Object.prototype.__proto__

img

45.如何获得对象非原型链上的属性?

使用后 hasOwnProperty()方法来判断属性是否属于原型链的属性:

function iterate(obj){
var res=[];
for(var key in obj){
if(obj.hasOwnProperty(key))
res.push(key+': '+obj[key]);
}
return res;
}

46.对闭包的理解

1、闭包是什么?

一个函数和对其周围状态的引用捆绑在了一起(或者说函数被引用包围),这样的组合就是闭包。也就是说闭包就是一个让你可以访问外层函数的作用域。通俗来说,闭包其实就是一个可以访问其他函数内部变量的一个函数,即定义在函数内部的函数,或者说闭包就是一个内嵌函数。作用(私有化变量和设置缓存)

通常来说,函数内部的变量是无法在外部进行访问的(即全局变量和局部的变量的区别),因此使用闭包的作用实现了,能在外部访问某个函数内部的变量的功能,让这些变量的值始终可以保存在内存当中。

从直观上讲,闭包这个概念为JavaScript中访问函数内变量提供了途径和便利。这样做的好处有很多,比如闭包实现缓存等。

2、作用域链的概念

当访问一个变量时,代码编辑器会首先在当前作用域去查找,如果没还找到,就会继续往父级作用域去查找,直到找到该变量或者不存在父级作用域内,这样的链路操作就是作用域链。

3、闭包的本质是什么?

当前环境内存在对于父级作用域的引用

4、闭包的使用场景

  1. 在定时器、事件监听、Ajax请求、web workers、任何异步中,只要使用了回调函数,实际上就是使用闭包,防抖和节流,以及vue底层都使用了
// 定时器
setTimeout(function handler(){
console.log('1');
}1000);
// 事件监听
document.getElementById(app).addEventListener('click', () => {
console.log('Event Listener');
});
  1. 作为函数参数的传递的形式
var a = 1;
function foo(){
var a = 2;
function baz(){
console.log(a);
}
bar(baz);
}
function bar(fn){
// 这是闭包
fn();
}
foo(); // 输出2,而不是1
  1. 立即执行函数:创建了闭包,保存了全局作用域和当前作用域,因此可以输出全局的变量。
var a = 2;
(function IIFE(){
console.log(a); // 输出2
})();
  1. 结果缓存(备忘模式)
function memorize(fn) {
const cache = {}
function foo(args) {
const key = JSON.stringify(args)
let result = cache[key]
if (!result) {
cache[key] = args
result = fn(args)
}
return { cache, result }
}
foo.cache = cache
return foo
}

function add(a) {
return a + 1
}

const added = memorize(add)
console.log("", added(1))
console.log("", added(2))
console.log("", added(3))
console.log("", added(4))
adder(1) // 输出: 2 当前: cache: { '[1]': 2 }
adder(1) // 输出: 2 当前: cache: { '[1]': 2 }
adder(2) // 输出: 3 当前: cache: { '[1]': 2, '[2]': 3 }

5、常见的闭包的循环输出的问题

for(var i = 1; i <= 5; i ++){
setTimeout(function() {
console.log(i)
}, 0)
}
输出56
解决办法:
1、利用立即执行函数
for(var i = 1; i <= 5; i ++){
(function(j){
setTimeout(function() {
console.log(j)
}, 0)
})(i)
}
2、使用es6的let
3、使用定时器的第三个参数
for(var i=1;i<=5;i++){
setTimeout(function(j) {
console.log(j)
}, 0, i)
}
定时器的第三个参数,这些参数会作为回调函数的附加参数存在

47.对作用域、作用域链的理解

关键词:作用域是什么、存储变量访问变量的规则是什么、编译阶段和执行阶段、全局、函数、块级

1、作用域是什么?

作用域是一套规则,可以存储变量、访问变量的规则。

2、规则是什么?

在JavaScript底层当中,程序中的代码分为三个步骤:词法分析、语法分析、代码生成

  1. 词法分析:就是将代码拆成最小的、不可再分的词法单元(token)。比如var name = “hello”,就会被分成四个词法单元,var name = "hello";并且在JavaScript中,空格是直接被忽略的
  2. 语法分析:就是将上一步的词法单元数据,根据语法规则生成AST。如果源码复合语法规则,这一步就直接完成,如果存在语法错误,这一步就会被终止,并且抛出语法错误
  3. 代码生成:就是将上一步生成的AST转换成真正可以运行的代码,简单来说就是将AST转换成一组机器指令,例如var name = “hello”,则会创建一个name变量(需要给name分配内存),并将一个值存储到name中。

3、什么是编译阶段和执行阶段

  1. 编译阶段:编译器会找遍当前作用域,看看是不是有一个name变量,如果有则忽略,继续执行下一步;如果没有,则会在作用域立创建一个name变量,然后编译器会为引擎生成可以运行的代码,然后进入执行阶段
  2. 执行阶段:js引擎在执行代码的时候,仍然会继续查找当前作用域,,看看是不是有一个name的变量。如果能找到,就给他赋值;如果找不到,js引擎就会抛出一个异常

4、作用域链是什么?

当访问变量的作用域的时候,如果在当前作用域没找到,就会继续向上级查找,**直到找到该变量或者不存在父级作用域中,**这样的链路就是作用域链

5、有哪些作用域?

  1. 全局作用域:定义的全局变量默认是挂载在window对象上,具有全局作用域。并且如果没有声明直接赋值的变量也是全局变量。
  2. 函数作用域:定义在函数内部的变量,只能在函数内部访问,具有函数作用域。并且在函数执行完成后,这个局部变量也会被销毁
  3. 块级作用域:es6提出的let const关键词具有块级作用域。他们定义的变量只能在块级作用域内才能访问。 if 语句及 for 语句后面 这里面所包括的也是块级作用域

6、暂时性死区和变量提升是什么意思?

  1. 变量提升:使用var定义的变量存在变量提升,// a = 1
  2. 暂时性死区:如果使用let或const定义的变量,则会存在暂时性死区。起始域函数开头,终止于相关变量声明的这一行,在这个范围内无法访问该变量

48.对执行上下文的理解

1. 执行上下文类型

(1)全局执行上下文

任何不在函数内部的都是全局执行上下文,它首先会创建一个全局的window对象,并且设置this的值等于这个全局对象,一个程序中只有一个全局执行上下文。

(2)函数执行上下文

当一个函数被调用时,就会为该函数创建一个新的执行上下文,函数的上下文可以有任意多个。

(3)eval函数执行上下文

执行在eval函数中的代码会有属于他自己的执行上下文,不过eval函数不常使用,不做介绍。

2. 执行上下文栈
  • JavaScript引擎使用执行上下文栈来管理执行上下文
  • 当JavaScript执行代码时,首先遇到全局代码,会创建一个全局执行上下文并且压入执行栈中,每当遇到一个函数调用,就会为该函数创建一个新的执行上下文并压入栈顶,引擎会执行位于执行上下文栈顶的函数,当函数执行完成之后,执行上下文从栈中弹出,继续执行下一个上下文。当所有的代码都执行完毕之后,从栈中弹出全局执行上下文。
let a = 'Hello World!';
function first() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}
function second() {
console.log('Inside second function');
}
first();
//执行顺序
//先执行second(),在执行first()
3. 创建执行上下文

创建执行上下文有两个阶段:创建阶段和****执行阶段

1)创建阶段

(1)this绑定

  • 在全局执行上下文中,this指向全局对象(window对象)
  • 在函数执行上下文中,this指向取决于函数如何调用。如果它被一个引用对象调用,那么 this 会被设置成那个对象,否则 this 的值被设置为全局对象或者 undefined

(2)创建词法环境组件

  • 词法环境是一种有标识符——变量映射的数据结构,标识符是指变量/函数名,变量是对实际对象或原始数据的引用。
  • 词法环境的内部有两个组件:环境记录器:用来储存变量和函数声明的实际位置; 外部环境的引用:可以访问外部词法环境 let const
  • 所以

(3)创建变量环境组件

  • 变量环境也是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。

2)执行阶段

此阶段会完成对变量的分配,最后执行完代码。

简单来说执行上下文就是指:

在执行一点JS代码之前,需要先解析代码。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。这一步执行完了,才开始正式的执行程序。

在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出this、arguments和函数的参数。

  • 全局上下文:变量定义,函数声明
  • **函数上下文:变量定义,函数声明,this,**arguments

49.对this对象的理解

this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断。

  • 第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。
  • 第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,this 指向这个对象。
  • 第三种是构造器调用模式,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。
  • 第四种是 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。

这四种方式,使用构造器调用模式的优先级最高,然后是 apply、call 和 bind 调用模式,然后是方法调用模式,然后是函数调用模式。

50.call() 和 apply() 的区别?

它们的作用一模一样,区别仅在于传入参数的形式的不同。

  • apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数。
  • call 传入的参数数量不固定,跟 apply 相同的是,第一个参数也是代表函数体内的 this 指向,从第二个参数开始往后,每个参数被依次传入函数。

51.实现call、apply 及 bind 函数

  • 判断调用的对象是不是一个函数,即使是定义在函数原型上的,也有可能出现使用call等方式调用的情况
  • 截取除第一个外的剩余参数,并判断context上下文对象是否存在,不存在则设置为window
  • 将函数作为上下文对象的一个方法,添加在上下文对象上
  • 调用该上下文对象的方法并把参数传进去
  • 删除该上下文对象的方法属性
  • 返回调用后的结果
Function.prototype.myCall = function (ctx, ...args) {
if (typeof this !== "function") return
ctx = ctx || window
const fn = Symbol()
ctx[fn] = this
const result = ctx[fn](...args)
delete ctx[fn]
return result
}

(2)apply函数的实现

  • 判断调用对象是不是一个函数,即使定义在原型,也会出现apply方式调用的场景
  • 判断传入的context即第一个参数是否存在,如果不存在,那就默认是window
  • 给上下文对象添加属性方法fn
  • 判断apply的第二个参数是否存在,如果不存在直接调用,如果存在把参数放进去调用
  • 删除新添加的属性方法
  • 返回调用后的结果
Function.prototype.myCall = function (ctx, args) {
if (typeof this !== "function") return
ctx = ctx || window
const fn = Symbol()
ctx[fn] = this
const result = ctx[fn](...args)
delete ctx[fn]
return result
}

(3)bind函数的实现

  • 判断调用对象是不是一个函数,即使定义在原型上,也会出现bind方式调用的场景
  • 获取除第一个参数外的剩余参数
  • 判断context上下文对象是否存在
  • 返回一个函数,并且判断是否为new构造函数,即使用instanceof判断是否函数内部this指向构造函数
  • 如果不是则使用apply调用,把剩余参数映射到第一个位置
  • 如果是则使用new构造函数,把函数内部this指向新创建的对象
Function.prototype.myBind = function (ctx, ...args1) {
if (typeof this !== "function") return
const fn = this
return function (...args2) {
const allArgs = [...args1, ...args2]
if (new.target) {
// this instanceof fn 判断是否是new 构造函数调用
return new fn(...allArgs)
} else {
return fn.apply(ctx, allArgs)
}
}
}

52.异步编程的实现方式?

JavaScript中的异步机制可以分为以下几种:

  • 回调函数 的方式,使用回调函数的方式有一个缺点是,多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护。
  • Promise 的方式,使用 Promise 的方式可以将嵌套的回调函数作为链式调用。但是使用这种方法,有时会造成多个 then 的链式调用,可能会造成代码的语义不够明确。
  • generator 的方式,它可以在函数的执行过程中,将函数的执行权转移出去,在函数外部还可以将执行权转移回来。当遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕时再将执行权给转移回来。因此在 generator 内部对于异步操作的方式,可以以同步的顺序来书写。使用这种方式需要考虑的问题是何时将函数的控制权转移回来,因此需要有一个自动执行 generator 的机制,比如说 co 模块等方式来实现 generator 的自动执行。
  • async 函数 的方式,async 函数是 generator 和 promise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。因此可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。

53.setTimeout、Promise、Async/Await 的区别

(1)setTimeout

console.log('script start') //1. 打印 script start
setTimeout(function(){
console.log('settimeout') // 4. 打印 settimeout
}) // 2. 调用 setTimeout 函数,并定义其完成后执行的回调函数
console.log('script end') //3. 打印 script start
// 输出顺序:script start->script end->settimeout

(2)Promise

Promise本身是同步的立即执行函数, 当在executor中执行resolve或者reject的时候, 此时是异步操作, 会先执行then/catch等,当主栈完成后,才会去调用resolve/reject中存放的方法执行,打印p的时候,是打印的返回结果,一个Promise实例。

console.log('script start')
let promise1 = new Promise(function (resolve) {
console.log('promise1')
resolve()
console.log('promise1 end')
}).then(function () {
console.log('promise2')
})
setTimeout(function(){
console.log('settimeout')
})
console.log('script end')
// 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout

当JS主线程执行到Promise对象时:

  • promise1.then() 的回调就是一个 task
  • promise1 是 resolved或rejected: 那这个 task 就会放入当前事件循环回合的 microtask queue
  • promise1 是 pending: 这个 task 就会放入 事件循环的未来的某个(可能下一个)回合的 microtask queue 中
  • setTimeout 的回调也是个 task ,它会被放入 macrotask queue 即使是 0ms 的情况

(3)async/await

async function async1(){
console.log('async1 start');
await async2();
console.log('async1 end')
}
async function async2(){
console.log('async2')
}
console.log('script start');
async1();
console.log('script end')
// 输出顺序:script start->async1 start->async2->script end->async1 end

async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。

例如:

async function func1() {
return 1
}
console.log(func1())

func1的运行结果其实就是一个Promise对象。因此也可以使用then来处理后续逻辑。

func1().then(res => {
console.log(res); // 30
})

await的含义为等待,也就是 async 函数需要等待await后的函数执行完成并且有了返回结果(Promise对象)之后,才能继续执行下面的代码。await通过返回一个Promise对象来实现同步的效果。

54.对Promise的理解

Promise是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,他的出现大大改善了异步编程的困境,避免了地狱回调,它比传统的解决方案回调函数和事件更合理和更强大。

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

(1)Promise的实例有三个状态:

  • Pending(进行中)
  • Resolved(已完成)
  • Rejected(已拒绝)

当把一件事情交给promise时,它的状态就是Pending,任务完成了状态就变成了Resolved、没有完成失败了就变成了Rejected。

(2)Promise的实例有两个过程

  • pending -> fulfilled : Resolved(已完成)
  • pending -> rejected:Rejected(已拒绝)

注意:一旦从进行状态变成为其他状态就永远不能更改状态了。

Promise的特点:

  • 对象的状态不受外界影响。promise对象代表一个异步操作,有三种状态,pending(进行中)、fulfilled(已成功)、rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态,这也是promise这个名字的由来——“承诺”;
  • 一旦状态改变就不会再变,任何时候都可以得到这个结果。promise对象的状态改变,只有两种可能:从 pending****变为 fulfilled**,从** pending****变为 rejected**。这时就称为** resolved**(已定型)。如果改变已经发生了,你再对promise对象添加回调函数,也会立即得到这个结果。这与事件(event)完全不同,事件的特点是:如果你错过了它,再去监听是得不到结果的。**

Promise的缺点:

  • 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
  • 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
  • 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

总结:

Promise 对象是异步编程的一种解决方案,最早由社区提出。Promise 是一个构造函数,接收一个函数作为参数,返回一个 Promise 实例。一个 Promise 实例有三种状态,分别是pending、resolved 和 rejected,分别代表了进行中、已成功和已失败。实例的状态只能由 pending 转变 resolved 或者rejected 状态,并且状态一经改变,就凝固了,无法再被改变了。

状态的改变是通过 resolve() 和 reject() 函数来实现的,可以在异步操作结束后调用这两个函数改变 Promise 实例的状态,它的原型上定义了一个 then 方法,使用这个 then 方法可以为两个状态的改变注册回调函数。这个回调函数属于微任务,会在本轮事件循环的末尾执行。

注意:在构造 Promise 的时候,构造函数内部的代码是立即执行的

55.Promise的基本用法

const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});

一般情况下都会使用new Promise()来创建promise对象,但是也可以使用promise.resolvepromise.reject这两个方法:

  • Promise.resolve

**Promise.resolve(value)**的返回值也是一个promise对象,可以对返回值进行.then调用,代码如下:

Promise.resolve(11).then(function(value){
console.log(value); // 打印出11
});

**resolve(11)**代码中,会让promise对象进入确定(resolve状态),并将参数 11****传递给后面的 then****所指定的 onFulfilled 函数;

创建promise对象可以使用 new Promise****的形式创建对象,也可以使用 **Promise.resolve(value)**的形式创建promise对象;

  • Promise.reject

Promise.reject 也是 new Promise****的快捷形式,也创建一个promise对象。代码如下:

Promise.reject(new Error(“我错了,请原谅俺!!”));

就是下面的代码new Promise的简单形式:

new Promise(function(resolve,reject){
reject(new Error("我错了,请原谅俺!!"));
});

下面是使用resolve方法和reject方法:

function testPromise(ready) {
return new Promise(function(resolve,reject){
if(ready) {
resolve("hello world");
}else {
reject("No thanks");
}
});
};
// 方法调用
testPromise(true).then(function(msg){
console.log(msg);
},function(error){
console.log(error);
});

上面的代码的含义是给 testPromise****方法传递一个参数,返回一个promise对象,如果为 true****的话,那么调用promise对象中的 **resolve()**方法,并且把其中的参数传递给后面的 then****第一个函数内,因此打印出 “hello world”, 如果为 false****的话,会调用promise对象中的 reject()方法,则会进入 then****的第二个函数内,会打印 No thanks

(2)Promise方法

Promise有五个常用的方法:then()、catch()、all()、race()、finally。下面就来看一下这些方法。

  • then()

当Promise执行的内容符合成功条件时,调用 resolve****函数,失败就调用 reject****函数。Promise创建完了,那该如何调用呢?

promise.then(function(value) {
// success
}, function(error) {
// failure
});

then****方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为 resolved****时调用,第二个回调函数是Promise对象的状态变为 rejected****时调用。其中第二个参数可以省略。

then****方法返回的是一个新的Promise实例(不是原来那个Promise实例)。因此可以采用链式写法,即 then****方法后面再调用另一个then方法。

当要写有顺序的异步事件时,需要串行时,可以这样写:

let promise = new Promise((resolve,reject)=>{
ajax('first').success(function(res){
resolve(res);
})
})
promise.then(res=>{
return new Promise((resovle,reject)=>{
ajax('second').success(function(res){
resolve(res)
})
})
}).then(res=>{
return new Promise((resovle,reject)=>{
ajax('second').success(function(res){
resolve(res)
})
})
}).then(res=>{

})

那当要写的事件没有顺序或者关系时,还如何写呢?可以使用 all 方法来解决。

  • catch()

Promise对象除了有then方法,还有一个catch方法,该方法相当于 then****方法的第二个参数,指向 reject****的回调函数。不过 catch****方法还有一个作用,就是在执行 resolve****回调函数时,如果出现错误,抛出异常,不会停止运行,而是进入 catch****方法中。

p.then((data) => {
console.log('resolved',data);
},(err) => {
console.log('rejected',err);
}
);
p.then((data) => {
console.log('resolved',data);
}).catch((err) => {
console.log('rejected',err);
});
  • all()

all****方法可以完成并行任务, 它接收一个数组,数组的每一项都是一个 promise****对象。当数组中所有的 promise****的状态都达到 resolved****的时候,all方法的状态就会变成 resolved**,如果有一个状态变成了** rejected**,那么** all****方法的状态就会变成 rejected**。**

javascript
let promise1 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(1);
},2000)
});
let promise2 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(2);
},1000)
});
let promise3 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(3);
},3000)
});
Promise.all([promise1,promise2,promise3]).then(res=>{
console.log(res);
//结果为:[1,2,3]
})
function all(promises){
return new Promise((resolve,reject) => {
let lens = promises.length;
let count = 0;
let res = [];
for(let i = 0; i < lens; i++){
promises[i].then(val => {
count++;
res.push(val);
if(count === lens){
resolve(res)
}
}).catch(error => {
reject(error)
})
}
})
}

调用 all****方法时的结果成功的时候是回调函数的参数也是一个数组,这个数组按顺序保存着每一个promise对象 resolve****执行时的值。

  • race()

race****方法和 all****一样,接受的参数是一个每项都是 promise****的数组,但是与 all****不同的是,当最先执行完的事件执行完之后,就直接返回该 promise****对象的值。如果第一个 promise****对象状态变成 resolved**,那自身的状态变成了** resolved**;反之第一个** promise****变成 rejected**,那自身状态就会变成** rejected**。**

let promise1 = new Promise((resolve,reject)=>{
setTimeout(()=>{
reject(1);
},2000)
});
let promise2 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(2);
},1000)
});
let promise3 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(3);
},3000)
});
Promise.race([promise1,promise2,promise3]).then(res=>{
console.log(res);
//结果:2
},rej=>{
console.log(rej)};
)

那么 race****方法有什么实际作用呢?当要做一件事,超过多长时间就不做了,可以用这个方法来解决:

Promise.race([promise1,timeOutPromise(5000)]).then(res=>)

  • finally()

finally****方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

上面代码中,不管 promise****最后的状态,在执行完 then****或 catch****指定的回调函数以后,都会执行 finally****方法指定的回调函数。

下面是一个例子,服务器使用 Promise 处理请求,然后使用 finally****方法关掉服务器。

server.listen(port)
.then(function () {
// ...
})
.finally(server.stop);

finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是 fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。finally本质上是then方法的特例:

promise
.finally(() => {
// 语句
});
// 等同于
promise
.then(
result => {
// 语句
return result;
},
error => {
// 语句
throw error;
}
);

上面代码中,如果不使用 finally方法,同样的语句需要为成功和失败两种情况各写一次。有了 finally方法,则只需要写一次。

56.Promise解决了什么问题

在工作中经常会碰到这样一个需求,比如我使用ajax发一个A请求后,成功后拿到数据,需要把数据传给B请求;那么需要如下编写代码:

let fs = require('fs')
fs.readFile('./a.txt','utf8',function(err,data){
fs.readFile(data,'utf8',function(err,data){
fs.readFile(data,'utf8',function(err,data){
console.log(data)
})
})
})

上面的代码有如下缺点:

  • 后一个请求需要依赖于前一个请求成功后,将数据往下传递,会导致多个ajax请求嵌套的情况,代码不够直观。
  • 如果前后两个请求不需要传递参数的情况下,那么后一个请求也需要前一个请求成功后再执行下一步操作,这种情况下,那么也需要如上编写代码,导致代码不够直观。

Promise****出现之后,代码变成这样:

let fs = require('fs')
function read(url){
return new Promise((resolve,reject)=>{
fs.readFile(url,'utf8',function(error,data){
error && reject(error)
resolve(data)
})
})
}
read('./a.txt').then(data=>{
return read(data)
}).then(data=>{
return read(data)
}).then(data=>{
console.log(data)
})

这样代码看起了就简洁了很多,解决了地狱回调的问题。

57.Promise.all和Promise.race的区别的使用场景

(1)Promise.all

Promise.all****可以将多个 Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组**,而失败的时候则返回最先被reject失败状态的值。**

Promise.all中传入的是数组,返回的也是是数组,并且会将进行映射,传入的promise对象返回的值是按照顺序在数组中排列的,但是注意的是他们执行的顺序并不是按照顺序的,除非可迭代对象为空。

需要注意,Promise.all获得的成功结果的数组里面的数据顺序和Promise.all接收到的数组顺序是一致的,这样当遇到发送多个请求并根据请求顺序获取和使用数据的场景,就可以使用Promise.all来解决。

(2)Promise.race

顾名思义,Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。当要做一件事,超过多长时间就不做了,可以用这个方法来解决:

Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})

58.对async/await 的理解

async/await其实是 Generator 的语法糖,它能实现的效果都能用then链来实现,它是为优化then链而开发出来的。从字面上来看,async是“异步”的简写,await则为等待,所以很好理解async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。当然语法上强制规定await只能出现在asnyc函数中,先来看看async函数返回了什么:

async function testAsy(){
return 'hello world';
}
let result = testAsy();
console.log(result)

img

所以,async 函数返回的是一个 Promise 对象。async 函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。

**async 函数返回的是一个 Promise 对象,所以在最外层不能用 await 获取其返回值的情况下,当然应该用原来的方式:**then() 链来处理这个 Promise 对象,就像这样:

async function testAsy(){
return 'hello world'
}
let result = testAsy()
console.log(result)
result.then(v=>{
console.log(v) // hello world
})

那如果 async 函数没有返回值,又该如何?很容易想到,它会返回 Promise.resolve(undefined)****。

联想一下 Promise 的特点——无等待,所以在没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。

**注意:**Promise.resolve(x) 可以看作是 new Promise(resolve => resolve(x)) 的简写,可以用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。

59.await 到底在等啥?

因为 async 函数返回一个 Promise 对象,所以 await 可以用于等待一个 async 函数的返回值——这也可以说是 await 在等 async 函数,但要清楚,它等的实际是一个返回值。注意到 await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果,所以,await 后面实际是可以接普通函数调用或者直接量的。所以下面这个示例完全可以正确运行:

function getSomething() {
return "something";
}
async function testAsync() {
return Promise.resolve("hello async");
}
async function test() {
const v1 = await getSomething();
const v2 = await testAsync();
console.log(v1, v2);
}
test();

await 表达式的运算结果取决于它等的是什么。

  • 如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。
  • 如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。

来看一个例子:

function testAsy(x){
return new Promise(resolve=>{setTimeout(() => {
resolve(x);
}, 3000)
}
)
}
async function testAwt(){
let result = await testAsy('hello world');
console.log(result); // 3秒钟之后出现hello world
console.log('cuger') // 3秒钟之后出现cug
}
testAwt();
console.log('cug') //立即输出cug

这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。await暂停当前async的执行,所以'cug''最先输出,hello world'和‘cuger’是3秒钟后同时出现的。

60.async/await的优势

/**
* 传入参数 n,表示这个函数执行的时间(毫秒)
* 执行的结果是 n + 200,这个值将用于下一步骤
*/
function takeLongTime(n) {
return new Promise(resolve => {
setTimeout(() => resolve(n + 200), n);
});
}
function step1(n) {
console.log(`step1 with ${n}`);
return takeLongTime(n);
}
function step2(n) {
console.log(`step2 with ${n}`);
return takeLongTime(n);
}
function step3(n) {
console.log(`step3 with ${n}`);
return takeLongTime(n);
}

现在用 Promise 方式来实现这三个步骤的处理:

function doIt() {
console.time("doIt");
const time1 = 300;
step1(time1)
.then(time2 => step2(time2))
.then(time3 => step3(time3))
.then(result => {
console.log(`result is ${result}`);
console.timeEnd("doIt");
});
}
doIt();
// c:\var\test>node --harmony_async_await .
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1507.251ms

如果用 async/await 来实现呢,会是这样:

async function doIt() {
console.time("doIt");
const time1 = 300;
const time2 = await step1(time1);
const time3 = await step2(time2);
const result = await step3(time3);
console.log(`result is ${result}`);
console.timeEnd("doIt");
}
doIt();

结果和之前的 Promise 实现是一样的,但是这个代码看起来是不是清晰得多,几乎跟同步代码一样

61.async/await对比Promise的优势

  • 代码读起来更加同步,Promise虽然摆脱了回调地狱,但是then的链式调⽤也会带来额外的阅读负担
  • Promise传递中间值⾮常麻烦,⽽async/await⼏乎是同步的写法,⾮常优雅
  • 错误处理友好,async/await可以⽤成熟的try/catch,Promise的错误捕获⾮常冗余
  • 调试友好,Promise的调试很差,由于没有代码块,你不能在⼀个返回表达式的箭头函数中设置断点,如果你在⼀个.then代码块中使⽤调试器的步进(step-over)功能,调试器并不会进⼊后续的.then代码块,因为调试器只能跟踪同步代码的每⼀步。

62.async/await 如何捕获异常

async function fn(){
try{
let a = await Promise.reject('error')
}catch(error){
console.log(error)
}
}

63.什么是回调函数?回调函数有什么缺点?如何解决回调地狱问题?

以下代码就是一个回调函数的例子:

ajax(url, () => {
// 处理逻辑
})

回调函数有一个致命的弱点,就是容易写出回调地狱(Callback hell)。假设多个请求存在依赖性,可能会有如下代码:

ajax(url, () => {
// 处理逻辑
ajax(url1, () => {
// 处理逻辑
ajax(url2, () => {
// 处理逻辑
})
})
})

以上代码看起来不利于阅读和维护,当然,也可以把函数分开来写:

function firstAjax() {
ajax(url1, () => {
// 处理逻辑
secondAjax()
})
}
function secondAjax() {
ajax(url2, () => {
// 处理逻辑
})
}
ajax(url, () => {
// 处理逻辑
firstAjax()
})

以上的代码虽然看上去利于阅读了,但是还是没有解决根本问题。回调地狱的根本问题就是:

  1. 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身
  2. 嵌套函数一多,就很难处理错误

当然,回调函数还存在着别的几个缺点,比如不能使用 try catch 捕获错误,不能直接 return

64.setTimeout、setInterval、requestAnimationFrame 各有什么特点?

异步编程当然少不了定时器了,常见的定时器函数有 setTimeoutsetIntervalrequestAnimationFrame。最常用的是 setTimeout,很多人认为 setTimeout 是延时多久,那就应该是多久后执行。

其实这个观点是错误的,因为 JS 是单线程执行的,如果前面的代码影响了性能,就会导致 setTimeout 不会按期执行。当然了,可以通过代码去修正 setTimeout,从而使定时器相对准确:

let period = 60 * 1000 * 60 * 2
let startTime = new Date().getTime()
let count = 0
let end = new Date().getTime() + period
let interval = 1000
let currentInterval = interval
function loop() {
count++
// 代码执行所消耗的时间
let offset = new Date().getTime() - (startTime + count * interval);
let diff = end - new Date().getTime()
let h = Math.floor(diff / (60 * 1000 * 60))
let hdiff = diff % (60 * 1000 * 60)
let m = Math.floor(hdiff / (60 * 1000))
let mdiff = hdiff % (60 * 1000)
let s = mdiff / (1000)
let sCeil = Math.ceil(s)
let sFloor = Math.floor(s)
// 得到下一次循环所消耗的时间
currentInterval = interval - offset
console.log('时:'+h, '分:'+m, '毫秒:'+s, '秒向上取整:'+sCeil, '代码执行时间:'+offset, '下次循环间隔'+currentInterval)
setTimeout(loop, currentInterval)
}
setTimeout(loop, currentInterval)

接下来看 setInterval,其实这个函数作用和 setTimeout 基本一致,只是该函数是每隔一段时间执行一次回调函数。

通常来说不建议使用 setInterval。第一,它和 setTimeout 一样,不能保证在预期的时间执行任务。第二,它存在执行累积的问题,请看以下伪代码

function demo() {
setInterval(function(){
console.log(2)
},1000)
sleep(2000)
}
demo()

以上代码在浏览器环境中,如果定时器执行过程中出现了耗时操作,多个回调函数会在耗时操作结束以后同时执行,这样可能就会带来性能上的问题。

如果有循环定时器的需求,其实完全可以通过 requestAnimationFrame 来实现:

function setInterval(callback, interval) {
let timer
const now = Date.now
let startTime = now()
let endTime = startTime
const loop = () => {
timer = window.requestAnimationFrame(loop)
endTime = now()
if (endTime - startTime >= interval) {
startTime = endTime = now()
callback(timer)
}
}
timer = window.requestAnimationFrame(loop)
return timer
}
let a = 0
setInterval(timer => {
console.log(1)
a++
if (a === 3) cancelAnimationFrame(timer)
}, 1000)

首先 requestAnimationFrame 自带函数节流功能,基本可以保证在 16.6 毫秒内只执行一次(不掉帧的情况下),并且该函数的延时效果是精确的,没有其他定时器时间不准的问题,当然你也可以通过该函数来实现 setTimeout

65.对象创建的方式有哪些?

一般使用字面量的形式直接创建对象,但是这种创建方式对于创建大量相似对象的时候,会产生大量的重复代码。但 js和一般的面向对象的语言不同,在 ES6 之前它没有类的概念。但是可以使用函数来进行模拟,从而产生出可复用的对象创建方式,常见的有以下几种:

(1)第一种是工厂模式,工厂模式的主要工作原理是用函数来封装创建对象的细节,从而通过调用函数来达到复用的目的。但是它有一个很大的问题就是创建出来的对象无法和某个类型联系起来,它只是简单的封装了复用代码,而没有建立起对象和类型间的关系。

function createPerson(name) {
var o = new Object();
o.name = name;
o.getName = function() {
console.log(this.name);
};

return o;
}

var person1 = createPerson('kevin');

缺点:对象无法识别,因为所有的实例都指向一个原型

(2)第二种是构造函数模式。js 中每一个函数都可以作为构造函数,只要一个函数是通过 new 来调用的,那么就可以把它称为构造函数。执行构造函数首先会创建一个对象,然后将对象的原型指向构造函数的 prototype 属性,然后将执行上下文中的 this 指向这个对象,最后再执行整个函数,如果返回值不是对象,则返回新建的对象。因为 this 的值指向了新建的对象,因此可以使用 this 给对象赋值。构造函数模式相对于工厂模式的优点是,所创建的对象和构造函数建立起了联系,因此可以通过原型来识别对象的类型。但是构造函数存在一个缺点就是,造成了不必要的函数对象的创建,因为在 js 中函数也是一个对象,因此如果对象属性中如果包含函数的话,那么每次都会新建一个函数对象,浪费了不必要的内存空间,因为函数是所有的实例都可以通用的。

  function Person(name){
this.name = name;
this.getName = function(){
console.log(this.name);
}
}
var person1 = new Person('kevin')

优点: 实例可以识别一个特定的类型

缺点:每次创建实例的时候,每个方法都要被创建一次(方法指的是构造函数内部的方法)

(3)第三种模式是原型模式,因为每一个函数都有一个 prototype 属性,这个属性是一个对象,它包含了通过构造函数创建的所有实例都能共享的属性和方法。因此可以使用原型对象来添加公用属性和方法,从而实现代码的复用。这种方式相对于构造函数模式来说,解决了函数对象的复用问题。但是这种模式也存在一些问题,一个是没有办法通过传入参数来初始化值,另一个是如果存在一个引用类型如 Array 这样的值,那么所有的实例将共享一个对象,一个实例对引用类型值的改变会影响所有的实例。

function Person(name) {}

Person.prototype.name = 'kevin';
Person.prototype.getName = function() {
console.log(this.name);
}

var person1 = new Person();

优点: 方法不会被重新创建

缺点: 1、所有的属性和方法都共享,2、不能初始化参数

(4)第四种模式是组合使用构造函数模式和原型模式,这是创建自定义类型的最常见方式。因为构造函数模式和原型模式分开使用都存在一些问题,因此可以组合使用这两种模式,通过构造函数来初始化对象的属性,通过原型对象来实现函数方法的复用。这种方法很好的解决了两种模式单独使用时的缺点,但是有一点不足的就是,因为使用了两种不同的模式,所以对于代码的封装性不够好。

 function Person(name){
this.name = name;
}
Person.prototype = {
constructor: Person,
getName: function(){
console.log(this.name)
}
}

var person1 = new Person();

优点:应该共享的共享,该私有的私有,使用最广泛的方式

缺点:有的人就是希望全部写在一起,即拥有更好的封装性

(5)第五种模式是动态原型模式,这一种模式将原型方法赋值的创建过程移动到了构造函数的内部,通过对属性是否存在的判断,可以实现仅在第一次调用函数时对原型对象赋值一次的效果。这一种方式很好地对上面的混合模式进行了封装。

function Perosn(name) {
this.name = name;
if (typeof this.getName != "function") {
Person.prototype.getName = function() {
console.log(this.name);
};
}
}

var person1 = new Person();

解决了原型只创建一次

(6)第六种模式是寄生构造函数模式,这一种模式和工厂模式的实现基本相同,我对这个模式的理解是,它主要是基于一个已有的类型,在实例化时对实例化的对象进行扩展。这样既不用修改原来的构造函数,也达到了扩展对象的目的。它的一个缺点和工厂模式一样,无法实现对象的识别。

function Person(name) {
var o = new Object();
o.name = name;
o.getName = function() {
console.log(this.name);
};
return o;
}

var person1 = new Person("kevin");

console.log(person1 instanceof Person); // false
console.log(person2 instanceof Object); // true

66.对象继承的方式有哪些?

(1)第一种是以原型链的方式来实现继承,但是这种实现方式存在的缺点是,在包含有引用类型的数据时,会被所有的实例对象所共享,容易造成修改的混乱。还有就是在创建子类型的时候不能向父类型传递参数。

 function Parent1() {
this.name = 'parent1';
this.play = [1, 2, 3]
}
function Child1() {
this.type = 'child1';
}
Child1.prototype = new Parent1();
console.log(new Child1());

var s1 = new Child1()
var s2 = new Child1()
s1.play.push(4)
console.log(s1.play, s2.play) // [1,2,3,4] [1,2,3,4]

但是有一个弊端就是,原型继承的对象只是一个引用,那么就是每个实例都可以修改,这就是使用原型链继承方式的一个缺点。
因为我们期望的是s2 = [1,2,3]
关于编程想法,为什么Child1.prototype = new Parent1()不放入Child1构造函数里面呢?
因为当我们new关键字作用的时候,通过 obj.__proto__ = Object.create(constructor.protype),这相当于拷贝对象,
那么下面执行这个constructor改变的原型已经意义不大了,因为指向的位置变了。

(2)第二种方式是使用借用构造函数的方式,这种方式是通过在子类型的函数中调用超类型的构造函数来实现的,这一种方法解决了不能向超类型传递参数的缺点,但是它存在的一个问题就是无法实现函数方法的复用,并且超类型原型定义的方法子类型也没有办法访问到。

function Parent2(){
this.name = 'parent2';
}

Parent2.prototype.getName = function () {
return this.name;
}

function Child2(){
Parent2.call(this);
this.type = 'child2'
}

let child = new Child2();
console.log(child); // 没问题
console.log(child.getName()); // 会报错


除了 Child2 的属性 type 之外,也继承了 Parent2 的属性 name。这样写的时候子类虽然能够拿到父类的属性值,解决了第一种继承方式的弊端,但问题是,只能继承父类的实例属性和方法,不能继承原型属性或者方法。

上面的两种继承方式各有优缺点,那么结合二者的优点,于是就产生了下面这种组合的继承方式。

(3)第三种方式是组合继承,组合继承是将原型链和借用构造函数组合起来使用的一种方式。通过借用构造函数的方式来实现类型的属性的继承,通过将子类型的原型设置为超类型的实例来实现方法的继承。这种方式解决了上面的两种模式单独使用时的问题,但是由于我们是以超类型的实例来作为子类型的原型,所以调用了两次超类的构造函数,造成了子类型的原型中多了很多不必要的属性。

function Parent3 (age) {
 this.name = 'parent3';
 this.play = [1, 2, 3];
 this.age = age
}

Parent3.prototype.getName = function () {
 return this.name;
}
function Child3() {
 // 第二次调用 Parent3()
 Parent3.call(this,30);
 this.type = 'child3';
}

// 第一次调用 Parent3()
Child3.prototype = new Parent3(30);
// 手动挂上构造器,指向自己的构造函数
Child3.prototype.constructor = Child3;
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play);  // 不互相影响
console.log(s3.getName()); // 正常输出'parent3'
console.log(s4.getName()); // 正常输出'parent3'

又增加了一个新问题:通过注释我们可以看到 Parent3 执行了两次,第一次是改变Child3 的 prototype 的时候,第二次是通过 call 方法调用 Parent3 的时候,那么 Parent3 多构造一次就多进行了一次性能开销,这是我们不愿看到的。

这里还有一个比较细节的问题是第二次调用的Parent3,出现了属性在不同层级重复,Parent3的age也会在实例第一层对象上面,拥有这个“多余的”属性也按照原型链的规则,没什么问题。但在某些情况下会造成错误,例如删除实例上的age属性后,实际上还能访问到,此时获取到的是原型上的属性。

(4)第四种方式是原型式继承,原型式继承的主要思路就是基于已有的对象来创建新的对象,实现的原理是,向函数中传入一个对象,然后返回一个以这个对象为原型的对象。这种继承的思路主要不是为了实现创造一种新的类型,只是对某个对象实现一种简单继承,ES5 中定义的 Object.create() 方法就是原型式继承的实现。缺点与原型链方式相同。

let parent4 = {
   name: "parent4",
   friends: ["p1", "p2", "p3"],
   getName: function() {
     return this.name;
  }
};

 let person4 = Object.create(parent4);
 person4.name = "tom";
 person4.friends.push("jerry");

 let person5 = Object.create(parent4);
 person5.friends.push("lucy");

 console.log(person4.name); // tom
 console.log(person4.name === person4.getName()); // tom === tom ===> tom
 console.log(person5.name); // parent4
 console.log(person4.friends); // ["p1", "p2", "p3",'jerry','lucy']
 console.log(person5.friends); // ["p1", "p2", "p3",'jerry','lucy']
这种继承方式弊端是拷贝对象引用,这种可能会导致对象被修改。

(5)第五种方式是寄生式继承,寄生式继承的思路是创建一个用于封装继承过程的函数,通过传入一个对象,然后复制一个对象的副本,然后对象进行扩展,最后返回这个对象。这个扩展的过程就可以理解是一种继承。这种继承的优点就是对一个简单对象实现继承,如果这个对象不是自定义类型时。缺点是没有办法实现函数的复用。

 let parent5 = {
   name: "parent5",
   friends: ["p1", "p2", "p3"],
   getName: function() {
     return this.name;
  }
};
 function clone(original) {
   let clone = Object.create(original);
   clone.getFriends = function() {
     return this.friends;
  };
   return clone;
}
 let person5 = clone(parent5);
 console.log(person5.getName());
 console.log(person5.getFriends());
缺点是没有办法实现函数的复用

(6)第六种方式是寄生式组合继承,组合继承的缺点就是使用超类型的实例做为子类型的原型,导致添加了不必要的原型属性。寄生式组合继承的方式是使用超类型的原型的副本来作为子类型的原型,这样就避免了创建不必要的属性

  function Parent6() {
   this.name = 'parent6';
   this.play = [1, 2, 3];
}
  Parent6.prototype.getName = function () {
   return this.name;
}
 function Child6() {
   Parent6.call(this);
   this.friends = 'child6';
}
 function clone (parent, child) {
   // 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
   child.prototype = Object.create(parent.prototype);
   child.prototype.constructor = child;
}
 clone(Parent6, Child6);
 Child6.prototype.getFriends = function () {
   return this.friends;
}
 let person6 = new Child6();
 console.log(person6);
 console.log(person6.getName());
 console.log(person6.getFriends());
// 它可以解决组合继承 父类被调用两次和在不同层级属性重复的问题。

(7) es6的extends

67.浏览器的垃圾回收机制

关键词:垃圾回收的过程、如何实现垃圾回收、新生代和老生代、全停顿

1、垃圾回收的过程:

  1. 通过GC ROOT 标记空间中的活动对象和非活动对象:目前V8采用的是可访问性算法来判断堆中的对象是否是活动对象。这个算法是将一些GC ROOT 作为初始存活的对象的集合,从GC Roots对象出发,遍历GC Root中所有的对象

  2. 通过GC Root遍历的对象是可访问的,会在内存中保留下来,也叫活动对象

  3. 通过GC Roots遍历的对象是不可访问的,也叫非活动对象,这些对象就可能会被回收

  4. 回收非活动对象所占据的内存:标记完对象之后,统一清理内存中所有被标记为可回收的对象

  5. 内存整理:意思就是再释放掉大量内存之后,内存会存在大量不连续空间,这些内存空间称为内存碎片。当内存中出现大量内存碎片后,如果再次需要分配较大的连续内存时,就有可能出现内存不足的情况,所以最后一步就是整理这些内存碎片。这一步是可选的,因为有的垃圾回收不会产生内存碎片

2、V8的两个垃圾回收器:主垃圾回收器和副垃圾回收器

新生代:存放时间较短的对象;

老生代:存放时间较长的对象

  1. 副垃圾回收器(新生代):负责新生代的垃圾回收。大多数的对象最开始的时候就存放在新生代中,该存储空间较小,但分为两个空间:form空间(对象区)和to空间(休闲区)。

  2. 新加入的对象会被放在对象区,当对象区被放满时,就需要执行一次垃圾清理操作:首先将对对象区的垃圾做标记,标记完,执行清理操作。然后副垃圾回收器会把这些存活的对象放在休闲区,同时有序的排列。这个操作就相当于完成了内存整理,排列后的空闲区就不会有内存碎片了。

  3. 完成复制后,对象区和休闲区将会进行角色翻转,也就是将原来的对象区变成休闲区,原来的休闲区变成对象区,这种算法叫做Scavenge算法,这样就完成了垃圾对象的回收操作,同时这种翻转的操作可以让新生代的这两块区域无限重复使用下去

  4. 不过,副垃圾回收器每次执行清理操作的时候,都需要将存活的对象从对象区复制到空闲区,复制操作需要时间成本,如果新生区空间设置的太大,那么每次清理的时间就会过久,所以为了执行效率,一般新生区的空间都会被设置的很小。也正是因为新生区空间设置的小,所以很容易被活动的对象给装满,所以需要副垃圾回收器区中垃圾回收;并且副垃圾回收器针对经过了两次翻转还保留下来的对象,将会被晋升到老生代中

  5. 主垃圾回收器(老生代):负责老生代的垃圾回收。除了新生代晋升的对象外,还有一些大的对象会直接被分配到老生代理;因此老生代有两个特点:

  6. 对象占用空间大

  7. 对象存活时间久

  8. 因为老生代的对象比较大,所以不使用Scavenge算法进行垃圾回收。因为复制这些大的对象需要很长时间,影响执行效率。所以老生代采用的算法是标记清除算法进行垃圾回收。这个算法分为两个阶段:标记和清除

  9. 标记阶段:从一组根元素开始,递归的遍历这组根元素,在遍历过程中,能到达的元素被称为活动对象,没有到达的元素被判断为垃圾数据

  10. 清除阶段:主垃圾回收器会直接将被标记为垃圾的数据清理掉

  11. 然后因为主垃圾回收器清理掉的数据会产生内存碎片,而较大的连续的内存碎片会导致大数据对象无法分配到连续内存中;所以有引入了另一个算法----标记整理算法

  12. 这个算法和标记清除算法一样,只是在标记完可回收对象后,将所有活动的对象向一端移动,然后直接清理掉这一端之外的内存

  13. 全停顿

  14. 概念:因为JavaScript是单线程语言,运行在主线程上。一旦执行垃圾回收算法,都需要将正在执行的JavaScript脚本暂停下来,待垃圾回收完成后再切执行JavaScript脚本,这种行为就叫做全停顿

  15. 新生代因为空间较小,所以执行垃圾回收的时候影响不大;但是老生代在执行垃圾回收的时候,会执行很长时间,会阻塞主线程的执行,导致页面的卡顿现象。

  16. 所以为了减少这种卡顿的现象,V8将标记过程分为了一个个子标记过程,同时让垃圾回收标记和JavaScript任务中间交替去执行,直到标记完成。这种算法叫做增量标记算法。

  17. 增量标记算法就会将一个完整的垃圾回收过程分为一个个很小的任务,穿插到JavaScript任务中区,这样就不会让用户觉得在执行垃圾回收的过程会有卡顿的现象了

  18. 虽然浏览器可以进行垃圾回收,但是当代码比较复杂的时候,垃圾回收付出的代价很大,所以尽量减少垃圾回收。

  19. 对数组进行优化:设置数组的length为0而不是等于【】

  20. 对object进行优化:对象尽可能的复用,对于不再使用的对象,设置为null

  21. 对函数进行优化:在循环的函数表达式立,如果可以复用,尽量写在函数外面

扩展:引用计数算法:

对于值被引用的话,这该值的引用次数为1,相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变为0时,说明这个变量已经没有价值,

  • 这种方法会引起循环引用的问题:例如: obj1和 obj2通过属性进行相互引用,两个对象的引用次数都是2。当使用循环计数时,由于函数执行完后,两个对象都离开作用域,函数执行结束,obj1和 obj2还将会继续存在,因此它们的引用次数永远不会是0,就会引起循环引用。需要手动清理设置为null

68.哪些情况会导致内存泄漏

以下四种情况会造成内存的泄漏:

  • **意外的全局变量:**由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
  • **被遗忘的计时器或回调函数:**设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
  • **脱离 DOM 的引用:**获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。
  • 闭包:不合理的使用闭包,从而导致某些变量一直被留在内存当中。

69.请说一说this指向

关键词:函数执行上下文、箭头函数、new

规则:箭头函数>new>显式调用>隐式调用>默认绑定

解释:this是javascript中的一个关键字,多数情况下指向的是调用它的对象

首先,this应该指向的是一个对象(函数执行上下文对象),其次这个对象指向的是调用它它的对象,如果调用它的不是一个对象或者对象不存在,那么它指向的就是全局对象(严格模式下为undefined)

其实,this在函数被调用时就确定了,它的指向就是函数被调用的地方,而不是它声明的地方(除箭头函数外)。当函数被调用时,会创建一个执行上下文,它包含了函数在哪里被调用(调用栈)、函数调用的方式、以及函数的参数等信息,this就是一个记录它的一个属性,在函数执行的过程中会被用到

1.、默认绑定

函数的浏览器在全局环境中直接使用代表的是全局对象Window,在node环境下则是Global,如果是严格模式下,则是undefined

2、隐式绑定

理解:谁调用就指向谁

3、显式绑定

**apply、call、bind都是可以改变函数指向,**但是 call 和 apply 是直接进行函数调用 ,不会执行函数,而是返回一个新的函数,这个新的函数已经自动绑定了新的 this 指向,需要我们手动调用。 call 方法接受的是参数列表,而 apply 方法接受的是一个参数数组

const target = {}
fn.call(target, 'arg1', 'arg2')
fn.apply(target, ['arg1', 'arg2'])
fn.bind(target, 'arg1', 'arg2')()

如果传入了null或者undefined作为this指向,则this会使用默认绑定规则

并且使用多次bind绑定,永远取第一次this的指向

4、new绑定

函数作为构造函数使用 new 调用时, this 绑定的是新创建的构造函数的实例

5、特殊this指向

1.箭头函数中的 this 只取决包裹箭头函数的第一个普通函数的 this

2.数组方法:例如forEach、find、findIndex、map、some、every都可以改变this指向(第二个参数)

3.立即执行函数:就是默认的全局对象 window

4.setTimeout和setInterval:其实,延时效果(setTimeout)和定时效果(setInterval)都是在全局作用域下实现的。无论是 setTimeout 还是 setInterval 里传入的函数,都会首先被交到全局对象手上。因此,函数中 this 的值,会被自动指向 window

6、不在函数中的场景,可分为浏览器的 标签里,或 Node.js 的模块文件里。

​ 1) 在 标签里,this 指向 Window。

​ 2) 在 Node.js 的模块文件里,this 指向 Module 的默认导出对象,也就是 module.exports。