ES6 及 ESNext - 04

205 阅读5分钟

历史

version5.1 2011 年 6 月 version6 2015 年 6 月 版本最大的用处:polyfill 引入解析

ESNext -> ES的下一个版本

ES6及以后新增的常用API

let 和 const

面试题: 运行以下代码会输出什么?

for (var i = 0; i <= 3; i++) {
  setTimeout(function () {
    console.log(i);
  }, 100);
}
// 4 4 4 4

原因:

  1. var 定义的变量是全局的,所以全局只有一个变量i (作用域)
  2. setTimeout,下一轮时间循环的时候执行,即循环为同步代码,赋值 i 之后进入到循环内部遇到setTimeout 将其callback 推入异步队列,当 i = 3 时,推入最后一个 callback,再执行 i++ . 此时由于是全局变量, i = 4,console.log开始跑 4 遍 console.log(4) (异步、同步)

面试:如何改变一下让代码生成0123

for (let i = 0; i <= 3; i++) {
  setTimeout(function () {
    console.log(i);
  }, 100);
}
  1. let 引入了块级作用域的概念,创建setTimeout的时候,变量i只在作用域内生效。对于循环的每一次,引用的i都是不同的。

面试:如何不使用var,改变一下让代码生成0123

利用IIFE

for (var i = 0; i <= 3; i++) {
  (function (i) {
    setTimeout(function () {
      console.log(i);
    }, 100);
  })(i)
}
  1. 变量提升问题
console.log(i); // undefined
var i = 1;
console.log(i); // 1

console.log(i) // ReferenceError
let i = 1;
console.log(i) // 1
  1. const 常量 通常可以写url
const i = 1;
i = 2; // TypeError

箭头函数

  1. 箭头函数的this是在定义的位置决定的,而不同函数里的this是在使用的时候决定的。
// 普通函数 this
const teacher = {
  name: 'lubai',
  getName: function () {
    return this.name;  // == teacher.name
  }
}

console.log(teacher.getName()); // 'lubai'

// 箭头函数 this
const teacher = {
  name: 'lubai',
  getName: () => {
    return this.name;
  } // 在这里定义,此时的 this 为 window
}

console.log(teacher.getName()); // 'undefined' 这里相当于 window.name
  1. 简写
const arrowFn = (value) => Number(value);

const arrowFn = function (value) {
  return Number(value);
}

以箭头左右来区分,左边的是参数,多个参数有括号,单个可省略括号,没有参数直接写括号; 箭头右面为返回值; 如果箭头右面写 {} 编译器默认认为是个函数体;如果要返回对象,需要写一个小括号将对象包起来

const arrowFn = () => {}
// 相当于
const arrowFn = function () {
  return undefined;
}

// return 对象
const arrowFn = () => ({});
  1. 箭头函数不能被用作构造函数 构造函数:改变this指向,指到新实例。 箭头函数:this是在定义的时候决定的。
const arrowFn = () => ({
  this.name; // 这里this指向window
}) 

const instance = new arrowFn();
// 这里不能改变this指向到instance

class

  1. constructor: 如果希望是可以被实例化的类,需要一个constructor
  2. 静态属性 static
  3. getter setter 实现了 class 属性的监听
class Test {
  _name = '';
  constructor(name) {
    this.name = name;
  }

  static getFormatName() {
    return `${this._name} - 包装`;
  }

  get name() {
    return this._name;
  }

  set name(val) {
    console.log('检测到了name的赋值操作');
    this._name = val;
  }
}

const instance = new Test('lubai');

console.log(instance); 
// 检测到了name的赋值操作 --> 实例化的时候做了一个set name的赋值操作
// lubai --> constructor

// 静态属性调用
console.log(Test.getFormatName());
// Test - 包装

ES6 类的实例属性,相当于ES5 中的Test.prototype.name

模版字符串

// ES6 以前
const a = 'lubai';
console.log(a + 'cool!')

// 模版字符串
console.log(`${a} cool!`)
  1. 可以直接显示换行
const b = `第一行
第二行
第三行`

// ES5
const a = 'lubai\nlubai2\nlubai3';

面试题:写一个render函数,实现tempalte功能

const year = '2021';
const month = '10';
const day = '01';

const template = '${year}-${month}-${day}';
const context = { year, month, day } // { year: 2021, month: 10, day: 01 }

const str = render(template)(context) // 高阶函数:运行一个函数,返回另一个函数
// 相当于
/*
  function render(template) {
    return function(context) {

    }
  }
*/ 

// 输出
console.log(str) // 2021-10-01

// code here
function render(template) {
  return function (context) {
    return template.replace(/\$\{(.*?)\}/g, (match, key) => context[key]);
  }
}
//.*? 包括数字等任何字符
// () 为获得的参数作为后面回调的 key 传入

解构

  1. 数组的解构
// ES5
let arr = [1,2,3];
arr[0] = 1;
arr[1] = 2;
arr[2] = 3;

// ES6
let [a, b, c] = [1,2,3]
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3

let [a, b] = [{
  name: 'lubai'
}, {
  name: 'xxx'
}]
console.log(a); // { name: 'lubai'}
console.log(b); // { name: 'xxx'}
  1. 对象的解构
const {
  f1, f2
} = {
  f1: 'f1',
  f2: 'f2'
}

console.log(f1);
console.log(f2);
  1. 解构的原理
    针对可迭代对象Iterator,通过遍历按顺序获取对应的值进行赋值。
    3.1 Iterator 是什么?
    Iterator 是一种接口,interface,为各种不一样的数据提供统一的访问机制,任何数据解构只要有Iterator接口,就可以解构

    可以通过Symbol.iterator 判断是否有Interable 接口

    let num = 1; 
    let obj = {};
    
    console.log(num[Symbol.iterator]); // undefined
    console.log(obj[Symbol.iterator]); // undefined
    

    for of, 相当于一个遍历器,遍历数据的时候,去寻找Iterator,如果找不到,就会报错

    const obj = {
    name: 'lisa',
    age: 19
    }
    
    for (key of obj) {
      console.log(key);
    }
    // TypeError: obj is not iterable
    

    3.2 Iterator 有什么用呢?

    • 为各种不同的数据解构提供统一的接口。
    • 数据解构按顺序处理
    • for of 可以进行遍历

    实现一个生成可迭代对象函数
    可迭代对象中应有next函数,函数中每个以对象应该有两个属性:value,done

    function generateIterator (array) {
    let arrayIndex = 0;
      return {
        next: () => arrayIndex < array.length ? {
          value: array[arrayIndex++],
          done: false
        } : {
          value: undefined,
          done: true
        }
      }
    }
    
    const iterator = generateIterator([0,1,2]);
    console.log(iterator.next()); // { value: 0, done: false }
    console.log(iterator.next()); // { value: 1, done: false }
    console.log(iterator.next()); // { value: 2, done: false }
    console.log(iterator.next()); // { value: undefined, done: true }
    

    3.3 可迭代对象是什么? 是Iterator接口的实现。

    可迭代对象存在两种协议:可迭代协议,迭代器协议

    • 可迭代协议:对象必须实现iterator方法,即其对象或者原型链上必须有一个属性名为Symbol.iterator: () => 迭代器协议 (一个没有参数的函数,返回 迭代器协议)
    • 迭代器协议:定义了标准的方式来产生一个有限或无限序列值。其必须要有一个 next 方法,next方法返回对象 done value 属性

面试题:写一个函数实现对象可以使用for of 遍历 分析:

  1. 对象上必须有一个Symbol.iterator
  2. 其值为一个没有参数的函数,返回 next 方法
  3. next 方法必须返回一个对象,包含 done 和 value 属性
const obj = {
  count: 0,
  [Symbol.iterator]: () => {
    return {
      next: () => {
        obj.count++;
        if (obj.count <= 10) {
          return {
            value: obj.count,
            done: false
          } 
        } else {
          return {
            value: undefined,
            done: true
          }
        }
      }
    }
  }
};

for (let item of obj) {
  console.log(item);
}

遍历

  1. for in

遍历数组的时候,拿的是数组的下标; 遍历对象的时候,拿的是key,要拿value: obj[key]

let arr = [1,2,3]
let obj = {
  name: 'lubai',
  age: 18
}

for (let item in arr) {
  console.log(item);
}
// 0 1 2
for(let key in obj) {
  console.log(key);
}
// name age

for(let key in obj) {
  console.log(obj[key]);
}
// lubai 18

缺点:

  • 不仅会遍历当前对象,还会遍历原型链上的对象(某些第三方库更改了Object原型上的属性,导致也被遍历)
Object.prototype.protoName = 'ProtoName';

let obj = {
  name: 'lubai',
  age: 18
}

for(let key in obj) {
  console.log(obj[key]);
}
// lubai 18 ProtoName

优化:需要判断当前obj有自己的似有属性 obj.hasOwnProperty()

for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
        console.log(obj[key]);
    }
}
// lubai 18
  • 不合适遍历数组: 因为遍历出的都是数组的索引 forEach 不会被 break 中断
  1. for of
  • 可以被 break 中断
const arr = [{
  age: 11
}, {
  age: 6
}, {
  age: 100
}]

for (let {age} of arr) {
  if (age < 10) {
    break;
  }
  console.log(age);
}
// 11
  • 不适合遍历对象

Object

  1. Object.keys 拿到对象中的所有key组成一个数组
const obj = {
  a: 1,
  b: 2
};

console.log(Object.keys(obj));
// [ 'a', 'b' ]

面试:实现一个Object.keys? 注意要判断obj.hasOwnProperty

function getObjectKeys(obj) {
  const result = [];
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      result.push(key);
    }
  }
  return result;
}

const obj = {
  a: 1,
  b: 2
};

console.log(getObjectKeys(obj));
// [ 'a', 'b' ]
  1. Object.values 拿到对象中所有value的值,组成一个数组
const obj = {
  a: 1,
  b: 2
};

console.log(Object.values(obj));
// [1, 2]

实现一个Object.values?

function getObjectValues(obj) {
  const result = [];
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      result.push(obj[key]);
    }
  }
  return result;
}
console.log(getObjectValues(obj));
// [1,2]
  1. Object.entries 返回obj对象的键值对的数组
console.log(Object.entries(obj));

// [ [ 'a', 1 ], [ 'b', 2 ] ]

实现一个Object.entries?

function getObjectEntries(obj) {
  const result = [];
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      result.push([key, obj[key]]);
    }
  }
  return result;
}
console.log(getObjectEntries(obj));
// [ [ 'a', 1 ], [ 'b', 2 ] ]
  1. Object.getOwnProperNames 拿到对象自身的属性名,返回一个数组
Object.prototype.protoName = 'ProtoName';

const obj = {
  name: 'lubai',
  age: 19
}

console.log(Object.getOwnPropertyNames(obj));
// [ 'name', 'age' ]
  1. Object.getOwnProperDescriptor 拿到自身对象的描述符 Descriptor 是对象属性的描述符,是一个对象。

这个对象包含若干个属性:

  • configurable // 是否能够被配置 大开关:false的话,其他都无效
  • writable // 是否可写 只有在'use strict' 模式下才生效
  • enumerable // 可否枚举 for in /Object.keys 等
  1. Object.defineOwnProperty:
const obj = {};
let val = undefined;
Object.defineProperty(obj, 'a', {
    set: function (value) {
        console.log(`${value} - xxxx`);
        val = value;
    },
    get: function () {
        return val;
    },
    configurable: true,
})

obj.a = 111;

console.log(obj.a)
// 111 - xxxx 首先触发 setter
// 111 通过getter 拿到

面试问题:Vue3 与 Vue2 对于数据劫持有什么不同的实现? Proxy

const obj = new Proxy({}, {
    get: function (target, propKey, receiver) {
        console.log(`getting ${propKey}`);
        return target[propKey];
    },
    set: function (target, propKey, value, receiver) {
        console.log(`setting ${propKey}`);
        return Reflect.set(target, propKey, value, receiver);
    }
});

obj.something = 1;
console.log(obj.something);
// setting something
// getting something 1

Reflect 是什么?

  • 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法
  • 让Object操作都变成函数行为。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。
  • Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。
  1. Object.assign 浅拷贝 {...a, ...b} 将所有的对象进行合并,后面的属性覆盖前面的属性
const newObj = Object.assign({}, {name: 'xxx', age: 19}, {name: 'lubai'});

console.log(newObj);
// { name: 'lubai', age: 19 }

实现一个浅拷贝

function shallowClone (source) {
  let target = {};
  for (let i in source) {
    if (source.hasOwnProperty(i)) {
      target[i] = source[i];
    }
  }
  return target;
}

const a = {
  a: 'xxx',
  b: 2,
  c: {
    d: 'x'
  }
}

const b = shallowClone(a);

b.c.d = 'y';
console.log(a);

// { a: 'xxx', b: 2, c: { d: 'y' } }
  1. Object.create()

Object.create()方法创建一个新的对象,并以方法的第一个参数作为新对象的__proto__属性的值(根据已有的对象作为原型,创建新的对象。) Object.create()方法还有第二个可选参数,是一个对象,对象的每个属性都会作为新对象的自身属性,对象的属性值以descriptor(Object.getOwnPropertyDescriptor(obj, 'key'))的形式出现,且enumerable默认为false

const person = {
    isHuman: false,
    printIntroduction: function () {
        console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
    }
};
const me = Object.create(person);

me.name = "lubai";
me.isHuman = true;
me.printIntroduction();

console.log(person);

const myObject = Object.create(null)

传入第二个参数是怎么操作的呢?

function Person(name, sex) {
    this.name = name;
    this.sex = sex;
}

const b = Object.create(Person.prototype, {
    name: {
        value: 'coco',
        writable: true,
        configurable: true,
        enumerable: true,
    },
    sex: {
        enumerable: true,
        get: function () {
            return 'hello sex'
        },
        set: function (val) {
            console.log('set value:' + val)
        }
    }
})

console.log(b.name)
console.log(b.sex)

那么Object.create(null)的意义是什么呢? 平时创建一个对象Object.create({}) 或者 直接声明一个{} 不就够了?

Object.create(null)创建一个对象,但这个对象的原型链为null,即Fn.prototype = null

const b = Object.create(null) // 返回纯{}对象,无prototype

b // {}
b.__proto__ // undefined
b.toString() // throw error

所以当你要创建一个非常干净的对象, 没有任何原型链上的属性, 那么就使用Object.create(null). for in 遍历的时候也不需要考虑原型链属性了.

  1. Object.is 方法判断两个值是否为同一个值
const a = {
    name: 1
};
const b = a;
console.log(Object.is(a, b))

console.log(Object.is({}, {}))

Promise

Promise.allSettled 返回所有的promise的状态和结果

function PromiseAllSettled(promiseArray) {
    return new Promise(function (resolve, reject) {
        //判断参数类型
        if (!Array.isArray(promiseArray)) {
            return reject(new TypeError('arguments muse be an array'))
        }
        let counter = 0;
        const promiseNum = promiseArray.length;
        const resolvedArray = [];
        for (let i = 0; i < promiseNum; i++) {
            Promise.resolve(promiseArray[i])
                .then((value) => {
                    resolvedArray[i] = {
                        status: 'fulfilled',
                        value
                    };

                })
                .catch(reason => {
                    resolvedArray[i] = {
                        status: 'rejected',
                        reason
                    };
                })
                .finally(() => {
                    counter++;
                    if (counter == promiseNum) {
                        resolve(resolvedArray)
                    }
                })
        }
    })
}

数组

  1. Array.flat(arr, 指定深度) 将多围数组打平为规定纬度数组
const arr1 = [1,2,[3,4]]

console.log(arr1.flat())
// [ 1, 2, 3, 4 ]

const arr1 = [1,2,[3,4,[5]]]
console.log(arr1.flat(1));
// [ 1, 2, 3, 4, [ 5 ] ]

打平所有层级:Array.flat(Infinity) 实现Array.flat()

const arr1 = [1,2,[3,4,[5]]]

function flatDeep(arr, d = 1) {
  if (d > 0) {
    return arr.reduce((res, val) => {
      if (Array.isArray(val)) {
        res = res.concat(flatDeep(val, d - 1));
      } else {
        res = res.concat(val);
      }
      return res;
    }, []);
  } else {
    return arr.slice(); // 不改变原数组,浅拷贝
  }
}
console.log(flatDeep(arr1, Infinity))
// [ 1, 2, 3, 4, 5 ]

Array.reduce(遍历函数, 存结果数组)

  1. Array.includes 判断元素是存在
const arr = [1,2,3,4,5]

console.log(arr.includes(6)); // false;
  1. Array.from(可迭代对象,map)

Array.map() 遍历整个数组,依次对数组元素执行回调函数并用这些返回值创建一个新的数组

const arr1 = [1,2,3,4,5]

console.log(Array.from(arr1, x => x + 1))
// [ 2, 3, 4, 5, 6 ]

面试题:如何把一个类数组(arguments)转成真数组??

  1. [...arguments]
  2. Array.from(arguments)
  3. Array.prototype.slice.call(arguments) // 相当于arguments.slice()

babel 编译工具的使用

astexplorer.net 查看AST

  1. 解析 解析代码 输出AST
  • 词法分析
  • 语法分析
  1. 转换 接收AST对其进行遍历,可以对节点进行添加、更新、移除等操作

  2. 生成 把转换过的AST生成为字符串形式的代码,并且创建source map (源码)

一个插件就是一个函数 plugin.js

const template = require('babel-template');

const temp = template("var b = 1")

module.exports = function ({
    types: t
}) {
    // 插件内容
    return {
        visitor: {
            // 接收两个参数path, state
            VariableDeclaration(path, state) {
                // 找到AST节点
                const node = path.node;
                // 判断节点类型 是否是变量节点, 申明方式是const
                if (t.isVariableDeclaration(node, {
                        kind: "const"
                    })) {
                    // 将const 声明编译为let
                    node.kind = "let";
                    // var b = 1 的AST节点
                    const insertNode = temp();
                    // 插入一行代码var b = 1
                    path.insertBefore(insertNode);
                }
            }
        }
    }
}

使用插件 babel.js

const myPlugin = require('./plugin')
const babel = require('@babel/core');
const content = 'const name = lubai';
// 通过你编写的插件输出的代码
const {
    code
} = babel.transform(content, {
    plugins: [
        myPlugin
    ]
});

console.log(code);

面试题:webpack

  1. webpack的运行原理?
  2. wd 是怎么运行的?webpack dev server? 这些内容存在哪里?发开环境如何读取?
  3. 热重载怎么实现的?
  4. split chunk 是怎么实现的?