万字总结之重学js

94 阅读29分钟

数据类型

  • 基础数据类型

    存储在栈内存,被引用或拷贝时,会创建一个完全相等的变量

    undefinednullbooleanstringnumbersymbolbigInt

    number的最大整数为: Number.MAX_SAFE_INTEGER === 2^53 === 9,007,199,254,740,992 超过最大值则会精度丢失。

    bigInt用来声明任意长度的整数

    const big1 = 1234567890123456789012345678901234567890n
    const big2 = BigInt('1234567890123456789012345678901234567890')
    big1 === big2 //true
    

    symbol es6新增返回唯一值

  • 引用数据类型

    存储在堆内存,存储的是地址,多个引用指向同一个地址,一个值发生了改变,另外一个也随之跟着变化

    ObjectArrayFunctionRegExp(正则对象)Date(日期对象)Math(数学函数)

数据类型检测

typeof

基础数据类型中除了null都可以使用typeof判断,因为 typeof null === 'object',所以判断null可以直接if(xxx === null)

而引用数据类型除了function返回的是'function',其余都是'object'

instanceof

判断这个对象是否是之前那个构造函数生成的对象,arr instanceof Array === true

instanceof可以准确地判断复杂引用数据类型,但是不能正确判断基础数据类型

Object.prototype.toString

对于 Object 对象,直接调用 toString() 就能返回 [object Object];而对于其他对象,则需要通过 call 来调用,才能返回正确的类型信息

Object.prototype.toString({})       // "[object Object]"
Object.prototype.toString.call({})  // 同上结果,加上call也ok
Object.prototype.toString.call(1)    // "[object Number]"
Object.prototype.toString.call('1')  // "[object String]"
Object.prototype.toString.call(true)  // "[object Boolean]"
Object.prototype.toString.call(function(){})  // "[object Function]"
Object.prototype.toString.call(null)   //"[object Null]"
Object.prototype.toString.call(undefined) //"[object Undefined]"
Object.prototype.toString.call(/123/g)    //"[object RegExp]"
Object.prototype.toString.call(new Date()) //"[object Date]"
Object.prototype.toString.call([])       //"[object Array]"
Object.prototype.toString.call(document)  //"[object HTMLDocument]"
Object.prototype.toString.call(window)   //"[object Window]"

封装类型检测方法

function getType(obj){
    const type = typeof obj
    if(type !== 'object'){
        return type
    }
    let referenceType = Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1')
    //typeof返回的是小写,Object.prototype.toString首字母返回的是大写,统一格式
    return referenceType.slice(0,1).toLowerCase() + referenceType.slice(1)
}

getType([])     // "array" typeof []是object,因此toString返回
getType('123')  // "string" typeof 直接返回
getType(window) // "window" toString返回
getType(null)   // "null" typeof null是object,需toString来判断
getType(undefined)   // "undefined" typeof 直接返回
getType()            // "undefined" typeof 直接返回
getType(function(){}) // "function" typeof能判断 直接返回
getType(/123/g)      //"regExp" toString返回

数据类型转换

强制类型转换

Number()

  • 如果是布尔值,true 和 false 分别被转换为 1 和 0;
  • 如果是数字,返回自身;
  • 如果是 null,返回 0;
  • 如果是 undefined,返回 NaN;
  • 如果是字符串,遵循以下规则:如果字符串中只包含数字(或者是 0X / 0x 开头的十六进制数字字符串,允许包含正负号),则将其转换为十进制;如果字符串中包含有效的浮点格式,将其转换为浮点数值;如果是空字符串,将其转换为 0;如果不是以上格式的字符串,均返回 NaN;
  • 如果是 Symbol,抛出错误;
Number('123.456');   // 123.456
Number('123');       // 123
Number('0111');      //111
Number(null);        //0
Number('');          //0
Number('1a');        //NaN
Number(-0X11);       //-17
Number('0X11')       //17
Number(true);        //1
Number(false)        //0
Number(undefned)     //NaN

parseInt()

  • 数字:截断小数,返回小数点前的数值。没有小数点不转换还是为自身。
  • null:转为NaN。
  • undefined:转为NaN。
  • 字符串:会忽略前面的0和空格直到找到第一个数字然后一直找到非数字字符为止。
  • 字符串为数字:转为对应的数值。
  • 字符串中有一个小数点:截断小数,返回小数点前的数值转为数值形。
  • 字符串中有0x:转为十六进制对应的十进制数值。
  • 字符串为空:转为NaN。
  • 字符串为非空非数字非0x:转为NaN。
  • 字符串中有科学计数法e:不支持科学计数法,返回e之前的数值片段。
parseInt('123.456');   // 123
parseInt('123');       // 123
parseInt('0111');      //111
parseInt(null);        //NaN
parseInt('');          //NaN
parseInt('1a');        //NaN
parseInt(-0X11);       //-17
parseInt('0X11')       //17
parseInt(true);        //NaN
parseInt(false)        //NaN
parseInt(undefned)     //NaN

parseFloat()

与parseInt()的本质区别在于字符串中有小数点,转为对应的浮点数值,如果有第二个小数点,则取第二个小数点之前的片段。

Boolean()

这个方法的规则是:除了 undefined、 null、 false、 ''、 0(包括 +0,-0)、 NaN 转换出来是 false,其他都是 true。

toString()

toString()可以将所有的的数据都转换为[字符串],但是要排除null 和 undefined。 false.toString()

String()

String()可以将null和undefined转换为字符串,但是没法转进制字符串 String(null)

隐形类型转换

凡是通过逻辑运算符 (&&、 ||、 !)、运算符 (+、-、*、/)、关系操作符 (>、 <、 <= 、>=)、相等运算符 (==) 或者 if/while 条件的操作,如果遇到两个数据类型不一样的情况,都会出现隐式类型转换。

'==' 的隐式类型转换规则

  • 如果类型相同,无须进行类型转换;
  • 如果其中一个操作值是 null 或者 undefined,那么另一个操作符必须为 null 或者 undefined,才会返回 true,否则都返回 false;
  • 如果其中一个是 Symbol 类型,那么返回 false;
  • 两个操作值如果为 string 和 number 类型,那么就会将字符串转换为 number;
  • 如果一个操作值是 boolean,那么转换成 number;
  • 如果一个操作值为 object 且另一方为 string、number 或者 symbol,就会把 object 转为原始类型再进行判断(调用 object 的 valueOf/toString 方法进行转换)。

'+' 的隐式类型转换规则

  • 如果其中有一个是字符串,另外一个是 undefined、null 或布尔型,则调用 toString() 方法进行字符串拼接;如果是纯对象、数组、正则等,则默认调用对象的转换方法会存在优先级,然后再进行拼接。
  • 如果其中有一个是数字,另外一个是 undefined、null、布尔型或数字,则会将其转换成数字进行加法运算,对象的情况还是参考上一条规则。
  • 如果其中一个是字符串、一个是数字,则按照字符串规则进行拼接

深浅拷贝

浅拷贝

如果对象属性是基本的数据类型,复制的就是基本类型的值给新对象;但如果属性是引用数据类型,复制的就是内存中的地址,如果其中一个对象改变了,肯定会影响到另一个对象。

直接赋值的形式修改第一层就会影响另一个对象,用于基本数据类型

let source = { a: 1 };
let target = source;
console.log(target); // { a: 1 }; 
target.a = 10; 
console.log(source); // { a: 10 }; 
console.log(target); // {a: 10};

浅拷贝修改第二层属性才会影响另一个对象,引用数据类型浅拷贝的几个方式:

  1. Object.assign(target,source)该方法可以用于 JS 对象的合并
let target = {};
let source = { a: { b: 2 } };
Object.assign(target, source);
console.log(target); // { a: { b: 10 } }; 
source.a.b = 10; 
console.log(source); // { a: { b: 10 } }; 
console.log(target); // { a: { b: 10 } };
  1. 扩展运算符(ES6)可用于对象和数组
/* 对象的拷贝 */
let obj = {a:1,b:{c:1}}
let obj2 = {...obj}
obj.a = 2
console.log(obj)  //{a:2,b:{c:1}} console.log(obj2); //{a:1,b:{c:1}}
obj.b.c = 2
console.log(obj)  //{a:2,b:{c:2}} console.log(obj2); //{a:1,b:{c:2}}
/* 数组的拷贝 */
let arr = [1, 2, 3];
let newArr = [...arr]; //跟arr.slice()是一样的效果
  1. concat 拷贝数组
let arr = [1, 2, 3];
let newArr = arr.concat();
newArr[1] = 100;   // 修改第一层数据不会相互影响
console.log(arr);  // [ 1, 2, 3 ]
console.log(newArr); // [ 1, 100, 3 ]
  1. slice 拷贝数组
let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
newArr[2].val = 1000; // 修改第二层会相互影响
console.log(arr);  //[ 1, 2, { val: 1000 } ]

深拷贝

浅拷贝只是创建了一个新的对象,复制了原有对象的基本类型的值,而引用数据类型只拷贝了一层属性,再深层的还是无法进行拷贝。深拷贝则不同,对于复杂引用数据类型,其在堆内存中完全开辟了一块内存地址,并将原有的对象完全复制过来存放。这两个对象是相互独立、不受影响的,彻底实现了内存上的分离。

深拷贝的方式:

  1. JSON.stringify JSON.stringify() 是目前开发过程中最简单的深拷贝方法,其实就是把一个对象序列化成为 JSON 的字符串,并将对象里面的内容转换成字符串,最后再用 JSON.parse() 的方法将JSON 字符串生成一个新的对象
let obj1 = { a:1, b:[1,2,3] }
let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
console.log(obj2);   //{a:1,b:[1,2,3]} 
obj1.a = 2;
obj1.b.push(4);
console.log(obj1);   //{a:2,b:[1,2,3,4]}
console.log(obj2);   //{a:1,b:[1,2,3]}

弊端

  • 拷贝的对象的值中如果有函数、undefined、symbol 这几种类型,经过 JSON.stringify 序列化之后的字符串中这个键值对会消失;
  • 拷贝 Date 引用类型会变成字符串;
  • 无法拷贝不可枚举的属性;
  • 无法拷贝对象的原型链;
  • 拷贝 RegExp 引用类型会变成空对象;
  • 对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null;
  • 无法拷贝对象的循环引用,即对象成环 (obj[key] = obj)。
  1. 封装函数,解决上述bug
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)
const deepClone = function (obj, hash = new WeakMap()) {
  if (obj.constructor === Date) 
  return new Date(obj)       // 日期对象直接返回一个新的日期对象
  if (obj.constructor === RegExp)
  return new RegExp(obj)     //正则对象直接返回一个新的正则对象
  //如果循环引用了就用 weakMap 来解决
  if (hash.has(obj)) return hash.get(obj)
  let allDesc = Object.getOwnPropertyDescriptors(obj)
  //遍历传入参数所有键的特性
  let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)
  //继承原型链
  hash.set(obj, cloneObj)
  for (let key of Reflect.ownKeys(obj)) { 
    cloneObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key], hash) : obj[key]
  }
  return cloneObj
}
// 下面是验证代码
let obj = {
  num: 0,
  str: '',
  boolean: true,
  unf: undefined,
  nul: null,
  obj: { name: '我是一个对象', id: 1 },
  arr: [0, 1, 2],
  func: function () { console.log('我是一个函数') },
  date: new Date(0),
  reg: new RegExp('/我是一个正则/ig'),
  [Symbol('1')]: 1,
};
Object.defineProperty(obj, 'innumerable', {
  enumerable: false, value: '不可枚举属性' }
);
obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))
obj.loop = obj    // 设置loop成循环引用的属性
let cloneObj = deepClone(obj)
cloneObj.arr.push(4)
console.log('obj', obj)
console.log('cloneObj', cloneObj)
  1. lodash函数库实现深拷贝
let result = _.cloneDeep(test)

继承

实现继承的几种方式

  1. 原型链继承

    每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针 Child1.prototype = new Parent1()

    弊端是原型属性共享问题

  2. 构造函数继承(借助 call)

    弊端是父类原型对象中一旦存在父类之前自己定义的方法,那么子类将无法继承这些方法

  3. 组合继承(前两种组合)

    弊端是每实例化一个子类就会构造一次父类,消耗性能

  4. 基于Object.create 方法的原型继承

    Object.create(参数1:用作新对象原型的对象,参数2:为新对象定义额外属性的对象(可选参数)),可以得到一个新对象。

    通过 Object.create() 这个方法可以实现普通对象的继承,不仅仅能继承属性,同样也可以继承方法,但是是浅拷贝,引用数据类型是“共享”的。

  5. 寄生式继承

    和原型式继承一样,区别在于在继承过程中可以给父类添加更多的方法

  6. 寄生组合式继承(最优的继承方案)

    这种方式较好地实现了继承想要的结果,同时也减少了构造次数,减少了性能的开销

      function clone (parent, child) {
         // 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
         child.prototype = Object.create(parent.prototype);
         child.prototype.constructor = child;
       }
    
       function Parent6() {
         this.name = 'parent6';
         this.play = [1, 2, 3];
       }
        Parent6.prototype.getName = function () {
         return this.name;
       }
       function Child6() {
         Parent6.call(this);
         this.friends = 'child5';
       }
    
       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());
    

    es6 的 extends 关键字

extends就是个语法糖,底层采用的也是寄生组合继承方式

class Person {
   constructor(name) {
     this.name = name
   }
   // 原型方法
   // 即 Person.prototype.getName = function() { }
   // 下面可以简写为 getName() {...}
   getName = function () {
     console.log('Person:', this.name)
   }
 }
 class Gamer extends Person {
   constructor(name, age) {
     // 子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
     super(name)
     this.age = age
   }
 }
 const asuna = new Gamer('Asuna', 20)
 asuna.getName() // 成功访问到父类的方法

原型链

在js中 构造函数 默认带 有prototype 属性,这个属性值是 构造函数的原型对象 ,这个原型对象带有一个constructor属性指向构造函数,每一个 对象 都有一个 proto 属性指向 构造函数的prototype ,当读取实例的属性时,如果找不到就会通过__proto__去原型对象上找一直找到最顶层Object.prototype,Object.prototype的原型指向null。

image.png

蓝色的线串联起来的就是原型链,所以挂载到原型是可以实现共享的。

new 关键字

主要作用就是执行一个构造函数、返回一个实例对象;如果构造函数return了一个对象,那么new返回的是return的对象

new 在这个生成实例的过程中发生了什么?

  1. 创建一个新对象
  2. 将构造函数中的作用域赋给新对象(this指向新对象)
  3. 执行构造函数中的代码(为新对象添加属性)
  4. 返回新对象

this指向

  1. this的指向不是在编写时确定的,⽽是在执⾏时确定的,默认情况下指向window;
  2. 如果函数被调⽤的位置存在上下⽂对象时,那么函数是被隐式绑定的,this指向上下文对象
  3. new 调⽤⼀个构造函数,this指向这个新对象。
  4. 通过 apply 、call 、 bind显示改变this

this绑定优先级: new绑定 > 显式绑定 >隐式绑定 >默认绑定

箭头函数的this是捕获该箭头函数所在上下⽂的 this ,作为自己的this值。

var obj = {
  birth: 1995,
  getAge: function() {
    var b = this.birth; // 1995;
    var fn = function() {
      return this.birth; 
      // this 指向被改变了!
      // 因为这里重新定义了个 function,
      // 假设它内部有属于自己的 this1,
      // 然后 getAge 的 this 为 this2,
      // 那么,fn 当然奉行就近原则,使用自己的 this,即:this1
    };
    return fn();
  }
}

obj.getAge(); // undefined

-----------------------------------

var obj = {
  birth: 1995,
  getAge: function() {
    var b = this.birth; // 1995
    var fn = () => this.birth;  //this指向getAge的上下文也就是obj
    return fn();
  }
}
obj.getAge(); // 1995

apply & call & bind

call、apply 和 bind 是挂在 Function 对象上的三个方法,调用这三个方法的必须是一个函数。

func.call(thisArg, param1, param2, ...)
func.apply(thisArg, [param1,param2,...])
func.bind(thisArg, param1, param2, ...)

其中 func 是要调用的函数,thisArg 一般为 this 所指向的对象,后面的 param1、2 为函数 func 的多个参数,如果 func 不需要参数,则后面的 param1、2 可以不写。

三个方法的区别: 这三个方法共有的、比较明显的作用就是,都可以改变函数 func 的 this 指向。call 和 apply 的区别在于,传参的写法不同:apply 的第 2 个参数为数组; call 则是从第 2 个至第 N 个都是给 func 的传参;而 bind 和这两个(call、apply)又不同,bind 虽然改变了 func 的 this 指向,但不是马上执行,而这两个(call、apply)是在改变了函数的 this 指向之后立马执行。

如果使用bind()改变了this指向后会返回一个新函数,要想立刻执行需要变成立即执行函数func.bind(thisArg, param1, param2, ...)()

应用场景: 会通过“借用”的方式去复用已有的方法,来节约内存、优化代码。

function getType(obj){
  let type  = typeof obj;
  if (type !== "object") {
    return type;
  }
  // obj 借用了 Object原型链上的toString()方法,最后返回用来判断传入的 obj 的字符串
  return Object.prototype.toString.call(obj).replace(/^$/, '$1');
}
//数组借用了Math的max和min方法
let arr = [13, 6, 10, 11, 16];
const max = Math.max.apply(Math, arr); 
const min = Math.min.apply(Math, arr);
console.log(max);  // 16
console.log(min);  // 6

闭包

作用域

  1. 函数作用域(函数内部定义变量)

    是局部变量,只能在函数内部访问,当这个函数被执行完之后,这个局部变量也相应会被销毁。

  2. 全局作用域 (函数外部定义的变量,是挂载在 window 对象下的变量)

  3. 块级作用域({}内部,es6才有的作用域)

    使用let关键字定义在if 语句及 for语句后面 {...} 这里面所包括的,就是块级作用域

    console.log(a) //a is not defined
     if(true){
       let a = '123'console.log(a); // 123
     }
     console.log(a) //a is not defined
    

作用域链

当访问一个变量时,代码解释器会首先在当前的作用域查找,如果没找到,就去父级作用域去查找,直到找到该变量或者不存在父级作用域中,这样的链路就是作用域链

什么是闭包?

闭包是指有权访问另外一个函数作用域中的变量的函数。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。(变量保存在内存中,不被销毁)

通俗的说闭包是一个定义在函数内部的函数。

闭包产生的本质就是:当前环境中存在指向父级作用域的引用

在使用闭包的时候,你要尽量注意一个原则:如果该闭包会一直使用,那么它可以作为全局变量而存在(会一直存在内存中不被销毁造成内存泄漏),但如果使用频率不高,而且占用内存又比较大的话,那就尽量让他成为一个局部变量(不用的时候,垃圾回收会收回这块内存)。

闭包的表现形式

  1. 返回一个函数
function fun1() {
 var a = 2
 function fun2() {
   console.log(a);  //2
 }
 return fun2;
}
var result = fun1();
result();
  1. 在定时器、事件监听、Ajax 请求、Web Workers 或者任何异步中,只要使用了回调函数,实际上就是在使用闭包
// 定时器
setTimeout(function handler(){
 console.log('1');
},1000);
// 事件监听
$('#app').click(function(){
 console.log('Event Listener');
});
  1. 立即执行函数
for (var i = 0; i < 5; i++) { console.log(i);}console.log(i); //5

在es5中是没有块级作用域的,所以在 for 循环中声明的 i 变量实际上是一个全局变量,可以在全局作用域中访问到。

块级作用域,也可以称为私有作用域。也就是说只在for循环的语句块中有定义,一旦循环结束,变量 i 就会被销毁。而在ES5中,我们主要通过匿名函数的方式来达到块级作用域的效果。

var a = 2;
(function IIFE(){
 console.log(a);  // 输出2
})();

如何解决循环输出问题?

// 输出 5个6
for(var i = 1; i <= 5; i ++){
 setTimeout(function() {
   console.log(i)
 }, 0)
}

为什么上述代码输出的是5个6呢?

  1. setTimeout为宏任务,由于 JS 中单线程 eventLoop 机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后 setTimeout 中的回调才依次执行。
  2. 因为 setTimeout 函数也是一种闭包,往上找它的父级作用域链就是 window,变量 i 为 window 上的全局变量,开始执行 setTimeout 之前变量 i 已经就是 6 了,因此最后输出的连续就都是 6。

如果按顺序输出1、2、3、4、5,可以使用let声明变量,利用IIFE(立即执行函数),给setTimeout传入第三个参数

垃圾回收机制

JavaScript 的内存管理

不管是什么样的计算机程序语言,运行在对应的代码引擎上,对应的使用内存过程大致逻辑是一样的,可以分为这三个步骤:

  1. 分配你所需要的系统内存空间;
  2. 使用分配到的内存进行读或者写等操作;
  3. 不需要使用内存时,将其空间释放或者归还。

不同于其他语言的是,在 JavaScript 中,当我们创建变量的时候,系统会自动给对象分配对应的内存。

  • 基本数据类型在内存中会占据固定的内存空间,它们的值都保存在栈空间中,直接可以通过值来访问这些;

  • 由于引用类型值大小不固定(比如对象可以添加属性等),栈内存中存放地址指向堆内存中的对象,是通过引用来访问的。

栈内存中的基本类型,可以通过操作系统直接处理;而堆内存中的引用类型,正是由于可以经常变化,大小不固定,因此需要 JavaScript 的引擎通过垃圾回收机制来处理

Chrome 内存回收机制

  1. 新生代内存回收,JavaScript 的 V8 引擎会将正在使用内存空间的对象检查一遍,如果不是存活的对象,则直接进行系统回收。但是如果堆内存不是连续分配而是零散的分配情况就造成了内存碎片,使用Scavenge算法排列。
  2. 老生代内存回收,新生代中的变量如果经过回收之后依然一直存在,那么就会被放入到老生代内存中采用了 Mark-Sweep(标记清除) 和 Mark-Compact(标记整理)的策略。首先它会遍历堆上的所有的对象,分别对它们打上标记;然后在代码执行过程结束之后,对使用过的变量取消标记。把还有标记的进行整体清除,从而释放内存空间。标记清除之后可能存在内存碎片的情况需要标记整理策略(Mark-Compact)

内存泄漏与优化

内存泄漏的场景:

  1. 过多的缓存未释放;
  2. 闭包太多未释放;
  3. 定时器或者回调太多未释放;
  4. 太多无效的 DOM 未释放;
  5. 全局变量太多未被发现。

优化

  1. 减少不必要的全局变量,使用严格模式避免意外创建全局变量。
  2. 在你使用完数据后,及时解除引用(闭包中的变量,DOM 引用,定时器清除)。
  3. 组织好你的代码逻辑,避免死循环等造成浏览器卡顿、崩溃的问题。

数组

Array构造器

// 使用 Array 构造器,可以自定义长度
var a = Array(6); // [empty × 6]
var c = Array(1,2,3,4,5,6); // [1,2,3,4,5,6]
var d = Array(0); // []
// 使用对象字面量
var b = [];
b.length = 6; // [undefined × 6]

ES6新增的构造方法

Array.of

用于将参数依次转化为数组中的一项,然后返回这个新数组

Array.of(8.0); // [8]
Array(8.0); // [empty × 8]
Array.of(8.0, 5); // [8, 5]
Array(8.0, 5); // [8, 5]
Array.of('8'); // ["8"]
Array('8'); // ["8"]

Array.from

从一个类似数组的可迭代对象中创建一个新的数组实例,拥有迭代器的对象包括String、Set、Map 等,Array.from 统统可以处理。

//类似数组的对象
Array.from({0: 'a', 1: 'b', 2:'c',length:3}); //['a', 'b', 'c']
// String
Array.from('abc');         // ["a", "b", "c"]
// Set
Array.from(new Set(['abc', 'def'])); // ["abc", "def"]
// Map
Array.from(new Map([[1, 'ab'], [2, 'de']])); 
// [[1, 'ab'], [2, 'de']]

除了Array.from方法外,还可以使用展开运算符将类数组转换成数组

function sum(a, b) {
  let args = [...arguments];
  console.log(args.reduce((sum, cur) => sum + cur));
}
sum(1, 2);    // 3

不使用上述es6的两个方法的话,还可以用[].slice.call(arguments)

function sum(a, b) {
  let args = [].slice.call(arguments);
 // let args = Array.prototype.slice.call(arguments); // 这样写也是一样效果
  console.log(args.reduce((sum, cur) => sum + cur));
}
sum(1, 2);  // 3

判断数组

  1. es6新增 Array.isArray(arr)
  2. Object.prototype.toString.call(arg) === '[object Array]';

改变自身的方法

一共有 9 个,分别为 pop、push、reverse、shift、sort、splice、unshift,以及两个 ES6 新增的方法 copyWithin 和 fill

// pop方法
var array = ["cat", "dog", "cow", "chicken", "mouse"];
var item = array.pop();
console.log(array); // ["cat", "dog", "cow", "chicken"]
console.log(item); // mouse
// push方法
var array = ["football", "basketball",  "badminton"];
var i = array.push("golfball");
console.log(array); 
// ["football", "basketball", "badminton", "golfball"]
console.log(i); // 4
// reverse方法
var array = [1,2,3,4,5];
var array2 = array.reverse();
console.log(array); // [5,4,3,2,1]
console.log(array2===array); // true
// shift方法
var array = [1,2,3,4,5];
var item = array.shift();
console.log(array); // [2,3,4,5]
console.log(item); // 1
// unshift方法
var array = ["red", "green", "blue"];
var length = array.unshift("yellow");
console.log(array); // ["yellow", "red", "green", "blue"]
console.log(length); // 4
// sort方法
var array = ["apple","Boy","Cat","dog"];
var array2 = array.sort();
console.log(array); // ["Boy", "Cat", "apple", "dog"]
console.log(array2 == array); // true
// splice方法
var array = ["apple","boy"];
var splices = array.splice(1,1);
console.log(array); // ["apple"]
console.log(splices); // ["boy"]
// copyWithin方法
var array = [1,2,3,4,5]; 
var array2 = array.copyWithin(0,3);
console.log(array===array2,array2);  // true [4, 5, 3, 4, 5]
// fill方法
var array = [1,2,3,4,5];
var array2 = array.fill(10,0,3);
console.log(array===array2,array2); 
// true [10, 10, 10, 4, 5], 可见数组区间[0,3]的元素全部替换为10

不会改变自身的方法

不会改变自身的方法也有 9 个,分别为 concat、join、slice、toString、toLocaleString、indexOf、lastIndexOf、未形成标准的 toSource,以及 ES7 新增的方法 includes

// concat方法
var array = [1, 2, 3];
var array2 = array.concat(4,[5,6],[7,8,9]);
console.log(array2); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log(array); // [1, 2, 3], 可见原数组并未被修改
// join方法
var array = ['We', 'are', 'Chinese'];
console.log(array.join()); // "We,are,Chinese"
console.log(array.join('+')); // "We+are+Chinese"
// slice方法
var array = ["one", "two", "three","four", "five"];
console.log(array.slice()); // ["one", "two", "three","four", "five"]
console.log(array.slice(2,3)); // ["three"]
// toString方法
var array = ['Jan', 'Feb', 'Mar', 'Apr'];
var str = array.toString();
console.log(str); // Jan,Feb,Mar,Apr
// tolocalString方法
var array= [{name:'zz'}, 123, "abc", new Date()];
var str = array.toLocaleString();
console.log(str); // [object Object],123,abc,2016/1/5 下午1:06:23
// indexOf方法
var array = ['abc', 'def', 'ghi','123'];
console.log(array.indexOf('def')); // 1
// includes方法
var array = [-0, 1, 2];
console.log(array.includes(+0)); // true
console.log(array.includes(1)); // true
var array = [NaN];
console.log(array.includes(NaN)); // true

数组遍历的方法

基于 ES6,不会改变自身的遍历方法一共有 12 个,分别为 forEach、every、some、filter、map、reduce、reduceRight,以及 ES6 新增的方法 entries、find、findIndex、keys、values

// forEach方法
var array = [1, 3, 5];
var obj = {name:'cc'};
var sReturn = array.forEach(function(value, index, array){
  array[index] = value;
  console.log(this.name); // cc被打印了三次, this指向obj
},obj);
console.log(array); // [1, 3, 5]
console.log(sReturn); // undefined, 可见返回值为undefined
// every方法
var o = {0:10, 1:8, 2:25, length:3};
var bool = Array.prototype.every.call(o,function(value, index, obj){
  return value >= 8;
},o);
console.log(bool); // true
// some方法
var array = [18, 9, 10, 35, 80];
var isExist = array.some(function(value, index, array){
  return value > 20;
});
console.log(isExist); // true 
// map 方法
var array = [18, 9, 10, 35, 80];
array.map(item => item + 1);
console.log(array);  // [19, 10, 11, 36, 81]
// filter 方法
var array = [18, 9, 10, 35, 80];
var array2 = array.filter(function(value, index, array){
  return value > 20;
});
console.log(array2); // [35, 80]
// reduce方法
var array = [1, 2, 3, 4];
var s = array.reduce(function(previousValue, value, index, array){
  return previousValue * value;
},1);
console.log(s); // 24
// ES6写法更加简洁
array.reduce((p, v) => p * v); // 24
// reduceRight方法 (和reduce的区别就是从后往前累计)
var array = [1, 2, 3, 4];
array.reduceRight((p, v) => p * v); // 24
// entries方法
var array = ["a", "b", "c"];
var iterator = array.entries();
console.log(iterator.next().value); // [0, "a"]
console.log(iterator.next().value); // [1, "b"]
console.log(iterator.next().value); // [2, "c"]
console.log(iterator.next().value); // undefined, 迭代器处于数组末尾时, 再迭代就会返回undefined
// find & findIndex方法
var array = [1, 3, 5, 7, 8, 9, 10];
function f(value, index, array){
  return value%2==0;     // 返回偶数
}
function f2(value, index, array){
  return value > 20;     // 返回大于20的数
}
console.log(array.find(f)); // 8
console.log(array.find(f2)); // undefined
console.log(array.findIndex(f)); // 4
console.log(array.findIndex(f2)); // -1
// keys方法
[...Array(10).keys()];     // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[...new Array(10).keys()]; // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
// values方法
var array = ["abc", "xyz"];
var iterator = array.values();
console.log(iterator.next().value);//abc
console.log(iterator.next().value);//xyz

数组扁平化

普通递归实现

// 方法1
var a = [1, [2, [3, 4, 5]]];
function flatten(arr) {
  let result = [];

  for(let i = 0; i < arr.length; i++) {
    if(Array.isArray(arr[i])) {
      result = result.concat(flatten(arr[i]));
    } else {
      result.push(arr[i]);
    }
  }
  return result;
}
flatten(a);  //  [1, 2, 3, 4,5]

利用reduce函数迭代

// 方法2
var arr = [1, [2, [3, 4]]];
function flatten(arr) {
    return arr.reduce(function(prev, next){
        return prev.concat(Array.isArray(next) ? flatten(next) : next)
    }, [])
}
console.log(flatten(arr));//  [1, 2, 3, 4,5]

扩展运算符和 some 实现

// 方法3
var arr = [1, [2, [3, 4]]];
function flatten(arr) {
    while (arr.some(item => Array.isArray(item))) {
        arr = [].concat(...arr);
    }
    return arr;
}
console.log(flatten(arr)); //  [1, 2, 3, 4,5]

split 和 toString 共同处理

由于数组会默认带一个 toString 的方法,所以可以把数组直接转换成逗号分隔的字符串,然后再用 split 方法把字符串重新转换为数组。

// 方法4
var arr = [1, [2, [3, 4]]];
function flatten(arr) {
    return arr.toString().split(',');
}
console.log(flatten(arr)); //  [1, 2, 3, 4]

调用 ES6 中的 flat

// 方法5
var arr = [1, [2, [3, 4]]];
function flatten(arr) {
  //Infinity代表不论多少层都要展开
  return arr.flat(Infinity);
}
console.log(flatten(arr)); //  [1, 2, 3, 4,5]

正则和 JSON 方法共同处理

将 JSON.stringify 的方法先转换为字符串,然后通过正则表达式过滤掉字符串中的数组的方括号,最后再利用 JSON.parse 把它转换成数组。

// 方法 6
let arr = [1, [2, [3, [4, 5]]], 6];
function flatten(arr) {
  let str = JSON.stringify(arr);
  str = str.replace(/(\[|\])/g, '');
  str = '[' + str + ']';
  return JSON.parse(str); 
}
console.log(flatten(arr)); //  [1, 2, 3, 4,5]

数组去重

双层循环(时间复杂度O(n^2))

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    let res = [arr[0]]
    for (let i = 1; i < arr.length; i++) {
        let flag = true
        for (let j = 0; j < res.length; j++) {
            if (arr[i] === res[j]) {
                flag = false;
                break
            }
        }
        if (flag) {
            res.push(arr[i])
        }
    }
    return res
}

indexOf方法去重

创建新数组,遍历老数组,新数组查找元素的位置,-1则push进新数组

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    let res = []
    for (let i = 0; i < arr.length; i++) {
        if (res.indexOf(arr[i]) === -1) {
            res.push(arr[i])
        }
    }
    return res
}

indexOf + filter 方法去重

利用indexOf检测元素在数组中第一次出现的位置是否和元素现在的位置相等,如果不等则说明该元素是重复元素。

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    return Array.prototype.filter.call(arr, function(item, index){
        return arr.indexOf(item) === index;
    });
}

相邻元素去重

首先调用了数组的排序方法sort(),然后根据排序后的结果进行遍历及相邻元素比对,如果相等则跳过改元素,直到遍历结束。

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    arr = arr.sort()
    let res = []
    for (let i = 0; i < arr.length; i++) {
        if (arr[i] !== arr[i-1]) {
            res.push(arr[i])
        }
    }
    return res
}

es6的set与解构赋值去重

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    return [...new Set(arr)]
}

es6的 Array.from与set去重

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    return Array.from(new Set(arr))
}

数组排序

冒泡排序

两次循环,当前元素依次和前面所有元素比较,如果比当前元素大,则交换位置

var a = [1, 3, 6, 3, 23, 76, 1, 34, 222, 6, 456, 221];
function bubbleSort(array) {
  const len = array.length
  if (len < 2) return array
  for (let i = 0; i < len; i++) {
    for (let j = 0; j < i; j++) {
      if (array[j] > array[i]) {
        const temp = array[j]
        array[j] = array[i]
        array[i] = temp
      }
    }
  }
  return array
}
bubbleSort(a);  // [1, 1, 3, 3, 6, 6, 23, 34, 76, 221, 222, 456]

快速排序

最主要的思路是从数列中挑出一个元素,称为 “基准”(pivot);然后重新排序数列,所有元素比基准值小的摆放在基准前面、比基准值大的摆在基准的后面;在这个区分搞定之后,该基准就处于数列的中间位置;然后把小于基准值元素的子数列(left)和大于基准值元素的子数列(right)递归地调用 quick 方法排序完成,这就是快排的思路

var a = [1, 3, 6, 3, 23, 76, 1, 34, 222, 6, 456, 221];
function quickSort(array) {
  var quick = function(arr) {
    if (arr.length <= 1) return arr
    const len = arr.length
    const index = Math.floor(len >> 1)
    const pivot = arr.splice(index, 1)[0]
    const left = []
    const right = []
    for (let i = 0; i < len; i++) {
      if (arr[i] > pivot) {
        right.push(arr[i])
      } else if (arr[i] <= pivot) {
        left.push(arr[i])
      }
    }
    return quick(left).concat([pivot], quick(right))
  }
  const result = quick(array)
  return result
}
quickSort(a);//  [1, 1, 3, 3, 6, 6, 23, 34, 76, 221, 222, 456]

插入排序

首先循环遍历从 i 等于 1 开始,拿到当前的 current 的值,从后向前扫描,去和前面的值比较,如果前面的大于当前的值,就把前面的值和当前的那个值进行交换

var a = [1, 3, 6, 3, 23, 76, 1, 34, 222, 6, 456, 221];
function insertSort(array) {
  const len = array.length
  let current
  let prev
  for (let i = 1; i < len; i++) {
    current = array[i]
    prev = i - 1
    while (prev >= 0 && array[prev] > current) {
      array[prev + 1] = array[prev]
      prev--
    }
    array[prev + 1] = current
  }
  return array
}
insertSort(a); // [1, 1, 3, 3, 6, 6, 23, 34, 76, 221, 222, 456]

选择排序

首先将最小的元素存放在序列的起始位置,再从剩余未排序元素中继续寻找最小元素,然后放到已排序的序列后面……以此类推,直到所有元素均排序完毕

var a = [1, 3, 6, 3, 23, 76, 1, 34, 222, 6, 456, 221];
function selectSort(array) {
  const len = array.length
  let temp
  let minIndex
  for (let i = 0; i < len - 1; i++) {
    minIndex = i
    for (let j = i + 1; j < len; j++) {
      if (array[j] <= array[minIndex]) {
        minIndex = j
      }
    }
    temp = array[i]
    array[i] = array[minIndex]
    array[minIndex] = temp
  }
  return array
}
selectSort(a); // [1, 1, 3, 3, 6, 6, 23, 34, 76, 221, 222, 456]

堆排序

堆积是一个近似完全二叉树的结构,并同时满足堆积的性质,即子结点的键值或索引总是小于(或者大于)它的父节点。堆的底层实际上就是一棵完全二叉树,可以用数组实现

var a = [1, 3, 6, 3, 23, 76, 1, 34, 222, 6, 456, 221];
function heap_sort(arr) {
  var len = arr.length
  var k = 0
  function swap(i, j) {
    var temp = arr[i]
    arr[i] = arr[j]
    arr[j] = temp
  }
  function max_heapify(start, end) {
    var dad = start
    var son = dad * 2 + 1
    if (son >= end) return
    if (son + 1 < end && arr[son] < arr[son + 1]) {
      son++
    }
    if (arr[dad] <= arr[son]) {
      swap(dad, son)
      max_heapify(son, end)
    }
  }
  for (var i = Math.floor(len / 2) - 1; i >= 0; i--) {
    max_heapify(i, len)
  }
 
  for (var j = len - 1; j > k; j--) {
    swap(0, j)
    max_heapify(0, j)
  }
 
  return arr
}
heap_sort(a); // [1, 1, 3, 3, 6, 6, 23, 34, 76, 221, 222, 456]

归并排序

将已有序的子序列合并,得到完全有序的序列;先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

var a = [1, 3, 6, 3, 23, 76, 1, 34, 222, 6, 456, 221];
function mergeSort(array) {
  const merge = (right, left) => {
    const result = []
    let il = 0
    let ir = 0
    while (il < left.length && ir < right.length) {
      if (left[il] < right[ir]) {
        result.push(left[il++])
      } else {
        result.push(right[ir++])
      }
    }
    while (il < left.length) {
      result.push(left[il++])
    }
    while (ir < right.length) {
      result.push(right[ir++])
    }
    return result
  }
  const mergeSort = array => {
    if (array.length === 1) { return array }
    const mid = Math.floor(array.length / 2)
    const left = array.slice(0, mid)
    const right = array.slice(mid, array.length)
    return merge(mergeSort(left), mergeSort(right))
  }
  return mergeSort(array)
}
mergeSort(a); // [1, 1, 3, 3, 6, 6, 23, 34, 76, 221, 222, 456]

sort 方法的底层实现

  1. 当 n<=10 时,采用插入排序;
  2. 当 n>10 时,采用三路快速排序;
  3. 10<n <=1000,采用中位数作为哨兵元素;
  4. n>1000,每隔 200~215 个元素挑出一个元素,放到一个新数组中,然后对它排序,找到中间位置的数,以此作为中位数。 如果当 n 足够小的时候,最好的情况下,插入排序的时间复杂度为 O(n) 要优于快速排序的 O(nlogn)

异步编程

JavaScript 是单线程的,如果 JS 都是同步代码会造成阻塞,;而如果使用异步则不会阻塞,我们不需要等待异步代码执行的返回结果,可以继续执行该异步任务之后的代码逻辑。

回调函数

一旦层级变多就会陷入回调地狱

promise

用链式调用一定程度上解决回调地狱,如果操作过多,可读性虽然有所提升,但是依旧很难维护。

function read(url) {
    return new Promise((resolve, reject) => {
        fs.readFile(url, 'utf8', (err, data) => {
            if(err) reject(err);
            resolve(data);
        });
    });
}
read(A).then(data => {
    return read(B);
}).then(data => {
    return read(C);
}).then(data => {
    return read(D);
}).catch(reason => {
    console.log(reason);
});

一般 Promise 在执行过程中,必然会处于以下几种状态之一。

  1. 待定(pending):初始状态,既没有被完成,也没有被拒绝。
  2. 已完成(fulfilled):操作成功完成。
  3. 已拒绝(rejected):操作失败。

promise 如何解决回调地狱?

  1. 回调函数延迟绑定,通过then传入
  2. 返回值穿透,根据 then 中回调函数的传入值创建不同类型的 Promise,然后把返回的 Promise 穿透到外层,以供后续的调用
  3. 错误冒泡,前面产生的错误会一直向后传递,被 catch 接收到

promise的静态方法

all

promise.all([]),当所有结果成功返回时按照请求顺序返回成功,当其中有一个失败方法时,则进入失败方法。

//1.获取轮播数据列表
function getBannerList(){
  return new Promise((resolve,reject)=>{
      setTimeout(function(){
        resolve('轮播数据')
      },300) 
  })
}
//2.获取店铺列表
function getStoreList(){
  return new Promise((resolve,reject)=>{
    setTimeout(function(){
      resolve('店铺数据')
    },500)
  })
}
//3.获取分类列表
function getCategoryList(){
  return new Promise((resolve,reject)=>{
    setTimeout(function(){
      resolve('分类数据')
    },700)
  })
}
function initLoad(){ 
  Promise.all([getBannerList(),getStoreList(),getCategoryList()])
  .then(res=>{
    console.log(res) 
  }).catch(err=>{
    console.log(err)
  })
} 
initLoad()

allSettled

参数无论返回结果是否成功,都返回每个参数的执行状态

const resolved = Promise.resolve(2);
const rejected = Promise.reject(-1);
const allSettledPromise = Promise.allSettled([resolved, rejected]);
allSettledPromise.then(function (results) {
  console.log(results);
});
// 返回结果:
// [
//    { status: 'fulfilled', value: 2 },
//    { status: 'rejected', reason: -1 }
// ]

any

参数中只要有一个成功,就返回改成功的执行结果

const resolved = Promise.resolve(2);
const rejected = Promise.reject(-1);
const anyPromise = Promise.any([resolved, rejected]);
anyPromise.then(function (results) {
  console.log(results);
});
// 返回resolved的结果:
// 2

race

返回最先执行成功的结果

//请求某个图片资源
function requestImg(){
  var p = new Promise(function(resolve, reject){
    var img = new Image();
    img.onload = function(){ resolve(img); }
    img.src = 'http://www.baidu.com/img/flexible/logo/pc/result.png';
  });
  return p;
}
//延时函数,用于给请求计时
function timeout(){
  var p = new Promise(function(resolve, reject){
    setTimeout(function(){ reject('图片请求超时'); }, 5000);
  });
  return p;
}
Promise.race([requestImg(), timeout()])
.then(function(results){
  console.log(results);
})
.catch(function(reason){
  console.log(reason);
});

Generator

Generator 函数是异步任务的容器,需要暂停的地方,都用 yield 语法来标注。最后返回的是迭代器。Generator不自动执行,需要next方法一步一步往下执行

function* gen() {
    let a = yield 111;
    console.log(a);
    let b = yield 222;
    console.log(b);
    let c = yield 333;
    console.log(c);
    let d = yield 444;
    console.log(d);
}
let t = gen();
t.next(1); //第一次调用next函数时,传递的参数无效,故无打印结果
t.next(2); // a输出2;
t.next(3); // b输出3; 
t.next(4); // c输出4;
t.next(5); // d输出5;

co函数库包装Generator可以返回promise对象

async/await

ES7中提出的新异步解决方案,async 是 Generator 函数的语法糖。 async返回的是promise对象,await控制执行顺序。

事件循环Eventloop

js是单线程,那么js是怎么处理这些任务不阻塞的呢?

浏览器的Eventloop

  1. 调用堆栈(call stack)负责跟踪所有要执行的代码。每当一个函数执行完成时,就会从堆栈中弹出(pop)该执行完成函数;如果有代码需要进去执行的话,就进行 push 操作。
  2. 事件队列(event queue)负责将新的 function 发送到队列中进行处理。它遵循 queue 的数据结构特性,先进先出,在该顺序下发送所有操作以进行执行。
  3. 每当调用事件队列(event queue)中的异步函数时,都会将其发送到浏览器 API,根据从调用堆栈收到的命令,API 开始自己的单线程操作。
  4. JavaScript 语言本身是单线程的,而浏览器 API 充当单独的线程。事件循环(Eventloop)促进了这一过程,它会不断检查调用堆栈是否为空。如果为空,则从事件队列中添加新的函数进入调用栈(call stack);如果不为空,则处理当前函数的调用。我们把整个过程串起来就是这样的一个循环执行流程。

1654756219(1).jpg

简单来说 Eventloop 通过内部两个队列来实现 Event Queue 放进来的异步任务。以 setTimeout 为代表的任务被称为宏任务,放到宏任务队列(macrotask queue)中;而以 Promise 为代表的任务被称为微任务,放到微任务队列(microtask queue)中。

macrotasks(宏任务): 
script(整体代码),setTimeout,setInterval,setImmediate,I/O,UI rendering,event listner
microtasks(微任务): 
process.nextTick(优先级比promise高), Promises, Object.observe, MutationObserver

Eventloop的执行过程:

  1. JavaScript 引擎首先从宏任务队列(macrotask queue)中取出第一个任务;
  2. 执行完毕后,再将微任务(microtask queue)中的所有任务取出,按照顺序分别全部执行(这里包括不仅指开始执行时队列里的微任务),如果在这一步过程中产生新的微任务,也需要执行;
  3. 然后再从宏任务队列中取下一个,执行完毕后,再次将 microtask queue 中的全部取出,循环往复,直到两个 queue 中的任务都取完。 总结起来就是:一次 Eventloop 循环会处理一个宏任务和所有这次循环中产生的微任务

Node.js 的 Eventloop

Node.js 和浏览器端宏任务队列的另一个很重要的不同点是,浏览器端任务队列每轮事件循环仅出队一个回调函数接着去执行微任务队列;而 Node.js 端只要轮到执行某个宏任务队列,则会执行完队列中所有的当前任务,但是当前轮次新添加到队尾的任务则会等到下一轮次才会执行。

EventLoop 对渲染的影响

requestIdlecallback 和 requestAnimationFrame浏览器宿主环境提供的方法。

requestAnimationFrame: 有屏幕的硬件限制,比如 60Hz 刷新率,简而言之就是 1 秒刷新了 60 次,16.6ms 刷新一次。requestAnimationFrame,这个 API 保证在下次浏览器渲染之前一定会被调用,因此,最平滑动画的最佳循环间隔是1000ms/60,约等于16.6ms。

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

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

  3. requestAnimationFrame是由浏览器专门为动画提供的API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了CPU开销。IE9-浏览器不支持该方法,可以使用setTimeout来兼容

requestIdlecallback: 当宏任务队列中没有任务可以处理时,浏览器可能存在“空闲状态”。这段空闲时间可以被 requestIdlecallback 利用起来执行一些优先级不高、不必立即执行的任务.当然为了防止浏览器一直处于繁忙状态,导致 requestIdlecallback 可能永远无法执行回调,它还提供了一个额外的 timeout 参数,为这个任务设置一个截止时间。浏览器就可以根据这个截止时间规划这个任务的执行。

v8引擎

V8 引擎执行 JS 代码都要经过哪些阶段?

  1. Parse 阶段:V8 引擎负责将 JS 代码转换成 AST(抽象语法树);
  2. Ignition 阶段:解释器将 AST 转换为字节码,解析执行字节码也会为下一个阶段优化编译提供需要的信息;
  3. TurboFan 阶段:编译器利用上个阶段收集的信息,将字节码优化为可以执行的机器码;
  4. Orinoco 阶段:垃圾回收阶段,将程序中不再使用的内存空间进行回收。

生成AST

  1. 词法分析:这个阶段会将源代码拆成最小的、不可再分的词法单元,称为 token。比如这行代码 var a =1;通常会被分解成 var 、a、=、2、; 这五个词法单元。另外刚才代码中的空格在 JavaScript 中是直接忽略的。
  2. 语法分析:这个过程是将词法单元转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树,这个树被称为抽象语法树。

抽象语法树的应用场景:前端领域经常使用一个工具 Babel,比如现在浏览器还不支持 ES6 语法,需要将其转换成 ES5 语法,这个过程就要借助 Babel 来实现。将 ES6 源码解析成 AST,再将 ES6 语法的抽象语法树转成 ES5 的抽象语法树,最后利用它来生成 ES5 的源代码。另外 ESlint 的原理也大致相同,检测流程也是将源码转换成抽象语法树,再利用它来检测代码规范。

生成字节码

V8 重新引进了 Ignition 解释器,将抽象语法树转换成字节码,来解决AST直接转机器码带来的内存占用过大的问题。

生成机器码

生成的字节码以及分析数据会传给 TurboFan 编译器,它会根据分析数据的情况生成优化好的机器码。直接执行编译后的机器码,这样性能会更好。