面试复习之ES6

212 阅读15分钟

感谢@程序员小鹿 写的系列文章,这是我用来学习用的摘抄

变量提升

基本概念

变量还未被声明,但是却可以使用。

在代码执行前,先在词法环境中进行注册。

变量提升的根本原因就是解决函数之间互相调用的情况。

怎么提升的?

  • 第一阶段:对所有函数声明进行提升(忽略表达式和箭头函数),引用类型的赋值分为三步:
    • 开辟堆空间
    • 存储内容
    • 将地址赋值给变量
  • 第二阶段:对所有变量进行提升,全部赋值为undefined,然后依次执行代码。

使用let和const的话,不能在声明之前使用变量,这叫暂时性死区。

var、let、const

三者区别

  • var存在变量提升,其他两个没有
  • var的全局声明的变量会挂载到window上,其他不会
  • let和const作用基本一样,后者声明的变量不能再次赋值,但是能改变值,就是不能改变其栈内存指向的堆内存地址,但是你可以改堆内存里面的东西。

map、filter、reduce

map

map作用是map中传入一个函数,该函数会遍历数组,对每一个元素做变换后返回新的数组。

let arr = [1,2,3]
arr = arr.map((element,index,arr)=>{
    return arr[index]+1
})//[2,3,4]

filter

filter作用也是生成一个数组,传入的函数返回值是布尔类型,返回值为true的元素放入新数组,通常用来筛选不要的元素。

let arr = [1,2,3,4]
let arr = arr.filter((element,index,arr)=>{
    return element != 4
})//[1,2,3]

reduce

reduce可以将数组中的元素通过回调函数最终转换为一个值。

let arr = [1,2,3]
let sum = arr.reduce((acc,element)=>{
    return acc + element
},0)

Proxy

getter和setter访问属性

作用:

  • 避免意外错误发生
  • 需要记录属性的变化
  • 数据绑定:vue中数据的双向绑定

定义getter和setter:

  • 字面量定义
  • ES6的Class定义
  • 使用Object.definedProperty方法

字面量定义

对象访问属性通常隐式调用getter和setter方法,属性自动关联getter和setter方法。在访问属性的时候都是立即执行的。

const collection = {
    name:'william',
    //读取属性
    get name(){
        return this.name
    }
    //设置属性
    set name(value){
        this.name = value
    }
}
collection.name //隐式调用getter
collection.name = 'djp'//隐式调用setter

ES6的class定义

class William{
    constructor(){
        this.name = 'william'
    }
    
    get firstWilliam(){
        console.log('属性已访问')
        return this.name
    }
    
    set firstWilliam(value){
        this.name = value
    }
}

const x = new William()
x.firstWilliam

Object.defineProperty()定义

对象的字面量与类定义的getter和setter方法不是在同一作用域,因此那些希望作为私有变量属性的标量是无法实现的。

// Object.defineProperty
function xiaolu(){
    let count = 0;
	
    Object.defineProperty(this,'skillLevel',{
        get:() => {
            return count;
        },
        set:value => {
            count = value;
        }
    })
}

let x = new xiaolu();

// 隐士的调用 get 方法
console.log(x.skillLevel)

通过Object.defineProperty()创建的get,set方法,与私有变量处于相同的作用域中,get,set方法分别创建了含有私有变量的闭包。

Proxy代理

Proxy和getter/setter的区别

代理proxy是ES6新提出的。它使我们通过代理控制对另一个对象的访问。 区别是: getter和setter仅仅控制的是单个对象属性,而proxy代理的是对象交互的通用处理,包括对象的方法。

用法:

var proxy = new Proxy(target,handler)
const obj = { name: 'will' }
const proxy = new Proxy(obj, {
    get: (target, key) => {
        return key in target ? target[key] : '不存在该值'
    },
    set: (target, key, value) => {
        target[key] = value
    }
})

Proxy的基本应用

  • 日志记录——当访问属性时,可以在get和set中记录访问日志
  • 校验值——有效避免指定属性类型错误的发生
  • 定义如何计算属性值——每次访问属性值都会进行计算属性值
  • 数据的双向绑定(Vue)——Vue3.0中会通过Proxy替换原来的Object.defineProperty来实现数据响应式。

ES6/7的异步编程

要解决函数回调地狱的问题

Generator生成器

如何使用,阶段状态变化

使用生成器函数可以生产一组值的序列,每个值的生成是基于每次请求的,并不同于标准函数立即生成。

调用生成器不会直接执行,而是通过叫做迭代器的对象,控制生成器执行。

function* WeaponGenerator(){
    yield "1";
    yield "2";
    yield "3";
}

for(let item of WeaponGenerator()){
    console.log(item);
}
//1
//2
//3

使用迭代器控制生成器。

  • 通过调用生成器返回一个迭代器对象,用来控制生成器的执行。
  • 调用迭代器的next方法向生成器请求一个值。
  • 请求的结果返回一个对象,对象中包含一个value值和done布尔值,告诉我们生成器是否还会生成值。
  • 如果没有可执行的代码,生成器就会返回一个undefined值,表示整个生成器已经完成。
function* WeaponGenerator() {
    yield '1'
    yield '2'
    yield '3'
}
let weapon = WeaponGenerator()
console.log(weapon.next());
console.log(weapon.next());
console.log(weapon.next());
console.log(weapon.next());

状态变化如下:

  • 每当代码执行到yield属性,就会生成一个中间值,返回一个对象。
  • 每当生成一个值后,生成器就会非阻塞的挂起执行,等待下一次值的请求。
  • 再次调用next方法,将生成器从挂起状态唤醒,中断执行的生成器从上次离开的位置继续执行。
  • 直到遇到下一个yield,生成器又挂起。
  • 当执行到没有可执行的代码,就会返回一个结果对象,value值为undefined,done为true,生成器执行完毕

PGenerator内部结构实现

生成器就像一个状态运动的状态机。

  • 挂起开始状态——创建一个生成器处于未执行状态。
  • 执行状态
  • 挂起让渡状态——遇到第一个yield
  • 完成状态——代码执行到return 全部代码进入完成状态
function* generator(action) {
    yield '1'+action
    yield console.log(222)
    yield '3'
}

let iterator = generator('djp')
let result1 = iterator.next()
let result2 = iterator.next()
let result3 = iterator.next()
console.log(result1)
console.log(result2)
console.log(result3)
  • 调用生成器之前,全局环境除了生成器变量的引用,其他变量都为undefined
  • 调用生成器但没有执行函数,而是返回一个Iterator迭代器对象并指向当前生成器的上下文。
  • 一般函数调用完成上下午弹出栈,然后被摧毁。当生成器的函数调用完成之后,当前生成器的上下文出栈,但是在全局的迭代器对象还保持与生成器执行上下文引用,所有生成器的词法环境还在。
  • 执行next方法,一般函数会重新创建执行上下文,但是生成器会重新激活并推入栈。(所以标准函数重复调用时,会从头执行,但是生成器是会挂起然后再唤醒激活,不用从头执行。)
  • 当遇到yield时,生成器上下文出栈,但是迭代器还是保持引用,处于非阻塞暂时挂起状态。
  • 当遇到next,继续在原位置执行,知道遇到return,返回值结束。

Promise

Promise的原理

为什么会有Promise?

  • 第一:由于JS单线程,所以执行耗时长的任务时,就会造成UI渲染的阻塞。当前解决方法是使用回调函数来解决,当任务执行完毕,会调用回调方法。
  • 第二:回调函数存在缺点:
    • 不能捕捉异常——回调函数的代码和开始任务的代码不在同一个事件循环内
    • 回调地狱——嵌套回调
    • 处理并行任务棘手——请求只见互不依赖

实现一个简单的Promise:

let promise = new Promise((resolve, reject) => {
    resolve()
})
promise.then((res)=>{
    console.log('回调成功');
},(err)=>{
    console.log('回调失败');
})
  • 通过内置的Promise构造函数可以创建一个Promise对象,构造函数传入两个函数参数:resolve和reject。两个函数的作用是:在函数内手动调用resolve时,就说明回调成功;调用reject就说明失败。通常在promise中进行耗时的异步操作,响应是否成功,就根据判断调用对应的函数。
  • 调用Promise对象内置的then方法,传入两个函数,一个是成功的回调函数,一个是失败的回调函数。当在promise内部调用resolve函数时,就会回调then方法里的第一个函数,当调用reject就回调then的第二个函数。
  • promise相当于一个承诺,当承诺兑现(调用resolve)就会调用then中第一个函数,在回调函数中做处理。当承诺出现未知错误或异常(调用reject),就会调用then的第二个函数,提示错误。

Promise的状态

其实Promise对象用作异步任务的一个占位符,代表暂时还没有获得但在未来获得的值.

一共三种状态,完成和拒绝都是由等待状态转变的,一旦进入完成或拒绝就不能切换了。

  • 等待状态pending
  • 完成状态resolve
  • 拒绝状态reject

拒绝状态分两组:显式拒绝(直接调用reject)和隐式拒绝(抛出异常)

一个Promise实例:

function getJson(url) {
    return Promise((resolve, reject) => {
        const request = new XMLHttpRequest()
        request.open('GET', url)

        request.onload = function () {
            try {
                if (this.status == 200) {
                    resolve(JSON.parse(this.response))
                } else {
                    reject(this.status + ' ' + this.statusText)
                }
            } catch (e) {
                reject(e.message)
            }
        }

        request.onerror = () => {
            reject(this.status + ' ' + this.statusText)
        }

        request.send()
    })
}

getJson('http://localhost:3000').then((res) => {
    console.log('拿到的数据为:' + res);
}, (err) => {
    console.log('错误信息为:' + err);
})

Promise链

promise可以实现链式调用,每次then之后返回的都是一个promise对象,可以紧接着使用then继续处理接下来的任务,这样就实现了链式调用。如果then中使用了return,那么return的值也会被Promise.resolve()包装。

Promise.resolve(1)
  .then(res => {
    console.log(res) // => 1
    return 2 // 包装成 Promise.resolve(2)
  })
  .then(res => {
    console.log(res) // => 2
  })

嵌套任务处理

// 链式回调
getJson("url")
    .then(n => {getJson(n[0].url)})
    .then(m => {getJson(m[0].url)})
    .then(w => {getJson(w[0].url)})
    .catch((error => {console.log("异常错误!")}))
  • then方法会返回promise对象,所以连续调用then方法可以进行链式调用promise
  • 多个异步任务可能出现错误,只需要调用catch方法,并向其传入错误处理的回调函数。

并行处理任务

上述的链式调用主要处理多个异步任务之间存在依赖,如果要同时执行多个异步任务就用Promise的all方法。

Promise.all([getJson(url), getJson(url), getJson(url)]).then(res => {
    if (res[0] == 1 && res[1] == 1 && res[2] == 1) {
        console.log('请求成功');
    }
}).catch(err => {
    console.log('异常');
})
  • 使用all方法将多个请求任务封装成数组进行同步请求。
  • 返回的结果值会打包成一个数组,可以通过数组下标获取每个返回结果。
  • 只有全部请求成功才会进入成功的方法,否则就会调用catch
  • 与race方法不同,race只要其中一个返回成功,就会调用成功的方法。

如何实现一个Promise

根据Promise的执行顺序,手动实现一个

  • 先执行MyPromise构造函数
  • 注册then方法
  • 此时promise挂起,UI非堵塞,执行其他的同步代码
  • 执行回调函数
// 三种状态
const PENDING = "pending";
const RESOLVE = "resolve";
const REJECT = "reject";

// promise 函数
function MyPromise(fn){
    const that = this;           // 回调时用于保存正确的 this 对象
    that.state = PENDING;        // 初始化状态
    that.value = null;           // value 用于保存回调函数(resolve/reject 传递的参数值)
    that.resolvedCallbacks = []; // 用于保存 then 中的回调
    that.rejectedCallbacks = [];

    // resolve 和 reject 函数 
    function resolve(value) {
        if(that.state === PENDING){
            that.state = 'resolve';
            that.value = value;
            that.resolvedCallbacks.map(cb => cb(that.value));
        }
    }

    function reject(value) {
        if (that.state === PENDING) {
            that.state = 'reject'
            that.value = value
            that.rejectedCallbacks.map(cb => cb(that.value))
        }
    }

    // 实现如何执行 Promise 中传入的函数
    try {
        fn(resolve, reject)
    } catch (e) {
        reject(e)
    }
}

// 实现 then 函数
MyPromise.prototype.then = function(onResolved, onRejected) {
    const that = this;
    // 判断两个参数是否为函数类型(如果不是函数,就创建一个函数赋值给对应的参数)
    onResolved = typeof onResolved === 'function' ? onResolved : v => v;
    onRejected = typeof onRejected === 'function' ? onRejected : r => {throw r}

    // 判断当前的状态
    if (that.state === 'pending') {
        that.resolvedCallbacks.push(onResolved)
        that.rejectedCallbacks.push(onRejected)
    }
    if (that.state === 'resolve') {
        onResolved(that.value)
    }
    if (that.state === 'reject') {
        onRejected(that.value)
    }
}

new MyPromise((resolve, reject) => {
    setTimeout(() => {
        resolve(1)
    }, 0)
}).then(value => {
    console.log(value)
})

async和await

与Generator和Promise有什么区别。

ES7中的async/await就是Generator和Promise的语法糖,内部实现原理还是原来的,只不过在写法上有所改变,这些实现异步任务写起来更像是执行同步任务。

特点:

一个函数前加上async关键字,就将该函数返回一个Promise,async直接将返回值使用Promise.reslove()包裹。

await只能配套async使用,await内部实现了generator,await就是generator加上Promise的语法糖,且内部实现了自动执行generator。

优点

  • 内置执行器。async函数自带执行器,所以与普通函数一样,只需要一行
  • 更好的语义。async表示函数有异步操作,await表示紧跟在后面的表达式需要等待结果。
  • 更广的适用性。co函数库约定,yield命令后面只能是Thunk函数或Promise对象,而async函数的await命令后面,可以跟Promise对象和原始类型的值

缺点

因为await将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了await,就会导致性能降低。

await原理

就是将Generator函数和自动执行器包装在一个函数里面。

function spawn(genF) {
  return new Promise(function(resolve, reject) {
    var gen = genF();
    function step(nextF) {
      try {
        var next = nextF();
      } catch(e) {
        return reject(e); 
      }
      if(next.done) {
        return resolve(next.value);
      } 
      Promise.resolve(next.value).then(function(v) {
        step(function() { return gen.next(v); });      
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(undefined); });
  });
}

模块化

为什么使用模块化?

模块化解决了命名冲突问题,提高代码复用和可维护性。

好处:

  • 避免命名冲突
  • 更好分离,按需加载
  • 复用性,可维护性

方式1:函数

最起初,实现模块化的方式是使用函数进行封装。将不同功能的代码实现封装到不同的函数中。通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。

function a(){
  // 功能二
}
function b(){
  // 功能一
}

但容易发生命名冲突以及数据不安全。

方式2:立即执行函数

立即执行函数中的匿名函数中有独立的词法作用域,避免了外接访问此作用域的变量。通过函数作用域解决了命名冲突,污染全局作用域的问题。

(function (window) {
    let name = 'xiaolu'
    //暴露的接口来访问数据
    function a() {
        console.log(`name:${name}`);
    }
    //暴露接口
    window.myModule = { a }
})(window)

方式3:CommonJS

  • CommonJS的规范主要用在NodeJS中,为模块提供了四个接口:module、exports、require、global
  • CommonJS用同步的方式加载模块(服务器端),在浏览器端使用的是异步加载模块
// lib.js
var counter = 3;
function incCounter() {
    counter++;
}
// 对外暴露接口
module.exports = {
    counter: counter,
    incCounter: incCounter,
};
// 加载外部模块
var mod = require('./lib');

console.log(mod.counter);  // 3
mod.incCounter();
// 原始类型的值被缓存,所以就没有被改变(commonJS 不会随着执行而去模块随时调用)
console.log(mod.counter); // 3

加载机制

CommonJS的加载机制是,输入的是被输出的值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值

特点:

  • 所有代码都运行在模块作用域,不会污染全局作用域
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载就直接读取缓存结果。要想让模块再次运行,必须清楚缓存。
  • 模块加载的顺序,按照在代码出现的顺序。

方式4:AMD和CMD

AMD是RequireJS推广产出的,CMD是SeaJS推广产出的。类似的还有CommonJS

这些都是为了JavaScript的模块化开发。

区别

  1. 对于依赖的模块,AMD是提前执行,CMD是延迟执行
  2. CMD推崇依赖就近,AMD推崇依赖前置。
  3. AMD的API默认一个当多个用,CMD的API严格区分,职责单一。

方式5:ES6 Module

ES6实现的模块非常简单,用于浏览器和服务端。

import命令会被JavaScript引擎静态分析,在编译时,就引入模块,主要有export和import

  • export用于对外接口
  • import用于引入其他模块
// 指定指定的值暴露对外的接口
export let counter = 3;
export function incCounter() {
  counter++;
}

// 加载模块中的某个值
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
// ES6 模块不同的是,静态加载完毕之后,每执行到模块中的方法,就去模块内调用(外部的变量总是与模块进行绑定的),而且值不会被缓存。
console.log(counter); // 4

ES6模块和CommonJS模块的区别

  • CommonJS模块输出的是一个值的拷贝,ES6输出的是值的引用。
    • ES6模块是动态引用,不缓存值,模块内外是绑定的,而且是只读引用,不能修改值。ES6的js引擎对脚本静态分析的时候,遇到import就生成一个只读引用,当真正用到模块里面的值时,就去模块内部拿。
  • CommonJS模块是运行时加载,ES6模块是编译时加载输出接口。
    • 运行时加载:CommonJS模块就是对象;是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法
    • 编译时加载:ES6模块不是对象,而是通过export显式指定输出的代码,import时采用静态命令的形式,在import指定加载某个输出值,而不是加载整个模块。

小结

  • CommonJS主要用于服务端,加载模块是同步的,所以不适合浏览器环境,同步意味着阻塞
  • AMD是异步并且可以并行加载模块,不过其开发成本高,难
  • CMD与AMD相似
  • ES6在语言标准的层面实现了模块功能,完全可取代CommonJS