前端面试知识点梳理——js基础

980 阅读24分钟

- 变量类型和计算

变量类型

  • String
  • Number
  • Boolean
  • Object
  • Null
  • Undefined
  • Symbol

常见值类型

let a  //undefined
let s = 'abc' // String
let n = 100 // Number
let b = true // Boolean
let s = Symbol('s') // Symbol

常见引用类型

let Obj = { x:100 }
let arr = ['a','b','c']
let n = null //特殊引用类型,指针指向为空地址
function fn(){} //特殊引用类型,但不用于存储数据,所以没有“拷贝,复制函数”这一说

值类型和引用类型的区别

  • 值类型
    • 值类型的值直接存储在栈中,各不相关,如图:

值类型

  • 引用类型
    • 引用类型的值存储的是一个内存地址,这个内存地址指向于那个对象(堆中)栈从上往下排列,堆从下往上排列,如图:

引用类型

为什么会区分这两种类型

  • 根据内存空间和cpu耗时考虑规划出来的解决方法
  • 如果像值类型一样直接存储复制值,会出现占内存、耗时等问题

面试题举例

  1. 例1
    let a = 100
    let b = a
    a = 200
    console.log(b)  //100
    
  2. 例2
    let a = {age:20}
    let b = a
    b.age = 30
    console.log(b.age)  //30
    

typeof 运算符

作用

  • 识别所有的值类型
  • 识别函数
  • 判断是否是引用类型(弊端:不可再细分)
    • 引申(Object.prototype.toString.call()
    • 引申 (instanceof) —— 基于原型链实现的
    • 引申(Reflect.apply(toString,[],[])

用法

//判断所有的值类型
let a;               typeof a   // undefined
let s = 'abc';       typeof s   // string
let n = 100;         typeof n   // number
let b = true;        typeof b   // boolean
let s = Symbol('s'); typeof s   // Symbol
//判断函数
typeof console.log   // function
typeof function(){}   // function
//能识别引用类型(不能再细分)
typeof null        // object
typeof ['a','b']   // object
typeof { x:100 }   // object

深拷贝——手写

Object.assign(有个坑),不是深拷贝

  • 只深拷贝第一层key,val数据,多层拷贝的是引用地址

原理图

深拷贝

/**
 * 深拷贝
 * @param {*} obj 要拷贝的对象
 * @returns 
 */
function deepClone(obj={}){
    if(typeof obj !='object' || obj != null){
        // obj如果不是对象或者数组,是null,直接返回
        return obj
    }

    // 初始化返回结果
    let result
    if(obj instanceof Array){
        result = []
    }else{
        result = {}
    }

    for(let key in obj){
        // 保证 key 不是原型的属性
        if(obj.hasOwnProperty(key)){
            // 递归调用!!!!
            result[key] = deepClone(obj[key])
        }
    }
    // 返回结果
    return result
}
let obj1 = {
    age:20,
    name:'xxx',
    address:{
        city:'beijing'
    },
    arr:['a','b','c']
}

let  obj2 = deepClone(obj1)

obj2.age = 21;
console.log(obj1.age) // 20;

变量计算-类型转换

字符串拼接

  • 任何类型 + 字符串类型 均会转换为字符串拼接
const a = 100 + 10    // 110
const b = 100 + '10'  // '10010'
const c = true + '10' // 'true10'

== 运算符——不严格模式(先做类型转换在比较值)

100 == '100'      // true
0 == ''           // true
0 == false        // true
false == ''       // true
null == undefined //true

=== 运算符——严格模式(即判定值也判定类型)

  • 使用严格模式,上面的结果都为false。

if语句和逻辑运算

  • truly 变量:!!a === true 的变量 (经过两次取反结果为true的变量)
  • falsely 变量:!!a === false 的变量
    !!0 === false
    !!NaN === false
    !!'' === false
    !!null === false
    !!undefined === false
    !!false === false
    

逻辑判断

console.log(10 && 0)        // 0
console.log(''||'abc')      // 'abc'
console.log(!window.abc)    // true

面试题举例

  • typeof 能判断哪些类型
  • 何时使用 === 何时使用 ==
  • 值类型和引用类型的区别
const obj1 = { x: 100, y: 200};
const obj2 = obj1;
let x1 = obj1.x;
obj2.x = 101;
x1 = 102;
console.log(obj1.x);    //101
  • 引申-判断数据类型有几种方法,分别是什么
    • typeof
    • instanceof
    • Object.prototype.toString.call()
    • 根据对象的constructor 判断
    • jQ封装判断类型方法
      jQuery.isArray();是否为数组\
      jQuery.isEmptyObject();是否为空对象 (不含可枚举属性)。\
      jQuery.isFunction():是否为函数\
      jQuery.isNumberic():是否为数字\
      jQuery.isPlainObject():是否为使用“{}”或“new Object”生成对象,而不是浏览器原生提供的对象。\
      jQuery.isWindow(): 是否为window对象;\
      jQuery.isXMLDoc(): 判断一个DOM节点是否处于XML文档中。
      

- 原型和原型链

  • Js本身是基于原型继承的面向对象语言。

如何用class实现继承

class的由来

  • JavaScript属于弱类型的语言,在JS中并没有像Java中的那样的类的概念,ES6的Class实际上基于JS中的原型属性prototype,改良出来的语法糖

class写法

class Student {
    constructor(name,age){
        this.name = name
        this.age = age
    }
    sayHi(){
        console.log('hello')
    }
}
let wang = new Student('wang',18)
console.log(wang.age) // 18

class实质

  • 其实class类实质上就是一个函数。类自身指向的就是构造函数。所以可以认为ES6中的类其实就是构造函数的另外一种写法. 类的所有方法都定义在类的prototype属性上面

construtor

  • constructor 方法是类的构造函数的默认方法,通过new 命令生成实例对象时,自动调用该方法。
  • constructor方法如果没有显式定义,会隐式生成一个constructor方法。所以即使你没有添加构造函数,构造函数也是存在的。constructor方法默认返回实例对象this,但是也可以指定constructor方法返回一个全新的对象,让返回的实例对象不是该类的实例。

继承

  • 继承的六种方式(区别详见)
    • 原型链继承
    • 借用构造函数继承
    • 组合继承(常用但不是最好用的)
    • 原型式继承
    • 寄生式继承
    • 寄生组合继承
  • ES6:class继承
  • ES5:prototype继承
  • class继承实现方式
class People {
    constuctor(name){
        this.name = name
    }
    eat(){
        console.log('eat someting')
    }
}
class Student extends People {
    constuctor(name,nubmer){
        super(name)
        this.nubmer = nubmer
    }
    sayHi(){
        console.log('heillo')
    }
}
let xialuo = new Sutdent('夏洛',100)

原型

  • 何为原型:如图,我们定义了一个构造函数,而每一个构造函数中都有一个prototype属性,这就是原型。他是一个指针,指向了原型对象。
  • 白话:存储共有属性和方法的对象

原型

  • 原型有几种
    • 显式原型 prototype
    • 隐式原型 __proto__
  • 原型关系
    • 每个实例都有隐式原型 __proto__ 
    • 每个class都有显式原型 prototype
    • 每个实例的隐式原型指向new class的显式原型
    • 每个class的隐式原型指向他继承的对象的显式原型
    • 默认javascript会将所有的对象将Object设置为最顶级(创始人),默认所有对象都会继承自Object
    console.log(People.prototype === Student.prototype.__proto__)  // true
    
  • 原型关系图 原型关系图
  • 基于原型的执行规则
    • 获取属性xialuo.name 或 执行方法xialuo.sayhi()时
    • 先在自身属性和方法中寻找
    • 如果找不到则自动去 __proto__ 中查找,即到上一级的显式原型prototype中查找
    • 如果在找不到,会继续向上查找,直到找到Object.__proto__ 返回null

原型链

  • 何为原型链
    • 当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有找到,则会去它的__proto__隐式原型上查找,即它的构造函数的prototype,如果还没有找到就会再在构造函数的prototype的__proto__中查找,这样一层一层向上查找就会形成一个链式结构,我们称为原型链。

    • 在JavaScript 中,每个对象都有一个指向它的原型(prototype)对象的内部链接。这个原型对象又有自己的原型,直到某个对象的原型为 null 为止(也就是不再有原型指向),组成这条链的最后一环。这种一级一级的链结构就称为原型链

原型链

  • 装逼原型链图解

原型链图解

面试题举例

  • 如何准确判定一个变量是不是数组
  • 手写一个简易的JQuery,考虑插件和扩展性
  • class的本质,怎么理解

- 作用域和闭包

作用域和自由变量

作用域

  • 何为作用域
    • 作用域就是变量的适用范围 
    • 如图:

作用域

  • 作用域类型

    类型对象
    全局作用域global/window
    函数作用域function
    动态作用域this
    块级作用域(ES6新增){}
  • 作用域链:
    当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

  • 作用域举例

// ES6 块级作用域
if(true){
    let x = 100
}
console.log(x)  //会报错 x未定义

自由变量

  • 何为自由变量
    • 凡是跨了自己的作用域的变量都叫自由变量。
  • 执行原理
    • 一个变量在当前作用域没有定义,但被使用了
    • 向上级作用域,一层层依次寻找,直到找到为止
    • 如果到全局作用域都没找到,则报错 xx is not defined
  • 面试题举例
    var aa = 22;
    function a(){
        console.log(aa);
    }
    function b(fn){
        var aa = 11;
        fn();
    }
    b(a); //22
    
    // 在创建这个函数的时候,这个函数的作用域就已经决定了,而是不是在调用的时候。
    

闭包

闭包概念

  • 闭包是指:有权访问另一个函数作用域中的变量的函数;

闭包原理:利用了作用域链的特性

  • 原理举例
    var age = 18;
    function cat(){
        age++;
        console.log(age);// cat函数内输出age,该作用域没有,则向外层寻找,结果找到了,输出[19];
    }
    cat();//19
    // 如果我们再次调用时,结果会一直增加,也就变量age的值一直递增。
    cat();//20
    cat();//21
    cat();//22
    // 那么问题来了:
    // 如果程序还有其他函数,也需要用到age的值,则会受到影响,而且全局变量还容易被人修改,比较不安全,这就是全局变量容易污染的原因,
    // 所以我们必须解决变量污染问题,那就是把变量封装到函数内,让它成为局部变量。如:
    function person(){
        var age = 18;
        function cat(){
            age++;
            console.log(age);
        }
        return cat;
    }
    person()();// 19
    person()();// 19
    // 那么问题又来了:
    // 每次调用函数person,进入该作用域,变量age就会重新赋值为18,所以cat的值一直是19;
    // 所以需要做一下调整:
    var per = person();//per相当于函数cat
    per();// 19 即cat() 这样每次调用不在经过age的初始值,这样就可以一直增加了
    per();// 20
    per();// 21
    // 而且变量age在函数内部,不易修改和外泄,相对来说比较安全。
    

闭包原理

闭包的表现形式

  • 函数作为参数被传递
function print(fn){
    let a = 200
    fn()
}
let a = 100
function fn(){
    console.log(a)
}
print(fn)  //100
  • 函数作为返回值被返回
function create(){
    let a =100
    return function(){
        console.log(a)
    }
}
let fn = create()
let a = 200
fn() //100

闭包的作用

  • 优点:
    • 隐藏变量,避免全局污染
    • 可以读取函数内部的变量
  • 缺点
    • 导致变量不会被垃圾回收机制回收,造成内存消耗
    • 不恰当的使用闭包可能会造成内存泄漏的问题

闭包的应用

  • 需求:实现变量a 自增
    var a  = 10;
    function Add3(){
        var a = 10;
        return function(){
            a++;
            return a;
        };
    };
    var cc =  Add3();
    console.log(cc()); // 11
    console.log(cc()); // 12
    console.log(cc()); // 13
    console.log(a);    // 10
    
  • 闭包隐藏数据,只提供api
    function createCache(){
        const data = {}  //闭包中的数据,被隐藏,不被外界访问
        return {
            set:function(key,val){
                data[key] = val
            }
            get: function(key){
                return data[key]
            }
        }
    }
    const c = createCache()
    c.set(a,100)
    console.log(c.get(a)) //100
    
  • 定时器
    function wait(message) {
        setTimeout(function timer() {
            //延时函数回调函数timer
            //timer内部函数具有涵盖wait函数作用域的闭包,还有对变量message的引用
            console.log(message);
        }, 1000)
    }
    wait('闭包函数应用')
    
  • 事件监听器
    function test() {
        var a = 0;
        //事件监听器 保持对test作用域的访问
        $('ele').on('click', function() {
            a++;
        });
    }
    
  • ajax
    ! function() {
        var localData = "localData here";
        var url = "http://www.baidu.com/";
        //ajax使用了localData,url
        $.ajax({
            url: url,
            success: function() {
                // do sth...              
                console.log(localData);
            }
        });
    }();
    
  • 异步(同步)操作
    • 只要使用了回调函数,实际上就是使用了闭包。
  • 模块
    var foo = (
        function Module() {
            var something = 'cool';
            var another = [1, 2];
    
            function doSomething() {
                console.log(something)
            }
    
            function doAnother() {
                console.log(another.join(','))
            }
            return {
                doSomething: doSomething,
                doAnother: doAnother
            }
        }
    )();
    foo.doSomething();
    foo.doAnother();
    

this指针

何为this

  • this就是指针, 指向我们调用函数的对象;  this是JavaScript中的一个关键字,它是函数运行时,在函数体内自动生成的一个对象,只能在函数体内部使用。

this取值时机(参考文章)

  • this取什么值是在函数调用的时候确认的,不是定义的时候确认的 !!!

this的几种赋值情况

  • 全局上下文

    • 非严格模式和严格模式中this都是指向顶层对象(浏览器中是window)。
      this === window // true
      'use strict'
      this === window;
      this.name = 'wchao';
      console.log(this.name); // wchao
      
  • 函数上下文

    • 普通函数调用模式

      • 非严格模式
        var name = 'window';
        var doSth = function(){
            console.log(this.name);
        }
        doSth(); // 'window'
        // 因为es5的全局变量挂载到顶层对象(window)上
        
        let name = 'window';
        let doSth = function(){
            console.log(this === window);
            console.log(this.name);
        }
        doSth(); // 'true' 'undefined'
        // 因为let没有给顶层对象中(浏览器是window)添加属性,this.name在window上找不到
        
      • 严格模式
        'use strict'
        var name = 'window';
        var doSth = function(){
            console.log(typeof this === 'undefined');
            console.log(this.name);
        }
        doSth(); // true,// 报错,因为this是undefined
        
      • 举例
        var name = '轩辕Rowboat';
        setTimeout(function(){
            console.log(this.name);  //this指向window 轩辕Rowboat
        }, 0);
        
    • 对象中的函数(方法)调用模式

      var name = 'window';
      var doSth = function(){
          console.log(this.name);
      }
      var student = {
          name: '轩辕Rowboat',
          doSth: doSth,
          other: {
              name: 'other',
              doSth: doSth,
          }
      }
      student.doSth(); // '轩辕Rowboat'
      student.other.doSth(); // 'other'
      // 用call类比则为:
      student.doSth.call(student);  // '轩辕Rowboat'
      // 用call类比则为:
      student.other.doSth.call(student);
      // 把对象中的函数赋值成一个变量,变回变成普通函数
      var studentDoSth = student.doSth;
      studentDoSth(); // 'window'
      // 用call类比则为:
      studentDoSth.call(undefined);
      
    • callapplybind 调用模式

      • 相同
        • 三个方法均可以改变普通函数的this指向
      • 不同
        • 参数不同
        fun.call(thisArg, arg1, ... , argN)
        fun.apply(thisArg, [argsArray])
        fun.bind(thisArg, arg1, ... , argN)
        
        • call,apply方法之后调用后,函数立即执行。bind方法调用后,返回了一个改变this后的函数,不会立即执行。
        • 被bind绑定过this的函数,this不会再被改变
      • 常见应用
        • 判断数据类型
          Object.prototype.toString.call(null) //[object Null]
          
        • 获取函数最大值和最小值
          Math.max.apply(Math,[1,2,3]) //3
          
        • 伪数组转真数组
          Array.prototype.slice.call(arguments)
          
      • 举例
        // 有只猫叫小猫,小猫会吃鱼
        const cat = {
            name: '小猫',
            eatFish(...args) {
                console.log('cat this指向=>', this);
                console.log('...args', args);
                console.log(this.name + '吃鱼');
            },
        }
        // 有只狗叫大狗,大狗会吃骨头
        const dog = {
            name: '大狗',
            eatBone(...args) {
                console.log('dog this指向=>', this);
                console.log('...args', args);
                console.log(this.name + '吃骨头');
            },
        }
        
        console.log('=================== call =========================');
        // 有一天大狗想吃鱼了,可是它不知道怎么吃。怎么办?小猫说我吃的时候喂你吃
        cat.eatFish.call(dog, '汪汪汪', 'call')
        // 大狗为了表示感谢,决定下次吃骨头的时候也喂小猫吃
        dog.eatBone.call(cat, '喵喵喵', 'call')
        
        console.log('=================== apply =========================');
        cat.eatFish.apply(dog, ['汪汪汪', 'apply'])
        dog.eatBone.apply(cat, ['喵喵喵', 'apply'])
        
        console.log('=================== bind =========================');
        // 有一天他们觉得每次吃的时候再喂太麻烦了。干脆直接教对方怎么吃
        const test1 = cat.eatFish.bind(dog, '汪汪汪', 'bind')
        const test2 = dog.eatBone.bind(cat, '喵喵喵', 'bind')
        test1()
        test2()
        
    • 构造函数调用模式

      function Student(name){
          this.name = name;
          console.log(this); // {name: '轩辕Rowboat'}
          // 相当于返回了
          // return this;
      }
      var result = new Student('轩辕Rowboat');
      
      • 使用new操作符调用函数,会自动执行以下步骤:
        • 创建了一个全新的对象。
        • 这个对象会被执行[[Prototype]](也就是__proto__)链接。
        • 生成的新对象会绑定到函数调用的this
        • 通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上。
        • 如果函数没有返回对象类型Object(包含FunctoinArrayDateRegExgError),那么new表达式中的函数调用会自动返回这个新的对象。
      • 变种
        function Student(name){
            this.name = name;
            console.log(this); // {name: '轩辕Rowboat'}
            // 相当于返回了
             return function(){
                 console.log(this)
             };
        }
        var result = new Student('轩辕Rowboat');
        result()  // window
        // 因为result相当于普通函数调用,在顶层调用,this指向window
        
    • 原型链中的调用模式

    function Student(name){
        this.name = name;
    }
    var s1 = new Student('轩辕Rowboat');
    Student.prototype.doSth = function(){
        console.log(this.name);
    }
    s1.doSth(); // '轩辕Rowboat'
    
    • 此调用模式与对象方法调用模式类似,都会生成新对象,并像原型链上查找
    • ES6 class写法
      class Student{
          constructor(name){
              this.name = name;
          }
          doSth(){
              console.log(this.name);
          }
      }
      let s1 = new Student('轩辕Rowboat');
      s1.doSth();
      
    • 箭头函数调用模式
      • 箭头函数和普通函数的区别
        • 没有自己的thissuperargumentsnew.target绑定。
        • 不能使用new来调用。
        • 没有原型对象。
        • 不可以改变this的绑定。
        • 形参名称不能重复。
      • 如果箭头函数被非箭头函数包含,则this绑定的是最近一层非箭头函数的this,否则this的值则被设置为全局对象。
        var name = 'window';
        var student = {
            name: '轩辕Rowboat',
            doSth: function(){
                // var self = this;
                var arrowDoSth = () => {
                    // console.log(self.name);
                    console.log(this.name);
                }
                arrowDoSth();
            },
            arrowDoSth2: () => {
                console.log(this.name);
            }
        }
        student.doSth(); // '轩辕Rowboat'
        student.arrowDoSth2(); // 'window'
        
        
    • DOM事件处理函数调用
    • 内联事件处理函数调用
  • 优先级

    • new 调用 > call、apply、bind 调用 > 对象上的函数调用 > 普通函数调用。
  • 如何判断this指向

    • 找到这个函数的直接调用位置
    • 判断函数类型
      • 如果是箭头函数,则为第一个包裹箭头函数的普通函数的this指向
      • 如果不是箭头函数,但是使用了bind,call,apply等改变this的方法,this被重新绑定为bind、call、apply函数的第一个参数
    • 如果是普通函数,并且没有绑定this
      • 如果是new的方式调用,this被绑定到实例上
      • 如果被调用,谁调用便指向谁
      • 如果直接执行,this指向window
    • 思维导图

    如何判断this指向

面试题举例(解题思路)

/**
 * Question 1
 */

var name = 'window'

var person1 = {
  name: 'person1',
  show1: function () {
    console.log(this.name)
  },
  show2: () => console.log(this.name),
  show3: function () {
    return function () {
      console.log(this.name)
    }
  },
  show4: function () {
    return () => console.log(this.name)
  }
}
var person2 = { name: 'person2' }

person1.show1()  // person1
person1.show1.call(person2)  // person2

person1.show2()   // window
person1.show2.call(person2)  // window

person1.show3()() //window
person1.show3().call(person2)  // person2
person1.show3.call(person2)()  // window

person1.show4()()  // person1
person1.show4().call(person2)  // person1
person1.show4.call(person2)()  // person2
/**
 * Question 2
 */
var name = 'window'

function Person (name) {
  this.name = name;
  this.show1 = function () {
    console.log(this.name)
  }
  this.show2 = () => console.log(this.name)
  this.show3 = function () {
    return function () {
      console.log(this.name)
    }
  }
  this.show4 = function () {
    return () => console.log(this.name)
  }
}

var personA = new Person('personA')
var personB = new Person('personB')

personA.show1() // personA
personA.show1.call(personB)  // personB

personA.show2() // personA
personA.show2.call(personB) // personA

personA.show3()() // window
personA.show3().call(personB) // personB
personA.show3.call(personB)() // window

personA.show4()() // personA
personA.show4().call(personB) // personA
personA.show4.call(personB)() // personB
  • 手写bind call apply方法
// bind
Function.prototype.bind = function(){
    if (typeof this !== 'function') { 
        throw new TypeError('Error') 
    }
    //将参数转为数组
    const args = Array.prototype.slice.call(arguments)
    // 获取this(取出数组第一项,剩余未参数)
    const t = args.shift()
    const self = this //当前函数
    // 返回一个函数
    return function(){
        // 执行原函数,并返回结果
        return self.apply(t,args)
    }

}
Function.prototype.bind = function(context,...args){
    if (typeof this !== 'function') { 
        throw new TypeError('Error') 
    }
    const fn = this   //获取被调用函数
    if(!context) context = window // 没有context,或者传递的是 null undefined,则重置为window
    return function(){
        return fn.apply(context,[...args])
    }
}
// call
Function.prototype.call = function(context,...args){
    if (typeof this !== "function") { 
        throw new TypeError('Error')
    }  
    if(!context) context = window; // 没有context,或者传递的是 null undefined,则重置为window
    const fn = Symbol(); // 指定唯一属性,防止 delete 删除错误
    context[fn] = this; // 将 this 添加到 context的属性上
    const result = context[fn](...args); // 直接调用context 的 fn
    delete context[fn]; // 删除掉context新增的symbol属性
    return result; // 返回返回值
}
// apply
Function.prototype.apply = function(context, args = []) { // args默认参数为空数组
   if(typeof this !== 'function') {
     throw new TypeError(`It's must be a function`)
   }
   if(!context) context = window;
   const fn = Symbol();
   context[fn] = this;
   const result = context[fn](...args);  // Function.prototype(...args)
   delete context[fn];
   return result;
}

- 异步

进程与线程

进程

  • 何为进程(浏览器为多进程)
    • 进程是 cpu 资源分配的最小单位(是能拥有资源和独立运行的最小单位)

线程

  • 何为线程(JS为单线程)
    • 线程是 cpu 调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)

进程与线程的关系

进程好比公司的一个个部门,每个部门有独立的资源,且相互独立的。线程好比部门里的员工,每个员工协作完成任务,员工之间共享空间。

JS运行机制

JavaScript是单线程,这已经成了这门语言的核心特征,将来也不会改变。

为什么JavaScript不能有多个线程

与它的用途有关,作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器就无法决定采用哪个线程的操作。当然,我们可以为浏览器引入“锁”的机制来解决这些冲突,但这会大大提高复杂性,所以 JavaScript 从诞生开始就选择了单线程执行。

单线程所产生的问题

JavaScript 是单线程的,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着,从而导致阻塞。 居于此问题,将任务分为了以下两种:

  • 同步任务(synchronous)
    同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
  • 异步任务(asynchronous)
    异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
  • JS执行异步代码的过程
    已AJAX异步请求为例
    主线程在发起 AJAX 请求后,会继续执行其他代码。主线程在执行完当前循环中的所有代码后,就会到消息队列取出这条消息,并执行它。到此为止,就完成了工作线程对主线程的通知,回调函数也就得到了执行。如果一开始主线程就没有提供回调函数,AJAX 线程在收到 HTTP 响应后,也就没必要通知主线程,从而也没必要往消息队列放消息。如图 JS执行异步代码的过程

Web Worker

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

event loop(事件循环/事件轮询)

  • 什么是event loop
    主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。
  • 图解:
    event loop
    • 主线程运行的时候,产生堆(heap)和 执行栈(call stack)
    • 栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)
    • 只要执行栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。
  • Node.js的Event Loop
    • Node.js也是单线程的Event Loop,但是它的运行机制不同于浏览器环境。
    • Node.js的运行机制
      • (1)V8引擎解析JavaScript脚本。
      • (2)解析后的代码,调用Node API。
      • (3)libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
      • (4)V8引擎再将结果返回给用户。
    • Node.js新增两个与"任务队列"有关的方法
      • process.nextTick 它指定的任务总是发生在所有异步任务之前
      process.nextTick(function A() {
        console.log(1);
        process.nextTick(function B(){console.log(2);});
      });
      
      setTimeout(function timeout() {
        console.log('TIMEOUT FIRED');
      }, 0)
      // 1
      // 2
      // TIMEOUT FIRED
      
      • setImmediate setImmediate指定的回调函数,总是排在setTimeout前面

宏任务/微任务

  • 宏任务(macroTask)
    • 什么是宏任务
      可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)
    • 哪些属于宏任务
      主代码块, setTimeout, setInterval, setImmediate, requestAnimationFrame,Ajax,Dom事件, I/O, UI rendering
    • 优先级
      主代码块 > setImmediate > MessageChannel > setTimeout / setInterval
  • 微任务(microtask)
    • 什么是微任务
      可以理解是在当前 task 执行结束后立即执行的任务
    • 哪些属于微任务
      process.nextTick, Promise, Object.observe, MutationObserver,async/awite
    • 优先级
      process.nextTick > Promise > MutationObserver
  • 宏任务与微任务的根本区别
    1. 宏任务执行流程,执行栈(call stack)-> 尝试DOM渲染 -> Web APIs -> 回调队列(callback Queue)-> 触发eventlop
    2. 微任务执行流程,执行栈(call stack)-> 微任务队列(micro task Queue) -> 尝试DOM渲染 -> 触发eventlop
    3. 综上所述:一个在渲染DOM后,一个在渲染DOM前,所以微任务比宏任务触发时机早
  • 面试题举例
    async function async1(){
        console.log('async1 start')
    await async2()
        console.log('async1 end')
    }
    async function async2(){
        console.log('async2')
    }
    async function async3(){
        console.log('async3')
    }
    console.log('sctipt start')
    setTimeout(function(){
        console.log('setTimeout')
        new Promise(function(resolve){
            console.log('promise3')
            resolve()
        }).then(function(){
            console.log('promise4')
        })
    },0)
    async1()
    new Promise(function(resolve){
        console.log('promise1')
         setTimeout(function(){
            console.log('setTimeout1')
        },0)
        resolve()
    }).then(function(){
        console.log('promise2')
    })
    console.log('sctipt end')
    
    
    //输出结果
    // sctipt start
    // async1 start
    // async2
    // promise1
    // sctipt end
    // async1 end
    // promise2
    // setTimeout
    // promise3
    // promise4
    // setTimeout1
    

promise

什么是promise

PromiseES6加入标准的一种异步编程解决方案,通常用来表示一个异步操作的最终完成 (或失败)。Promise标准的提出,解决了JavaScript地狱回调的问题。

语法

var p = new Promise(function(resolve, reject) {...} /* executor */); 
p.then(() => {}) // 成功resolve 
.catch(()=> {}); // 失败reject

promise的状态

  • 三种状态
    • pending(待定)初始状态
    • fulfilled(实现/成功)操作成功
    • rejected(被否决)操作失败
  • 状态改变
    • then 正常返回resolved状态,里面有报错则返回rejected状态
    • catch 正常返回resolved状态,里面有报错则返回rejected状态
  • 面试题举例
console.log('here we go')
new Promise(resolve => {
    setTimeout(()=>{
        resolve()
    },2000)
}).then(()=>{
    console.log('start')
    throw new Error('test error1')
}).catch(err1=>{
    console.log('I catch:',err1)
}).then(()=>{
    console.log('arrice here')
}).then(()=>{
    console.log('I arrice here')
    throw new Error('test error2')
}).catch((err2)=>{
    console.log('No, I cathc:',err2)
    throw new Error('test error3')
}).then(()=>{
     console.log('...and here')
}).catch((err3)=>{
    console.log('NoNoNo, I cathc:',err3)
})


//输出结果
here we go
start
I catch test error1
arrice here
I arrice here
No, I cathc:test error2
NoNoNo, I cathc:another error

promise常用方法

  • Promise.all(iterable)
    这个方法返回一个新的 promise 对象。一般该方法会接受一个iterable参数,里面是一个promise列表,当所有的promise都触发成功时才会触发成功,一旦有一个失败了,则会马上停止其他promise的执行。当iterable里面的结果都执行成功了,这个新的promise对象会将所有的结果已数组的形式依次返回。当有一个失败是,这个新的promise对象会将失败的信息返回。
    const promise1 = Promise.resolve(3);
    const promise2 = 42;
    const promise3 = new Promise((resolve, reject) => {
      setTimeout(resolve, 100, 'foo');
    });
    
    Promise.all([promise1, promise2, promise3]).then((values) => {
      console.log(values);
    });
    // expected output: Array [3, 42, "foo"]
    
  • Promise.race(iterable)
    方法返回一个 promise,一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝。
    const promise1 = new Promise((resolve, reject) => {
      setTimeout(resolve, 500, 'one');
    });
    
    const promise2 = new Promise((resolve, reject) => {
      setTimeout(resolve, 100, 'two');
    });
    
    Promise.race([promise1, promise2]).then((value) => {
      console.log(value);
      // Both resolve, but promise2 is faster
    });
    // expected output: "two"
    
  • Promise.reject(rease)
    方法返回一个带有拒绝原因的Promise对象。
    function resolved(result) {
      console.log('Resolved');
    }
    
    function rejected(result) {
      console.error(result);
    }
    
    Promise.reject(new Error('fail')).then(resolved, rejected);
    // expected output: Error: fail
    
  • Promise.resolve(value)
    方法返回一个以给定值解析后的Promise 对象。如果这个值是一个 promise ,那么将返回这个 promise ;如果这个值是thenable(即带有"then" 方法),返回的promise会“跟随”这个thenable的对象,采用它的最终状态;否则返回的promise将以此值完成。此函数将类promise对象的多层嵌套展平。
    const promise1 = Promise.resolve(123);
    
    promise1.then((value) => {
      console.log(value);
      // expected output: 123
    });
    

async/await

什么是async/await

  • 在解释async/await 之前需要前置了解下 Generator生成器
    • Generator语法
      function* gen() {
        yield 1;
        yield 2;
        yield 3;
      }
      
      let g = gen();
      // "Generator { }"
      
  • async/await 简单来说就是Generator的语法糖,他就像是隧道尽头的亮光,很多人认为它是异步操作的终极解决方案。
    • async/await语法
      async function gen() {
        await 1;
        await 2;
        await 3;
      }
      
  • async 函数的优点
    • 内置执行器
    • 更好的语义
    • 更广的实用性
  • async/await 与 promise 的关系
    • async只会返回一个Promise,即使返回的是值,它也会进行包裹,类似于then的返回。
    async function testAsync() {
        return "hello async";
    }
    
    const result = testAsync(); 
    console.log(result);  //Promise { 'hello async' }
    
    • await 相当于 promise的then
    • await命令后面的promise有可能是rejecked,一般用try catch 来捕获promise的 catch
      async function myFunction() {
        try {
          await somethingThatReturnsAPromise();
        } catch (err) {
          console.log(err);
        }
      }
      
      // 另一种写法
      
      async function myFunction() {
        await somethingThatReturnsAPromise().catch(function (err){
          console.log(err);
        });
      }
      
    • await 命令只能用在 async 函数之中,如果用在普通函数,就会报错。
  • async/await的优势
    • 在于处理 then 链
      /**
       * 传入参数 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();
      

for-of的应用场景

  • for...in(以及forEach) 是常规的同步遍历
    • 定义:for...in语句以任意顺序遍历一个对象的除Symbol以外的可枚举属性
    • 语法
      for (variable in object)
        statement
      
  • for...of 常用于异步的遍历
    • 定义:for...of语句在可迭代对象(包括Array,Map,Set,String,TypedArray,arguments对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句
    • 语法
      for (variable of iterable) {
          //statements
      }
      
  • 两者区别
function muti(num){
    return new Promise(resolve => {
        setTimeout(()=>{
            resolve(num * num)
        },1000)
    })
}
const nums = [1,2,3];
nums.forEach(async (i) => {
    const res = await muti(i)
    console.log(res)
})
nums.forEach(async (i) => {
    const res = await muti(i)
    console.log(res)
})
// 结果同时执行
!(async function(){
    for(let i of nums){
        const res = await muti(i)
        console.log(res)
    }
})()
// 结果异步执行

推荐学习js网站

js攻略书 tsejx.github.io/javascript-…