ES5和ES6

6.1 es6中箭头函数

参考答案:

  1. 基本语法

    ES6中允许使用箭头=>来定义箭头函数,具体语法,我们来看一个简单的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 箭头函数
    let fun = (name) => {
    // 函数体
    return `Hello ${name} !`;
    };

    // 等同于
    let fun = function (name) {
    // 函数体
    return `Hello ${name} !`;
    };

    可以看出,定义箭头函在数语法上要比普通函数简洁得多。箭头函数省去了function关键字,采用箭头=>来定义函数。函数的参数放在=>前面的括号中,函数体跟在=>后的花括号中。

    关于箭头函数的参数:

    如果箭头函数没有参数,直接写一个空括号即可。

    如果箭头函数的参数只有一个,也可以省去包裹参数的括号。

    如果箭头函数有多个参数,将参数依次用逗号(,)分隔,包裹在括号中即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 没有参数
    let fun1 = () => {
    console.log(111);
    };

    // 只有一个参数,可以省去参数括号
    let fun2 = name => {
    console.log(`Hello ${name} !`)
    };

    // 有多个参数
    let fun3 = (val1, val2, val3) => {
    return [val1, val2, val3];
    };

    关于箭头函数的函数体:

    如果箭头函数的函数体只有一句代码,就是简单返回某个变量或者返回一个简单的JS表达式,可以省去函数体的大括号{ }。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    let f = val => val;
    // 等同于
    let f = function (val) { return val };

    let sum = (num1, num2) => num1 + num2;
    // 等同于
    let sum = function(num1, num2) {
    return num1 + num2;
    };

    如果箭头函数的函数体只有一句代码,就是返回一个对象,可以像下面这样写:

    1
    2
    3
    4
    5
    6
    // 用小括号包裹要返回的对象,不报错
    let getTempItem = id => ({ id: id, name: "Temp" });

    // 但绝不能这样写,会报错。
    // 因为对象的大括号会被解释为函数体的大括号
    let getTempItem = id => { id: id, name: "Temp" };

    如果箭头函数的函数体只有一条语句并且不需要返回值(最常见是调用一个函数),可以给这条语句前面加一个void关键字

    1
    let fn = () => void doesNotReturn();

    箭头函数最常见的用处就是简化回调函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 例子一
    // 正常函数写法
    [1,2,3].map(function (x) {
    return x * x;
    });

    // 箭头函数写法
    [1,2,3].map(x => x * x);

    // 例子二
    // 正常函数写法
    var result = [2, 5, 1, 4, 3].sort(function (a, b) {
    return a - b;
    });

    // 箭头函数写法
    var result = [2, 5, 1, 4, 3].sort((a, b) => a - b);
  2. 箭头函数与普通函数的区别

    2.1 语法更加简洁、清晰

    从上面的基本语法示例中可以看出,箭头函数的定义要比普通函数定义简洁、清晰得多,很快捷。

    2.2 箭头函数不会创建自己的this

    箭头函数没有自己的this,它会捕获自己在定义时(注意,是定义时,不是调用时)所处的外层执行环境的this,并继承这个this值。所以,箭头函数中this的指向在它被定义的时候就已经确定了,之后永远不会改变。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    var id = 'Global';

    function fun1() {
    // setTimeout中使用普通函数
    setTimeout(function(){
    console.log(this.id);
    }, 2000);
    }

    function fun2() {
    // setTimeout中使用箭头函数
    setTimeout(() => {
    console.log(this.id);
    }, 2000)
    }

    fun1.call({id: 'Obj'}); // 'Global'

    fun2.call({id: 'Obj'}); // 'Obj'

    上面这个例子,函数fun1中的setTimeout中使用普通函数,2秒后函数执行时,这时函数其实是在全局作用域执行的,所以this指向Window对象,this.id就指向全局变量id,所以输出’Global’。 但是函数fun2中的setTimeout中使用的是箭头函数,这个箭头函数的this在定义时就确定了,它继承了它外层fun2的执行环境中的this,而fun2调用时this被call方法改变到了对象{id: ‘Obj’}中,所以输出’Obj’

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var id = 'GLOBAL';
    var obj = {
    id: 'OBJ',
    a: function(){
    console.log(this.id);
    },
    b: () => {
    console.log(this.id);
    }
    };

    obj.a(); // 'OBJ'
    obj.b(); // 'GLOBAL'

    上面这个例子,对象obj的方法a使用普通函数定义的,普通函数作为对象的方法调用时,this指向它所属的对象。所以,this.id就是obj.id,所以输出’OBJ’。 但是方法b是使用箭头函数定义的,箭头函数中的this实际是继承的它定义时所处的全局执行环境中的this,所以指向Window对象,所以输出’GLOBAL’。(这里要注意,定义对象的大括号{}是无法形成一个单独的执行环境的,它依旧是处于全局执行环境中!!

  3. 箭头函数继承而来的this指向永远不变(重要!!深入理解!!)

    上面的例子,就完全可以说明箭头函数继承而来的this指向永远不变。对象obj的方法b是使用箭头函数定义的,这个函数中的this就永远指向它定义时所处的全局执行环境中的this,即便这个函数是作为对象obj的方法调用,this依旧指向Window对象。

  4. .call()/.apply()/.bind()无法改变箭头函数中this的指向

    .call()/.apply()/.bind()方法可以用来动态修改函数执行时this的指向,但由于箭头函数的this定义时就已经确定且永远不会改变。所以使用这些方法永远也改变不了箭头函数this的指向,虽然这么做代码不会报错。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var id = 'Global';
    // 箭头函数定义在全局作用域
    let fun1 = () => {
    console.log(this.id)
    };

    fun1(); // 'Global'
    // this的指向不会改变,永远指向Window对象
    fun1.call({id: 'Obj'}); // 'Global'
    fun1.apply({id: 'Obj'}); // 'Global'
    fun1.bind({id: 'Obj'})(); // 'Global'
  5. 箭头函数不能作为构造函数使用

    我们先了解一下构造函数的new都做了些什么?简单来说,分为四步: ① JS内部首先会先生成一个对象; ② 再把函数中的this指向该对象; ③ 然后执行构造函数中的语句; ④ 最终返回该对象实例。

    但是因为箭头函数没有自己的this,它的this其实是继承了外层执行环境中的this,且this指向永远不会随在哪里调用、被谁调用而改变,所以箭头函数不能作为构造函数使用,或者说构造函数不能定义成箭头函数,否则用new调用时会报错

    1
    2
    3
    4
    5
    6
    7
    let Fun = (name, age) => {
    this.name = name;
    this.age = age;
    };

    // 报错
    let p = new Fun('cao', 24);
  6. 箭头函数没有自己的arguments

    箭头函数没有自己的arguments对象。在箭头函数中访问arguments实际上获得的是外层局部(函数)执行环境中的值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 例子一
    let fun = (val) => {
    console.log(val); // 111
    // 下面一行会报错
    // Uncaught ReferenceError: arguments is not defined
    // 因为外层全局环境没有arguments对象
    console.log(arguments);
    };
    fun(111);

    // 例子二
    function outer(val1, val2) {
    let argOut = arguments;
    console.log(argOut); // ①
    let fun = () => {
    let argIn = arguments;
    console.log(argIn); // ②
    console.log(argOut === argIn); // ③
    };
    fun();
    }
    outer(111, 222);

    上面例子二,①②③处的输出结果如下:

    img

    很明显,普通函数outer内部的箭头函数fun中的arguments对象,其实是沿作用域链向上访问的外层outer函数的arguments对象。

    可以在箭头函数中使用rest参数代替arguments对象,来访问箭头函数的参数列表!!

  7. 箭头函数没有原型prototype

    1
    2
    3
    4
    let sayHi = () => {
    console.log('Hello World !')
    };
    console.log(sayHi.prototype); // undefined
  8. 箭头函数不能用作Generator函数,不能使用yeild关键字

6.2 ES6新特性

参考答案:

  1. 变量和作用域

    1.1 let 、const、 块级作用域和变量声明

    let声明的变量只在所在块中生效

    let声明的变量可以解决var与for循环结合使用产生的无法取得最新变量值的问题(以往都需要通过闭包来解决这个问题);

    let声明的变量不存在变量提升(从undefined->ReferenceError,其实也是一种暂时性死区)、会造成变量暂时性死区(在声明let变量之前都不能用它)、也不允许重复声明;

    const声明的变量行为与let类似,只是多了两点更强的约束:1.声明时必须赋值;2.声明的变量内存地址不可变,需要注意的是:对于用const声明基本类型,值就保存在内存地址之中,意味着变量不可重新赋值;对于用const声明的对象,对象内容还是可以更改的,只是不能改变其指向。(冻结对象应该用Object.freeze())

    1.2 解构赋值(按照一定的结构解析出来进行赋值)

    解构赋值的使用场景:变量快捷赋值、提取数据、函数参数定义和默认值、遍历某结构

  2. 原生对象的方法扩展

    2.1 String

    加强了对unicode的支持、支持字符串遍历(后面有讲到实际上是部署了iterator接口)、repeat()等方法的支持、模板字符串

    2.2 RegExp

    构造函数第一个参数是正则表达式,指定第二个参数不再报错、u修饰符、y修饰符、s修饰符

    2.3 Number

    二进制和八进制新写法、新方法parseInt()、Number.EPSILON极小常量、安全整数、Math新方法

    2.4 Function

    函数参数默认值、rest参数、函数内部严格模式、函数的name属性、箭头函数

    2.5 Array

    扩展运算符…

    2.6 Object 和 Symbol

    (1) Object对象

    支持简写:同名属性K-V可以省略一个、函数声明可以省略function;支持属性名表达式、函数名表达 式。(注意:以上2个——表达式和简写不能同时使用)。

    对象的方法的name属性返回方法名,但有几个例外情况要小心。新增了Object方法

    Object.is()——用于解决==和===的部分兼容问题

    Object.assign()——将src的所有可枚举对象属性复制到dest对象上(浅复制)

    Object.setPrototypeOf()、Object.getPrototypeOf() (Object.__proto属性)

    Object.entries()、Object.keys()、Object.values()

    ES6中5种遍历对象属性的方法

    for-in——自身和继承的可枚举属性(除Symbol)

    Object.keys()——自身非继承的可枚举属性(除Symbol)

    Object.getOwnPropertyNames()——自身所有属性键名(包括不可枚举、除Symbol)

    Object.getOwnPropertySymbols()——自身的所有 Symbol 属性的键名

    Reflect.ownKeys()——自身的所有键名

    (2)Symbol类型

    ES5以前,对象属性都只能是字符串,容易造成重命名导致的冲突。Symbol提供了一种机制,可以保存 属性名是独一无二的。Symbol类型的使用注意:1)创建是调用函数,而不是new关键字 2)Symbol类 型的属性不会被for-*、Object.keys()、Object.getPropertyNames()返回,可以用后面两种方法遍历。

  3. 数据结构Set和Map

    Set是一种类似数组的数据结构,区别在于其存储的成员都是不重复的,由此带来了它的一个应用就是:去重。Set通过new关键字实例化,入参可以是数组or类数组的对象。

    值得注意的是:在Set中,只能存储一个NaN,这说明在Set数据结构中,NaN等于NaN

    Set实例的方法:操作方法add()、delete()、has()和clear();遍历方法:keys()、values()、entries()和forEach();扩展运算符…、数组方法map()、filter()方法也可以用于Set结构。由此它可以很方便的实现数组的交、并、差集。

    WeakSet类似于Set,主要区别在于1.成员只能是对象类型;2.对象都是弱引用(如果其他对象都不再引用该对象,垃圾回收机制会自动回收该对象所占的内存,不可预测何时会发生,故WeakSet不可被遍历)

    JavaScript对象Object都是键值K-V对的集合,但K取值只能是字符串和Symbol,Map也是K-V的集合,然而其K可以取任意类型。如果需要键值对的集合,Map比Object更适合。Map通过new关键字实例化。

    Map实例的方法:set()、get()、has()、delete()和clear();遍历方法同Set。

    Map与其它数据结构的互相转换:Map <—> 数组| Map <—> 对象| Map <—> JSON。

    WeakMap类似于Map,主要区别在于:1.只接受对象作为键名;2.键名所指向的对象不计入垃圾回收机制

  4. 元编程相关Proxy和Reflect

    4.1 Proxy

    对目标对象加一层“拦截”(“代理”),外界对对象的访问、修改都必须先通过这层拦截层。因而它提供了 一个机制可以对外界的访问进行过滤和改写。

    用法:var proxy = new Proxy(p1,p2); p1是要被代理的目标对象,p2是配置对象。

    值得注意的是:Proxy不是对目标对象透明的代理——即使不做任何拦截的情况下无法保证代理对象与目 标对象行为的完全一致。(主要原因在于代理时,目标对象内部的this会指向代理对象)

    4.2 Reflect

    与Proxy一样是ES6为语言层面的用于操作对象提供的新API,目前它所拥有的对象方法与Proxy对象一一对 应,引入目的:1.将Object对象上一些属于语言内部的方法放在Reflect上(目前都可以放)2.修改Object对 象上某些方法的返回值,使得更加合理化(健壮)3.让Object对象的操作从命令式完全转化为函数式

  5. 异步编程Promise、Generator和Async

    在JavaScript的世界里,对于异步编程存在如下几种方案:1.回调函数;2.事件触发监听;3.发布订阅者模式;4.Promise。首先介绍Promise,然后介绍ES6提供的生成器函数,async函数。

    Promise来源于社区,代表一个对象,它代表异步操作未来的一个结果(承诺)。它总共有三个状态,pending\fulfilled\rejected。另外,它的状态翻转路径只有两个:pending->fulfilled or pending->rejected,一旦状态翻转,就不可变了。它支持链式调用,支持错误传递,支持以同步代码的方式写异步操作。

    Promise是一个对象,创建此对象实例的方法如下(可以理解resolve和reject是已返回的承诺对象未来回调函数的占位)

    Generator函数是ES6提供的异步编程解决方案。对于Generator函数,可以将它理解为一个状态机,封装了多个内部状态;此外它还是一个遍历器生成函数,这个函数可以遍历出状态机的所有状态。

    函数特征:关键字function与函数名之间有*,函数体内部yeild关键字。

    生成器函数与普通函数的区别:函数调用后不执行,而是返回一个指针对象(遍历器对象)。调用对象的next()方法,执行一段yield逻辑。故函数的分段执行的,yield是暂停执行的标志,next()可以恢复执行

    yield与return的区别:yield有记忆功能,return没有;一个函数可以多次执行yeild,但只会return一次

    async函数是Generator函数的语法糖,它进行了改进:1.自带执行器;2.返回值是Promise;

    三家对比:使用Promise的异步代码存在大量自有API的调用,操作本身的语义夹杂其中,不是很清晰;Generator函数实现的异步代码语义比Promise清晰,但需要一个执行器;async函数的写法最简洁、符合语义,不需要执行器

  6. 语言层面类、模块的支持

    6.1 class

    从 ES6 开始,JavaScript 提供了 class 关键字来定义类,尽管,这样的方案仍然是基于原型运行时系统的模拟,大部分功能ES5可以实现。

    构造函数的prototype属性在 ES6 的“类”上面继续存在。事实上,类中所有方法都定义在类的prototype属性上面(因而也是不可枚举的)。

    constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。(默认构造函数);constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象。

    注意区别:类必须使用new调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行。

    类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

    实例属性除了定义在constructor()方法里面的this上面,也可以定义在类的最顶层。

    私有属性和方法如何实现?1.命名上加以区别 2.将私有方法移出模块,利用公有方法调用;3.Symbol属性上(都不完美)

    6.2 module

    在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定。

    编译时加载VS运行时加载——对象VS代码

    模块命令:export和import;一个文件即为一个模块,除非导入否则外部无法读取模块属性;

    export支持:变量、函数和类

    export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的import命令也是如此。

    输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。

    使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。

    模块之间也可以继承。

  7. JS中对象分类、及其它原生对象

  8. Iterator

    ES6之前在JS中只有Array和对象可以表示“集合”这种数据结构,ES6中增加了:Set和Map。由此,四种之间互相组合又可以定义新的数据结构。这些新定义的数据结构如何访问呢?遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作。遍历器对象本质上是一个指针对象。

    只要为某个数据结构部署 了Iterator接口,则可以称此数据结构是可遍历的。iterator属性部署在Symbol上。如下对象默认部署了Iterator结口:Array Set Map String等。部署iterator结构的要点:1)在Symbol.iterator上部署;2)必须包含next()函数。默认调用iterator接口的场景:解构赋值、…扩展运算符、yeild* 。for-of循环内部调用的即是调用数据机构内部的Symbol.iterator方法。

    for-in和for-of循环

    for-in用于遍历对象属性,对象自身和继承的可枚举属性(不可遍历Symbol属性)

    for-of循环是一种遍历所有数据机构的统一方法。实现原理是数据结构上部署的Symbol.iterator属性。

6.3 ES6 与 ES5 继承的区别

参考答案:

ES6 中有类 class 的概念,类 class 的继承是通过 extends 来实现的,ES5 中是通过设置构造函数的 prototype 属性,来实现继承的。

ES6 与 ES5 中的继承有 2 个区别,第一个是,ES6 中子类会继承父类的属性,第二个区别是,super() 与 A.call(this) 是不同的,在继承原生构造函数的情况下,体现得很明显,ES6 中的子类实例可以继承原生构造函数实例的内部属性,而在 ES5 中做不到。

解析:

下面通过 3 个 demo,来分析它们之间的区别。

  1. ES5 继承

直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function A() {
this.a = 'hello';
}

function B() {
A.call(this);
this.b = 'world';
}

B.prototype = Object.create(A.prototype, {
constructor: { value: B, writable: true, configurable: true }
});

let b = new B();

代码中,构造函数 B 继承构造函数 A,首先让构造函数 B 的 prototype 对象中的 proto 属性指向构造函数 A 的 prototype 对象,并且将构造函数 B 的 prototype 对象的 constructor 属性赋值为构造函数 B,让构造函数 B 的实例继承构造函数 A 的原型对象上的属性,然后在构造函数 B 内部的首行写上 A.call(this),让构造函数 B 的实例继承构造函数 A 的实例属性。在 ES5 中实现两个构造函数之间的继承,只需要做这两步即可。下面六幅图分别是,实例 b 的原型链及验证图,构造函数 B 的原型链及验证图,构造函数 A 的原型链及验证图。

实例 b 的原型链如下图:

img实例 b 的原型链验证图:

img构造函数 B 的原型链图下图:

img构造函数 B 的原型链验证图图:

img构造函数 A 的原型链图下图:

img构造函数 B 的原型链验证图图:

img从上面 6 幅图可知,构造函数 B 的实例 b 继承了构造函数 A 的实例属性,继承了构造函数 A 的原型对象上的属性,继承了构造函数 Object 的原型对象上的属性。构造函数 B 是构造函数 Function 的实例,继承了构造函数 Function 的原型对象上的属性,继承了构造函数 Object 的原型对象上的属性。 构造函数 A 是构造函数 Function 的实例,继承了构造函数 Function 的原型对象上的属性,继承了构造函数 Object 的原型对象上的属性。可看出,构造函数 A 与 构造函数 B 并没有继承关系,即构造函数 B 没有继承构造函数 A 上面的属性,在 ES6 中,用 extends 实现两个类的继承,两个类之间是有继承关系的,即子类继承了父类的方法,这是 ES6 与 ES5 继承的第一点区别,下面通过 ES6 的继承来说明这一点。

  1. ES6 继承

直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A {
constructor() {
this.a = 'hello';
}
}

class B extends A {
constructor() {
super();
this.b = 'world';
}
}

let b = new B();

代码中,类 B 通过 extends 关键字继承类 A 的属性及其原型对象上的属性,通过在类 B 的 constructor 函数中执行 super() 函数,让类 B 的实例继承类 A 的实例属性,super() 的作用类似构造函数 B 中的 A.call(this),但它们是有区别的,这是 ES6 与 ES5 继承的第二点区别,这个区别会在文章的最后说明。在 ES6 中,两个类之间的继承就是通过 extends 和 super 两个关键字实现的。下面四幅图分别是,实例 b 的原型链及验证图,类 B 的原型链及验证图。

实例 b 的原型链如下图:

img实例 b 的原型链验证图:

img类 B 的原型链图下图:

img类 B 的原型链验证图图:

img通过上面 4 幅图可知,在 ES6 与 ES5 中,类 B 的实例 b 的原型链与构造函数 B 的实例 b 的原型链是相同的,但是在 ES6 中类 B 继承了类 A 的属性,在 ES5 中,构造函数 B 没有继承构造函数 A 的属性,这是 ES6 与 ES5 继承的第一个区别。

  1. super() 与 A.call(this) 的区别

在 ES5 中,构造函数 B 的实例继承构造函数 A 的实例属性是通过 A.call(this) 来实现的,在 ES6 中,类 B 的实例继承类 A 的实例属性,是通过 super() 实现的。在不是继承原生构造函数的情况下,A.call(this) 与 super() 在功能上是没有区别的,用 babel 在线转换 将类的继承转换成 ES5 语法,babel 也是通过 A.call(this) 来模拟实现 super() 的。但是在继承原生构造函数的情况下,A.call(this) 与 super() 在功能上是有区别的,ES5 中 A.call(this) 中的 this 是构造函数 B 的实例,也就是在实现实例属性继承上,ES5 是先创造构造函数 B 的实例,然后在让这个实例通过 A.call(this) 实现实例属性继承,在 ES6 中,是先新建父类的实例对象this,然后再用子类的构造函数修饰 this,使得父类的所有行为都可以继承。下面通过 2 段代码说明这个问题。

代码 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function ArrFun() {
Array.call(this);
}

ArrFun.prototype = Object.create(Array.prototype, {
constructor: {
value: ArrFun,
writable: true,
configurable: true
}
});

var colors = new ArrFun();
colors[0] = "blue";
colors.length;

这段代码的思路就是,让构造函数 MyArray 继承原生构造函数 Array,然后验证 MyArray 的实例是否具有 Array 实例的特性。

代码 1 执行结果如下图:

img从结果可以看出,MyArray 的实例并不具有 Array 实例的特性,之所以会发生这种情况,是因为 MyArray 的实例无法获得原生构造函数 Array 实例的内部属性,通过 Array.call(this) 也不行。

代码 2:

1
2
3
4
5
6
7
8
9
class ArrFun extends Array {
constructor() {
super();
}
}

var arr = new ArrFun();
arr[0] = 12;
arr.length;

代码 2 执行结果如下图:

img

从结果可以看出,通过 super(),MyArray 的实例具有 Array 实例的特性。

6.4 哪些类型能被扩展操作符…扩展

参考答案:

适用类型:数组、对象、字符串。

复杂数据类型都可以,当要转化为可迭代数据结构时可设置对象的迭代器对扩展运算符扩展出来的值进行操作。

基础数据只有string可以使用扩展运算符,number,boolean,null,undefined无效

6.5 事件扩展符用过吗(…),什么场景下

参考答案:

扩展运算符的应用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// 1、函数调用
function add(x, y) {
return x + y;
}
add(...[4, 38]);

function f(v, w, x, y, z) { }
f(-1, ...[0, 1], 2, ...[3]);
// 123456789

//2.往数组里push多个元素
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
arr1.push(...arr2);
console.log(arr1); //[0,1,2,3,4,5]
//123456

//3.替代函数的apply方法
function f(x, y, z) { }
var args = [0, 1, 2];
f.apply(null, args); //ES5 的写法
f(...args); //ES6的写法
// 123456

//4.求一个数组的最大数简化
Math.max.apply(null, [14, 3, 77]) //ES5 的写法
Math.max(...[14, 3, 77]) //ES6 的写法,等同于Math.max(14, 3, 77)
//1234

//5.扩展运算符后面可以放表达式
const arr = [...(5 > 0 ? ['a'] : []),'b'];
console.log(arr); //['a','b']
//1234

//6.与解构赋值结合,用于生成数组
const a1 = [1, 2];
const a2 = [...a1]; //写法1
const [...a2] = a1; //写法2
const [first, ...rest] = [1, 2, 3, 4, 5];
first //1
rest //[2, 3, 4, 5]

const [first, ...rest] = [];
first //undefined
rest //[]

const [first, ...rest] = ["foo"];
first //"foo"
rest //[]
//1234567891011121314151617

//7.合并数组
[...arr1, ...arr2, ...arr3] //[ 'a', 'b', 'c', 'd', 'e' ]
123

//8.数组的克隆——————————————————————特别注意
var arr1 = [0, 1, 2];
var arr2 = [...arr1];
arr1[0]=100;
console.log(arr2); //[0, 1, 2]

/* 乍一看,arr2与arr1不共用引用地址,arr2不随着arr1变化,接着往下看 */

var arr1 = [0, [1,11,111], 2];
var arr2 = [...arr1];
arr1[1][0]=100;
console.log(arr2); //[0, [100,11,111], 2]

6.6 让不同的浏览器兼容ES6的方法

参考答案:

针对 ES6 的兼容性问题,很多团队为此开发出了多种语法解析转换工具,把我们写的 ES6 语法转换成 ES5,相当于在 ES6 和浏览器之间做了一个翻译官。比较通用的工具方案有 babel,jsx,traceur,es6-shim 等。

面试官:ES6中数组新增了哪些扩展?

一、扩展运算符的应用

ES6通过扩展元素符...,好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列

1
2
3
4
5
6
7
8
console.log(...[1, 2, 3])
// 1 2 3

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

[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]

主要用于函数调用的时候,将一个数组变为参数序列

1
2
3
4
5
6
7
8
9
10
function push(array, ...items) {
array.push(...items);
}

function add(x, y) {
return x + y;
}

const numbers = [4, 38];
add(...numbers) // 42

可以将某些数据结构转为数组

1
[...document.querySelectorAll('div')]

能够更简单实现数组复制

1
2
3
const a1 = [1, 2];
const [...a2] = a1;
// [1,2]

数组的合并也更为简洁了

1
2
3
4
5
const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]

注意:通过扩展运算符实现的是浅拷贝,修改了引用指向的值,会同步反映到新数组

下面看个例子就清楚多了

1
2
3
4
5
const arr1 = ['a', 'b',[1,2]];
const arr2 = ['c'];
const arr3 = [...arr1,...arr2]
arr[1][0] = 9999 // 修改arr1里面数组成员值
console.log(arr[3]) // 影响到arr3,['a','b',[9999,2],'c']

扩展运算符可以与解构赋值结合起来,用于生成数组

1
2
3
4
5
6
7
8
9
10
11
const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest // [2, 3, 4, 5]

const [first, ...rest] = [];
first // undefined
rest // []

const [first, ...rest] = ["foo"];
first // "foo"
rest // []

如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错

1
2
3
4
5
const [...butLast, last] = [1, 2, 3, 4, 5];
// 报错

const [first, ...middle, last] = [1, 2, 3, 4, 5];
// 报错

可以将字符串转为真正的数组

1
2
[...'hello']
// [ "h", "e", "l", "l", "o" ]

定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组

1
2
3
4
5
6
7
8
9
10
let nodeList = document.querySelectorAll('div');
let array = [...nodeList];

let map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);

let arr = [...map.keys()]; // [1, 2, 3]

如果对没有 Iterator 接口的对象,使用扩展运算符,将会报错

1
2
const obj = {a: 1, b: 2};
let arr = [...obj]; // TypeError: Cannot spread non-iterable object

二、构造函数新增的方法

关于构造函数,数组新增的方法有如下:

  • Array.from()
  • Array.of()

Array.from()

将两类对象转为真正的数组:类似数组的对象和可遍历(iterable)的对象(包括 ES6 新增的数据结构 SetMap

1
2
3
4
5
6
7
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']

还可以接受第二个参数,用来对每个元素进行处理,将处理后的值放入返回的数组

1
2
Array.from([1, 2, 3], (x) => x * x)
// [1, 4, 9]

Array.of()

用于将一组值,转换为数组

1
Array.of(3, 11, 8) // [3,11,8]

没有参数的时候,返回一个空数组

当参数只有一个的时候,实际上是指定数组的长度

参数个数不少于 2 个时,Array()才会返回由参数组成的新数组

1
2
3
Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]

三、实例对象新增的方法

关于数组实例对象新增的方法有如下:

  • copyWithin()
  • find()、findIndex()
  • fill()
  • entries(),keys(),values()
  • includes()
  • flat(),flatMap()

copyWithin()

将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组

参数如下:

  • target(必需):从该位置开始替换数据。如果为负值,表示倒数。
  • start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算。
  • end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。
1
2
[1, 2, 3, 4, 5].copyWithin(0, 3) // 将从 3 号位直到数组结束的成员(4 和 5),复制到从 0 号位开始的位置,结果覆盖了原来的 1 和 2
// [4, 5, 3, 4, 5]

find()、findIndex()

find()用于找出第一个符合条件的数组成员

参数是一个回调函数,接受三个参数依次为当前的值、当前的位置和原数组

1
2
3
[1, 5, 10, 15].find(function(value, index, arr) {
return value > 9;
}) // 10

findIndex返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1

1
2
3
[1, 5, 10, 15].findIndex(function(value, index, arr) {
return value > 9;
}) // 2

这两个方法都可以接受第二个参数,用来绑定回调函数的this对象。

1
2
3
4
5
function f(v){
return v > this.age;
}
let person = {name: 'John', age: 20};
[10, 12, 26, 15].find(f, person); // 26

fill()

使用给定值,填充一个数组

1
2
3
4
5
['a', 'b', 'c'].fill(7)
// [7, 7, 7]

new Array(3).fill(7)
// [7, 7, 7]

还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置

1
2
['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']

注意,如果填充的类型为对象,则是浅拷贝

entries(),keys(),values()

keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
or (let index of ['a', 'b'].keys()) {
console.log(index);
}
// 0
// 1

for (let elem of ['a', 'b'].values()) {
console.log(elem);
}
// 'a'
// 'b'

for (let [index, elem] of ['a', 'b'].entries()) {
console.log(index, elem);
}
// 0 "a"

includes()

用于判断数组是否包含给定的值

1
2
3
[1, 2, 3].includes(2)     // true
[1, 2, 3].includes(4) // false
[1, 2, NaN].includes(NaN) // true

方法的第二个参数表示搜索的起始位置,默认为0

参数为负数则表示倒数的位置

1
2
[1, 2, 3].includes(3, 3);  // false
[1, 2, 3].includes(3, -1); // true

flat(),flatMap()

将数组扁平化处理,返回一个新数组,对原数据没有影响

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

flat()默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将flat()方法的参数写成一个整数,表示想要拉平的层数,默认为1

1
2
3
4
5
[1, 2, [3, [4, 5]]].flat()
// [1, 2, 3, [4, 5]]

[1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]

flatMap()方法对原数组的每个成员执行一个函数相当于执行Array.prototype.map(),然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组

1
2
3
// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]

flatMap()方法还可以有第二个参数,用来绑定遍历函数里面的this

四、数组的空位

数组的空位指,数组的某一个位置没有任何值

ES6 则是明确将空位转为undefined,包括Array.from、扩展运算符、copyWithin()fill()entries()keys()values()find()findIndex()

建议大家在日常书写中,避免出现空位

五、排序稳定性

sort()默认设置为稳定的排序算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const arr = [
'peach',
'straw',
'apple',
'spork'
];

const stableSorting = (s1, s2) => {
if (s1[0] < s2[0]) return -1;
return 1;
};

arr.sort(stableSorting)
// ["apple", "peach", "straw", "spork"]

排序结果中,strawspork的前面,跟原始顺序一致

参考文献

面试官:对象新增了哪些扩展?

一、参数

ES6允许为函数的参数设置默认值

1
2
3
4
5
6
7
function log(x, y = 'World') {
console.log(x, y);
}

console.log('Hello') // Hello World
console.log('Hello', 'China') // Hello China
console.log('Hello', '') // Hello

函数的形参是默认声明的,不能使用letconst再次声明

1
2
3
4
function foo(x = 5) {
let x = 1; // error
const x = 2; // error
}

参数默认值可以与解构赋值的默认值结合起来使用

1
2
3
4
5
6
7
8
function foo({x, y = 5}) {
console.log(x, y);
}

foo({}) // undefined 5
foo({x: 1}) // 1 5
foo({x: 1, y: 2}) // 1 2
foo() // TypeError: Cannot read property 'x' of undefined

上面的foo函数,当参数为对象的时候才能进行解构,如果没有提供参数的时候,变量xy就不会生成,从而报错,这里设置默认值避免

1
2
3
4
5
function foo({x, y = 5} = {}) {
console.log(x, y);
}

foo() // undefined 5

参数默认值应该是函数的尾参数,如果不是非尾部的参数设置默认值,实际上这个参数是没发省略的

1
2
3
4
5
6
7
8
function f(x = 1, y) {
return [x, y];
}

f() // [1, undefined]
f(2) // [2, undefined]
f(, 1) // 报错
f(undefined, 1) // [1, 1]

二、属性

函数的length属性

length将返回没有指定默认值的参数个数

1
2
3
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2

rest 参数也不会计入length属性

1
(function(...args) {}).length // 0

如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了

1
2
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1

name属性

返回该函数的函数名

1
2
3
4
5
6
7
var f = function () {};

// ES5
f.name // ""

// ES6
f.name // "f"

如果将一个具名函数赋值给一个变量,则 name属性都返回这个具名函数原本的名字

1
2
const bar = function baz() {};
bar.name // "baz"

Function构造函数返回的函数实例,name属性的值为anonymous

1
(new Function).name // "anonymous"

bind返回的函数,name属性值会加上bound前缀

1
2
3
4
function foo() {};
foo.bind({}).name // "bound foo"

(function(){}).bind({}).name // "bound "

三、作用域

一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域

等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的

下面例子中,y=x会形成一个单独作用域,x没有被定义,所以指向全局变量x

1
2
3
4
5
6
7
8
9
let x = 1;

function f(y = x) {
// 等同于 let y = x
let x = 2;
console.log(y);
}

f() // 1

四、严格模式

只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 报错
function doSomething(a, b = a) {
'use strict';
// code
}

// 报错
const doSomething = function ({a, b}) {
'use strict';
// code
};

// 报错
const doSomething = (...a) => {
'use strict';
// code
};

const obj = {
// 报错
doSomething({a, b}) {
'use strict';
// code
}
};

五、箭头函数

使用“箭头”(=>)定义函数

1
2
3
4
5
6
var f = v => v;

// 等同于
var f = function (v) {
return v;
};

如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分

1
2
3
4
5
6
7
8
9
var f = () => 5;
// 等同于
var f = function () { return 5 };

var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
return num1 + num2;
};

如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回

1
var sum = (num1, num2) => { return num1 + num2; }

如果返回对象,需要加括号将对象包裹

1
let getTempItem = id => ({ id: id, name: "Temp" });

注意点:

  • 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象
  • 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误
  • 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替
  • 不可以使用yield命令,因此箭头函数不能用作 Generator 函数

参考文献

面试官:你是怎么理解ES6中Proxy的?使用场景?

一、介绍

定义: 用于定义基本操作的自定义行为

本质: 修改的是程序默认形为,就形同于在编程语言层面上做修改,属于元编程(meta programming)

元编程(Metaprogramming,又译超编程,是指某类计算机程序的编写,这类计算机程序编写或者操纵其它程序(或者自身)作为它们的数据,或者在运行时完成部分本应在编译时完成的工作

一段代码来理解

1
2
3
4
5
6
7
#!/bin/bash
# metaprogram
echo '#!/bin/bash' >program
for ((I=1; I<=1024; I++)) do
echo "echo $I" >>program
done
chmod +x program

这段程序每执行一次能帮我们生成一个名为program的文件,文件内容为1024行echo,如果我们手动来写1024行代码,效率显然低效

  • 元编程优点:与手工编写全部代码相比,程序员可以获得更高的工作效率,或者给与程序更大的灵活度去处理新的情形而无需重新编译

Proxy 亦是如此,用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)

二、用法

Proxy为 构造函数,用来生成 Proxy 实例

1
var proxy = new Proxy(target, handler)

参数

target表示所要拦截的目标对象(任何类型的对象,包括原生数组,函数,甚至另一个代理))

handler通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为

handler解析

关于handler拦截属性,有如下:

  • get(target,propKey,receiver):拦截对象属性的读取
  • set(target,propKey,value,receiver):拦截对象属性的设置
  • has(target,propKey):拦截propKey in proxy的操作,返回一个布尔值
  • deleteProperty(target,propKey):拦截delete proxy[propKey]的操作,返回一个布尔值
  • ownKeys(target):拦截Object.keys(proxy)for...in等循环,返回一个数组
  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象
  • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc),返回一个布尔值
  • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值
  • getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象
  • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值
  • setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值
  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作

Reflect

若需要在Proxy内部调用对象的默认行为,建议使用Reflect,其是ES6中操作对象而提供的新 API

基本特点:

  • 只要Proxy对象具有的代理方法,Reflect对象全部具有,以静态方法的形式存在
  • 修改某些Object方法的返回结果,让其变得更合理(定义不存在属性行为的时候不报错而是返回false
  • Object操作都变成函数行为

下面我们介绍proxy几种用法:

get()

get接受三个参数,依次为目标对象、属性名和 proxy 实例本身,最后一个参数可选

1
2
3
4
5
6
7
8
9
10
11
var person = {
name: "张三"
};

var proxy = new Proxy(person, {
get: function(target, propKey) {
return Reflect.get(target,propKey)
}
});

proxy.name // "张三"

get能够对数组增删改查进行拦截,下面是试下你数组读取负数的索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function createArray(...elements) {
let handler = {
get(target, propKey, receiver) {
let index = Number(propKey);
if (index < 0) {
propKey = String(target.length + index);
}
return Reflect.get(target, propKey, receiver);
}
};

let target = [];
target.push(...elements);
return new Proxy(target, handler);
}

let arr = createArray('a', 'b', 'c');
arr[-1] // c

注意:如果一个属性不可配置(configurable)且不可写(writable),则 Proxy 不能修改该属性,否则会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const target = Object.defineProperties({}, {
foo: {
value: 123,
writable: false,
configurable: false
},
});

const handler = {
get(target, propKey) {
return 'abc';
}
};

const proxy = new Proxy(target, handler);

proxy.foo
// TypeError: Invariant check failed

set()

set方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身

假定Person对象有一个age属性,该属性应该是一个不大于 200 的整数,那么可以使用Proxy保证age的属性值符合要求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let validator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer');
}
if (value > 200) {
throw new RangeError('The age seems invalid');
}
}

// 对于满足条件的 age 属性以及其他属性,直接保存
obj[prop] = value;
}
};

let person = new Proxy({}, validator);

person.age = 100;

person.age // 100
person.age = 'young' // 报错
person.age = 300 // 报错

如果目标对象自身的某个属性,不可写且不可配置,那么set方法将不起作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const obj = {};
Object.defineProperty(obj, 'foo', {
value: 'bar',
writable: false,
});

const handler = {
set: function(obj, prop, value, receiver) {
obj[prop] = 'baz';
}
};

const proxy = new Proxy(obj, handler);
proxy.foo = 'baz';
proxy.foo // "bar"

注意,严格模式下,set代理如果没有返回true,就会报错

1
2
3
4
5
6
7
8
9
10
11
'use strict';
const handler = {
set: function(obj, prop, value, receiver) {
obj[prop] = receiver;
// 无论有没有下面这一行,都会报错
return false;
}
};
const proxy = new Proxy({}, handler);
proxy.foo = 'bar';
// TypeError: 'set' on proxy: trap returned falsish for property 'foo'

deleteProperty()

deleteProperty方法用于拦截delete操作,如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var handler = {
deleteProperty (target, key) {
invariant(key, 'delete');
Reflect.deleteProperty(target,key)
return true;
}
};
function invariant (key, action) {
if (key[0] === '_') {
throw new Error(`无法删除私有属性`);
}
}

var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop
// Error: 无法删除私有属性

注意,目标对象自身的不可配置(configurable)的属性,不能被deleteProperty方法删除,否则报错

取消代理

1
Proxy.revocable(target, handler);

三、使用场景

Proxy其功能非常类似于设计模式中的代理模式,常用功能如下:

  • 拦截和监视外部对对象的访问
  • 降低函数或类的复杂度
  • 在复杂操作前对操作进行校验或对所需资源进行管理

使用 Proxy 保障数据类型的准确性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let numericDataStore = { count: 0, amount: 1234, total: 14 };
numericDataStore = new Proxy(numericDataStore, {
set(target, key, value, proxy) {
if (typeof value !== 'number') {
throw Error("属性只能是number类型");
}
return Reflect.set(target, key, value, proxy);
}
});

numericDataStore.count = "foo"
// Error: 属性只能是number类型

numericDataStore.count = 333
// 赋值成功

声明了一个私有的 apiKey,便于 api 这个对象内部的方法调用,但不希望从外部也能够访问 api._apiKey

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let api = {
_apiKey: '123abc456def',
getUsers: function(){ },
getUser: function(userId){ },
setUser: function(userId, config){ }
};
const RESTRICTED = ['_apiKey'];
api = new Proxy(api, {
get(target, key, proxy) {
if(RESTRICTED.indexOf(key) > -1) {
throw Error(`${key} 不可访问.`);
} return Reflect.get(target, key, proxy);
},
set(target, key, value, proxy) {
if(RESTRICTED.indexOf(key) > -1) {
throw Error(`${key} 不可修改`);
} return Reflect.get(target, key, value, proxy);
}
});

console.log(api._apiKey)
api._apiKey = '987654321'
// 上述都抛出错误

还能通过使用Proxy实现观察者模式

观察者模式(Observer mode)指的是函数自动观察数据对象,一旦对象有变化,函数就会自动执行

observable函数返回一个原始对象的 Proxy 代理,拦截赋值操作,触发充当观察者的各个函数

1
2
3
4
5
6
7
8
9
10
const queuedObservers = new Set();

const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {set});

function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
queuedObservers.forEach(observer => observer());
return result;
}

观察者函数都放进Set集合,当修改obj的值,在会set函数中拦截,自动执行Set所有的观察者

参考文献