js基础(3)--函数、类与继承

359 阅读36分钟

15. 深拷贝与浅拷贝

15.1 浅拷贝

1. Object.assign(..)

  • 它会遍历一个或多个源对象的所有可枚举的自有键包括 Symbol 类型的属性)
  • 并使用源对象的[[Get]]和目标对象的[[Set]],把它们复制到目标对象,最后返回目标对象;
  • 相同属性会覆盖,后面的覆盖前面的;

原始类型会被包装为对象

const v1 = "abc";
const v2 = true;
const v3 = 10;
const v4 = Symbol("foo")

const obj = Object.assign({}, v1, null, v2, undefined, v3, v4); 
// 原始类型会被包装,null 和 undefined 会被忽略。
// 注意,只有字符串的包装对象才可能有自身可枚举属性。
console.log(obj); // { "0": "a", "1": "b", "2": "c" }

上面代码中的源对象 v2、v3、v4 实际上被忽略了,原因在于他们自身没有可枚举属性。

不会拷贝继承属性和不可枚举属性

const obj = Object.create({foo: 1}, { // foo 是个继承属性。
    bar: {
        value: 2  // bar 是个不可枚举属性。
    },
    baz: {
        value: 3,
        enumerable: true  // baz 是个自身可枚举属性。
    }
});

const copy = Object.assign({}, obj);
console.log(copy); // { baz: 3 }

完整浅拷贝一个对象(含原型+属性特性):

Object.create(
  Object.getPrototypeOf(obj), 
  Object.getOwnPropertyDescriptors(obj) 
);

Object.assign() 方法只能拷贝源对象的可枚举的自身属性,无法拷贝属性的特性们,而且访问器属性会被转换成数据属性,也无法拷贝源对象的原型,该方法配合 Object.create() 方法可以实现上面说的这些。

异常会打断后续拷贝任务

const target = Object.defineProperty({}, "foo", {
    value: 1,
    writable: false
}); // target 的 foo 属性是个只读属性。

Object.assign(target, {bar: 2}, {foo2: 3, foo: 3, foo3: 3}, {baz: 4});
// TypeError: "foo" is read-only
// 注意这个异常是在拷贝第二个源对象的第二个属性时发生的。

console.log(target.bar);  // 2,说明第一个源对象拷贝成功了。
console.log(target.foo2); // 3,说明第二个源对象的第一个属性也拷贝成功了。
console.log(target.foo);  // 1,只读属性不能被覆盖,所以第二个源对象的第二个属性拷贝失败了。
console.log(target.foo3); // undefined,异常之后 assign 方法就退出了,第三个属性是不会被拷贝到的。
console.log(target.baz);  // undefined,第三个源对象更是不会被拷贝到的。

object-assign-模拟实现

2. ...展开赋值

扩展运算符(...)内部使用 for..of 循环,for..of 循环内部调用的是数据结构的 Symbol.iterator 方法。

  • 数组展开 -- ES2015
  • 对象展开 -- ES2018
    • 将已有对象的所有可枚举属性拷贝到新构造的对象中
    • 类似于 Object.assign() 方法。但 Object.assign() 函数会触发 setters,而展开语法不会

3. 浅拷贝数组:Array.prototype.slice()

arr.slice([begin[, end]])

let fruits = ['Apple', 'Banana']
let shallowCopy = fruits.slice()

15.2 深拷贝

其实深拷贝可以拆分成 2 步,浅拷贝 + 递归,浅拷贝时判断属性值是否是对象,如果是对象就进行递归操作,两个一结合就实现了深拷贝。

1. JSON.stringify()

在对象中遇到 undefined、function、symbol 时会自动将其忽略,在数组中则会返回 null(以保证单元位置不变);对包含循环引用的对象执行 JSON.stringify() 会出错

对于 JSON 安全(也就是说可以被序列化为一个 JSON 字符串,并且可以根据这个字符串解析出一个结构和值完全一样的对象)的对象来说,有一种巧妙的复制方法:

var newObj = JSON.parse( JSON.stringify( someObj ) );

转换异常情况

  • 会移除: undefined、function、Symbol(当拷贝数组时,会被转换成null);
  • 会被换成null:NaN;
  • 会被换成{}:正则、Set、Map;
  • new Date()转换后日期慢8小时:会调用日期toJSON,默认转成国际标准时间;
  • 循环引用报错
  • 引用丢失

2. jquery.extend()

// Todo

3. lodash.cloneDeep()

// Todo

lodash的源码中,对象是通过 Object.create( Object.getPrototypeOf(object) ) 来创建的,这样不会丢失原型方法。

4. 实现一个深拷贝

思路:浅拷贝 + 递归

  • 如果是原始类型,无需继续拷贝,直接返回
  • 如果是引用类型,创建一个新的对象,遍历需要克隆的对象,将需要克隆对象的属性执行深拷贝后依次添加到新对象上。

最简单的深拷贝:

function clone(source) {
    var target = {};
    for(var i in source) {
        if (source.hasOwnProperty(i)) {
            if (typeof source[i] === 'object') {
                target[i] = clone(source[i]); // 注意这里
            } else {
                target[i] = source[i];
            }
        }
    }

    return target;
}

存在问题:

  • 没有对参数做检验
  • 判断是否对象的逻辑不够严谨
  • 没有考虑数组的兼容

改进版

解决方案:

  1. 循环引用、引用丢失:使用循环检测解决,我们设置一个数组或者哈希表存储已拷贝过的对象,当检测到当前对象已存在于哈希表中时,取出该值并返回即可。
  2. 拷贝 Symbol
    • Object.getOwnPropertySymbols(..)
    • Reflect.ownKeys(..)
function isObject(obj) {
    return typeof obj === 'object' && obj != null;
}

function cloneDeep(source, hash = new WeakMap()) {

    if (!isObject(source)) return source; 
    if (hash.has(source)) return hash.get(source); 
      
    let target = Array.isArray(source) ? [...source] : { ...source };
    hash.set(source, target);
    
    Reflect.ownKeys(target).forEach(key => {
        if (isObject(source[key])) {
            target[key] = cloneDeep(source[key], hash); 
        }
    });
    return target;
}

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。

一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。

如果我们要拷贝的对象非常庞大时,使用 Map 会对内存造成非常大的额外消耗,而且我们需要手动清除 Map 的属性才能释放这块内存,而 WeakMap 会帮我们巧妙化解这个问题。

  1. 破解递归爆栈

    function cloneForce(x) {
        const uniqueList = []; // 用来去重
    
        let root = {};
    
        // 循环数组
        const loopList = [
            {
                parent: root,
                key: undefined,
                data: x,
            }
        ];
    
        while(loopList.length) {
            // 深度优先
            const node = loopList.pop();
            const parent = node.parent;
            const key = node.key;
            const data = node.data;
    
            // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
            let res = parent;
            if (typeof key !== 'undefined') {
                res = parent[key] = {};
            }
            
            // 数据已经存在
            let uniqueData = find(uniqueList, data);
            if (uniqueData) {
                parent[key] = uniqueData.target;
                break; // 中断本次循环
            }
    
            // 数据不存在
            // 保存源数据,在拷贝数据中对应的引用
            uniqueList.push({
                source: data,
                target: res,
            });
        
            for(let k in data) {
                if (data.hasOwnProperty(k)) {
                    if (typeof data[k] === 'object') {
                        // 下一次循环
                        loopList.push({
                            parent: res,
                            key: k,
                            data: data[k],
                        });
                    } else {
                        res[k] = data[k];
                    }
                }
            }
        }
    
        return root;
    }
    
    function find(arr, item) {
        for(let i = 0; i < arr.length; i++) {
            if (arr[i].source === item) {
                return arr[i];
            }
        }
    
        return null;
    }
    

5. 结构化克隆

// Todo

1) new MessageChannel()

  • 优点:能解决循环引用,内置对象都ok;
  • 缺点:异步,不能拷贝 symbol 和 function
let deepClone = function(obj) {
    let { port1, port2 } = new MessageChannel()
    
    return new Promise(resolve => {
        port1.postMessage(obj)
        port2.onmessage = e => resolve(e.data)
    })
}

let obj = {...}
deepClone(obj).then(res =>{
    console.log(res)
})

2) new Notification()

  • 优点:能解决循环引用,同步;
  • 缺点:不能拷贝 symbol 和 function

3) history.replaceState()

15.3 怎么判断两个对象相等?

// Todo

16. 函数

函数声明和函数表达式之间最重要的区别是它们的名称标识符将会绑定在何处。

  • 函数声明:function f(){..}
  • 函数表达式:(function f(){..}),作为函数表达式意味着 f 只能在 .. 所代表的位置中被访问

16.1 arguments

arguments对象不是一个 Array 。它类似于Array,但除了length属性和索引元素之外没有任何Array属性

将其转换为一个真正的Array:

var args = Array.prototype.slice.call(arguments);
var args = [].slice.call(arguments);

// ES2015
const args = Array.from(arguments);
const args = [...arguments];
  • arguments.callee 指向当前正在运行的函数
    • 在匿名函数中很有用
    • 在严格模式下,ES5 禁止使用 arguments.callee();
  • arguments.callee.caller 指向调用当前函数的函数
    • 即Function.caller,该特性是非标准的,请尽量不要在生产环境中使用它!

16.2 匿名函数表达式

最常见的就是回调参数:

setTimeout( function() {
    console.log("I waited 1 second!")
}, 1000)

缺点:

  • 在栈跟踪中不会显示出有意义的函数名,使得调试困难
  • 当函数需要引用自身时,只能使用已经过期的 arguments.callee
  • 不利于 代码可读性、可理解性

始终给函数表达式命名是一个最佳实践:

setTimeout( function timeoutHander() {
    console.log("I waited 1 second!")
}, 1000)

16.3 立即执行函数表达式(IIFE)

var a = 2;
(function foo() { 
    var a = 3;
    console.log( a ); // 3
})();
console.log( a ); // 2

由于函数被包含在一对 ( ) 括号内部,因此成为了一个表达式,通过在末尾加上另外一个 ( ) 可以立即执行这个函数。

函数名对 IIFE 当然不是必须的,IIFE 最常见的用法是使用一个匿名函数表达式。 常见的有两种写法

  • (function(){ .. })()
  • (function(){ .. }())

这两种形式在功能上是一致的。

进阶用法

把 IIFE 当作函数调用并传递参数进去:

var a = 2;
(function IIFE( global ) {
    var a = 3;
    console.log( a ); // 3 
    console.log( global.a ); // 2
})( window );
console.log( a ); // 2
  1. 解决 undefined 标识符的默认值被错误覆盖导致的异常
    undefined = true; // 给其他代码挖了一个大坑!绝对不要这样做! 
    (function IIFE( undefined ) {
        var a;
        if (a === undefined) {
            console.log( "Undefined is safe here!" );
        }
    })();
    
  2. 倒置代码的运行顺序:将需要运行的函数放在第二位,在 IIFE 执行之后当作参数传递进去。
    (function IIFE( def ) { 
        def( window );
    })(function def( global ) {
        var a = 3;
        console.log( a ); // 3 
        console.log( global.a ); // 2
    });
    

16.4 闭包

活动对象

函数被调用时,会使用 arguments 和其他命名参数的值来初始化函数的活动对象

function f(a, b){
    return a + b;
}
f(1, 2)

以上代码,先定义了 f() 函数,然后在全局作用域中调用了它。当调用 f() 时,会创建一个包含 arguments、a 和 b 的活动对象。

作用域链

本质上是一个指向变量对象的指针列表

  • 创建函数时,会创建一个预先包含全局变量对象的作用域链,保存在函数内部的 [[Scope]] 属性中;
  • 调用函数时,会为函数创建一个执行环境,然后通过复制函数的 [[Scope]] 属性中的对象,构建起执行环境的作用域链,然后把创建的活动对象推入执行环境作用域链的前端。
  • 在函数内部定义的函数会将外部函数的活动对象添加到它的作用域链中。

在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。

一般情况下,当函数执行完毕后,局部活动对象就会被销毁。但闭包有所不同,当外部函数执行完毕后,其执行环境的作用域链会被销毁,但其活动对象不会销毁,因为内部函数的作用域链仍然在引用这个活动对象。

闭包

是指有权访问另一个函数作用域中的变量的函数。即使函数是在当前词法作用域之外执行。

闭包使得函数可以继续访问定义时的词法作用域。

function outerFunc(prop) {
    return function(obj) {
        return obj[prop]; // 这里的 prop 指向 outerFunc 作用域内的 prop
    }
}

// 创建函数
var getName = outerFunc("name");
// 调用函数
var res = getName({ name:"kk" });
// 解除对匿名函数的引用(以便释放内存)
getName = null;

拜返回的匿名函数所声明的位置所赐,它拥有涵盖 outerFunc() 内部作用域的闭包,使得该作用域能够一直存活,以供该函数在之后任何时间进行引用。 getName 持有对 outerFunc 作用域的引用,而这个引用就叫作闭包

创建的函数被保存在变量 getName 中。通过将 getName 设置为 null,解除该函数的引用,就等于通知垃圾回收将其清除。随着匿名函数的作用域链被销毁,外部函数的作用域也都可以安全地销毁了。

闭包与变量

闭包保存的是整个变量对象,所以只能取得外部函数中变量的最后赋值。

function createFuncs(){
    var res = new Array();
    
    for (var i = 0; i < 10; i++){
        res[i] = function(){
            return i;
        }
    }
    return res;
}

let funcs = createFuncs()
funcs[0]() // 10 

因为每个函数的作用域链中都保存着 createFuncs() 函数的活动对象,所以它们引用的都是同一个变量 i。

通过创建另一个匿名函数,再加一层活动对象,强制让闭包的行为符合预期:

function createFuncs(){
    var res = new Array();
    
    for (var i = 0; i < 10; i++){
        res[i] = function(num){ // 改动 1      闭包 1
            return function(){              // 闭包 2
                return num; // 改动 3
            }
        }(i) // 改动 2
    }
    return res;
}

let funcs = createFuncs()
funcs[0]() // 0 

闭包与this

var name  = "The Window";

var obj = {
    name: "The Object",
    getNameFunc: function(){
        return function(){
            return this.name;
        }
    }
}
obj.getNameFunc()() // "The Window"

每个函数在被调用时,都会自动取得两个特殊的变量:this 和 arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。

通过把外部函数作用域中的 this 对象,保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了:

var name  = "The Window";

var obj = {
    name: "The Object",
    getNameFunc: function(){
        var that = this;    // 改动 1
        return function(){
            return that.name; // 改动 2
        }
    }
}
obj.getNameFunc()() // "The Object"

也可以利用箭头函数,保持this指向外部函数

var name  = "The Window";

var obj = {
    name: "The Object",
    getNameFunc: function(){
        return () => {      // 改动
            return this.name;
        }
    }
}
obj.getNameFunc()() // "The Object"

闭包与循环

for (var i=1; i<=5; i++) {
    var start = new Date().getTime()
    setTimeout( function timer() { 
        var end = new Date().getTime()
        console.log("i:",i,"time:", end - start ); 
    }, i*1000 ); 
}
/* 会以每秒一次的频率输出五次 6,输出结果:
i: 6 time: 1001
i: 6 time: 2001
i: 6 time: 3001
i: 6 time: 4001
i: 6 time: 5001
*/

改写方式一:

for (var i=1; i<=5; i++) { 
    (function(j) {  // 改动 1
        var start = new Date().getTime()
        setTimeout( function timer() { 
            var end = new Date().getTime()
            console.log("j:",j,"time:", end - start ); 
        }, j*1000 );    // 改动 3
    })(i);  // 改动 2
}
/* 会以每秒一次的频率,依次输出1~5,输出结果:
i: 1 time: 1001
i: 2 time: 2001
i: 3 time: 3002
i: 4 time: 4000
i: 5 time: 5001
*/

改写方式二,使用块作用域:

for (let i=1; i<=5; i++) {  // 改动:把 var 换成 let
    var start = new Date().getTime()
    setTimeout( function timer() { 
        var end = new Date().getTime()
        console.log("i:",i,"time:", end - start ); 
    }, i*1000 ); 
}
/* 会以每秒一次的频率,依次输出1~5,输出结果:
i: 1 time: 1000
i: 2 time: 2000
i: 3 time: 3000
i: 4 time: 4001
i: 5 time: 5001
*/

在定时器、事件监听器、 Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!

17. 继承

ES 只支持实现继承,并且主要是依靠原型链来实现的。

构造函数、原型、实例的关系:

每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。

原型搜索机制:

当以读取模式访问一个实例属性时,首先会在实例中搜索该属性;如果没有找到该属性,则会继续搜索实例的原型。在找不到属性或方法的情况下,搜索过程总是要一环一环地前行到原型链末端才会停下来。

17.1 原型链继承

function Father(){
	this.a = true;
}
Father.prototype.getA = function(){
	return this.a;
}

function Son(){
	this.b = false;
}

//继承 Father
Son.prototype = new Father(); 
// Son.prototype 被重写,会导致 Son.prototype.constructor 会指向 Father,而不再指向 Son
Son.prototype.constructor = Son

Son.prototype.getB = function(){
	return this.b;
}

var instance = new Son();
instance.getA(); //true

instance 实例通过原型链找到了 Father 原型中的 getA 方法。

注意

  • 不要把对象字面量赋值给子类的原型(即 Son.prototype);
  • 需要手动将 Son.prototype.constructor 指回 Son;
  • 给原型添加方法的代码(如 Son.prototype.getB ),一定要放在替换原型的语句之后。

缺点

  1. 当原型链中包含引用类型值的原型时,该引用类型值会被所有实例共享;
  2. 在创建子类型的实例时,不能向超类型的构造函数中传递参数。

17.2 借用构造函数(经典继承)

基本思想:通过使用 apply() 或 call() ,在子类型构造函数的内部调用超类型构造函数。

function Father(name){
    this.name = name
	this.colors = ["red","blue","green"];
	this.sayName = sayName
}
function sayName(){
    console.log("my name is:", this.name)
}
Father.prototype.sayHi = function(){
    console.log("hi, I'm the father")
}

function Son(name, age){
	// 继承了 Father,并且可以向父类型传递参数
	Father.call(this, name); 
	// 实例属性
	this.age = age 
}

var son1 = new Son("Jone", 10);
son1.colors.push("black");
console.log(son1.colors); //"red,blue,green,black"

var son2 = new Son("Jony", 20);
console.log(son2.colors); //"red,blue,green" 引用类型值是独立的

注意:

  • 为了确保 Father 构造函数不会重写子类型的属性,应该在调用超类型构造函数后,再添加应在子类型中定义的属性(如 this.age)。

优点

解决了原型链继承的两大问题

缺点

  • 只能继承父类的实例属性和方法,不能继承原型上的属性和方法(如 sayHi )。所以必须把所有方法放在构造函数中。
  • 但是会有构造函数自身存在的问题,构造函数中的每个方法都要在每个实例上重新创建一遍。(可优化,如上例中将 sayName 的函数定义转移到构造函数外部)

结果所有类型都只能使用构造函数模式。因此,借用构造函数的技术也很少单独使用。

17.3 组合继承

原型链继承 + 借用构造函数,又称伪经典继承

基本思想:使用原型链继承实现对原型上属性和方法的继承,而通过借用构造函数实现实例上属性和方法的继承。

这样,既通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性。

function Father(name){
    this.name = name
	this.colors = ["red","blue","green"];
	this.sayName = sayName
}
function sayName(){
    console.log("my name is:", this.name)
}
Father.prototype.sayHi = function(){
    console.log("hi, I'm the father")
}

function Son(name, age){
	// 继承属性
	Father.call(this, name); 
	// 实例属性
	this.age = age 
}

// 继承方法
Son.prototype = new Father() // 改动 1
Son.prototype.constructor = Son // 改动 2

var son1 = new Son("Jone", 10);
son1.colors.push("black");
console.log(son1.colors); //"red,blue,green,black"

var son2 = new Son("Jony", 20);
console.log(son2.colors); //"red,blue,green"

优点

  • 组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式
  • instanceof 和 isPrototypeOf() 也能够用于识别基于组合继承创建的对象。

缺点

  • 执行了两次父类构造函数,第一次是 Son.prototype = new Father() ,第二次是 Father.call(this, name) ,造成了不必要的浪费;
  • 父类的实例属性(如 name、colors、sayName )会在每个子类的实例上存两份,一份在实例上(如 son1.colors ),另一份在原型对象上(如 son1.__proto__.colors );只不过实例上的属性会屏蔽原型链上的同名属性,造成了内存浪费。

17.4 原型式继承

基本思想:在 create() 函数内部,先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。

function create(o){ // o 必须为对象类型
	function F(){}
	F.prototype = o;
	return new F();
}

从本质上讲,create() 对传入其中的对象执行了一次浅复制,如:

var person = {
	friends: ["a","b","c"]
};
var person1 = create(person);
person1.friends.push("d");
var person2 = create(person);
person2.friends.push("e");
console.log(person.friends); //["a", "b", "c", "d", "e"]

缺点

  • 与原型链继承一样,当原型链中包含引用类型值的原型时,该引用类型值会被所有实例共享;

Object.create() 原型式继承

在 ECMAScript5 中,通过新增 Object.create() 方法规范化了上面的原型式继承。

在传入一个参数的情况下,Object.create() 与 create() 方法的行为相同。改造一下上面的例子:

var person = {
	friends: ["a","b","c"]
};
var person1 = Object.create(person); // 改动 1
person1.friends.push("d");
var person2 = Object.create(person); // 改动 2
person2.friends.push("e");
console.log(person.friends); //["a", "b", "c", "d", "e"]

MDN 上 Object.create() 的 polyfill 如下,核心就是原型式继承:

if (typeof Object.create !== "function") {
    Object.create = function (proto, propertiesObject) {
        if (typeof proto !== 'object' && typeof proto !== 'function') {
            throw new TypeError('Object prototype may only be an Object: ' + proto);
        } else if (proto === null) {
            throw new Error("This browser's implementation of Object.create is a shim and doesn't support 'null' as the first argument.");
        }

        if (typeof propertiesObject != 'undefined') throw new Error("This browser's implementation of Object.create is a shim and doesn't support a second argument.");

        function F() {}
        F.prototype = proto;

        return new F();
    };
}

17.5 寄生式继承

基本思想:创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。

function createAnother(original){
	var clone = Object.create(original); //通过调用 Object.create 创建一个新对象
	clone.sayHi = function(){ //以某种方式来增强这个对象
		alert("hi");
	};
	return clone; //返回这个对象
}

缺点

  • 通原型式继承一样,会导致引用类型被所有实例共享
  • 同借用构造函数一样,由于每次创建对象都会创建一遍方法,方法存在于实例而非原型中,所以无法复用父类函数。

17.6 寄生组合式继承

回顾一下组合继承

function Father(name){
    this.name = name
	this.colors = ["red","blue","green"];
	this.sayName = sayName
}
function sayName(){
    console.log("my name is:", this.name)
}
Father.prototype.sayHi = function(){
    console.log("hi, I'm the father")
}

function Son(name, age){ // 借用构造函数
	// 继承属性
	Father.call(this, name);    
	// 实例属性
	this.age = age 
}

// 继承方法
Son.prototype = new Father() // 原型链继承
Son.prototype.constructor = Son 

var son1 = new Son("Jone", 10);
son1.colors.push("black");
console.log(son1.colors); //"red,blue,green,black"

var son2 = new Son("Jony", 20);
console.log(son2.colors); //"red,blue,green"

前面说过,组合继承是 JavaScript 最常用的继承模式;不过,它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次父类构造函数: 一次是在创建子类型原型的时候, 另一次是在子类型构造函数内部。

寄生组合式继承就是为了降低调用父类构造函数的开销而出现的

基本思路:通过寄生式继承来继承父类的原型,通过借用构造函数来继承实例上的属性和方法。(而不必为了指定子类型的原型而调用超类型的构造函数)

function extend(Son, Father) {
  const prototype = Object.create(Father.prototype); // 创建父类原型的副本
  prototype.constructor = Son; // 将副本的构造函数指向子类
  Son.prototype = prototype; // 将该副本赋值给子类的原型
}

这样我们就可以用调用 extend() 函数,来替换组合继承中为子类型原型赋值的语句了。如:

function Father(name){
    this.name = name
	this.colors = ["red","blue","green"];
	this.sayName = sayName
}
function sayName(){
    console.log("my name is:", this.name)
}
Father.prototype.sayHi = function(){
    console.log("hi, I'm the father")
}

function Son(name, age){    // 借用构造函数
	// 继承实例
	Father.call(this, name);
	this.age = age 
}

// 继承原型
extend(Son, Father)         // 寄生式继承--只改动了这里

var son1 = new Son("Jone", 10);
son1.colors.push("black");
console.log(son1.colors); //"red,blue,green,black"

var son2 = new Son("Jony", 20);
console.log(son2.colors); //"red,blue,green"

优点:

  • 寄生组合式继承的高效率体现在它只调用了一次 Father 的构造函数,因此避免了在 Father.prototype 上面创建不必要的、多余的属性;
  • 同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf()。

因此,寄生组合式继承可以说是在 ES6 之前最好的继承方式了

继承单个对象

function SuperClass() {
    // 在这里定义父类实例属性和方法
}
SuperClass.prototype = {
    // 在这里定义父类原型属性和方法
}
function SubClass() {
    SuperClass.call(this);
}
SubClass.prototype = Object.create(
    SuperClass.prototype,
    Object.getOwnPropertyDescriptors({
      // 在这里定义子类属性和方法
    })
);
SubClass.prototype.constructor = SubClass;

继承多个对象

function MyClass() {
     SuperClass.call(this);
     OtherSuperClass.call(this);
}

// 继承一个类
MyClass.prototype = Object.create(SuperClass.prototype);
// 混合其它
Object.assign(MyClass.prototype, OtherSuperClass.prototype);
// 重新指定constructor
MyClass.prototype.constructor = MyClass;

MyClass.prototype.myMethod = function() {
     //..
};

17.7 ES6 继承

Class 可以通过 extends 关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}

与 ES5 继承的区别

  • ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。
  • ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

Object.getPrototypeOf()

Object.getPrototypeOf 方法可以用来从子类上获取父类。

Object.getPrototypeOf(ColorPoint) === Point // true

因此,可以使用这个方法判断,一个类是否继承了另一个类。

类的 prototype 属性和__proto__属性

Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链

  1. 子类的__proto__属性,表示构造函数的继承,总是指向父类。
  2. 子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。
class A {
}

class B extends A {
}

B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true

这样的结果是因为,类的继承是按照下面的模式实现的。

class A {
}

class B {
}

// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);

// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true

这两条继承链,可以这样理解:

  • B 作为一个对象,子类的原型(B.__proto__)是父类(A);
  • B 作为一个构造函数,子类的原型对象(B.prototype)是父类的原型对象(A.prototype)的实例。

实例的__proto__属性

子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性。也就是说,子类的原型的原型,是父类的原型。

var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');

p2.__proto__.__proto__ === p1.__proto__ // true

上面代码中,ColorPoint继承了Point,导致前者原型的原型是后者的原型。

原生构造函数的继承

  • ES5 是先新建子类的实例对象this,再将父类的属性添加到子类上,由于原生构造函数的内部属性无法获取,导致无法继承原生的构造函数
  • ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。

下面是一个自定义Error子类的例子,可以用来定制报错时的行为。

class BaseError extends Error {
  constructor(message) {
    super();
    this.message = message;
    this.stack = (new Error()).stack;
    this.name = this.constructor.name;
  }
}

class MyError extends BaseError {
  constructor(m) {
    super(m);
  }
}

var myerror = new MyError('oops');
myerror.message // "oops"
myerror instanceof Error // true
myerror.name // "MyError"
myerror.stack
// Error
//     at MyError.BaseError
//     ...

分析 class、extends 原理

class 定义

class Point {
}

babel 转换后:

"use strict";

function _instanceof(left, right) {
  if (
    right != null &&
    typeof Symbol !== "undefined" &&
    right[Symbol.hasInstance]
  ) {
    return !!right[Symbol.hasInstance](left);
  } else {
    return left instanceof right;
  }
}

function _classCallCheck(instance, Constructor) {
  if (!_instanceof(instance, Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

var Point = function Point() {
  _classCallCheck(this, Point);
};
  1. _classCallCheck(instance, Constructor):检测函数,检查对象是否是 new 出来的,虽然类是用 ES5 中的构造函数构造出来的,但是 ES6 中的类不允许直接调用;

实例属性、原型方法和静态方法

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
  
  static classMethod() {
    return 'Point static method';
  }
}

babel 转换后:

"use strict";

function _instanceof(left, right) {
  // ..
}

function _classCallCheck(instance, Constructor) {
  // ..
}

function _defineProperties(target, props) {
  for (var i = 0; i < props.length; i++) {
    var descriptor = props[i];
    descriptor.enumerable = descriptor.enumerable || false;
    descriptor.configurable = true;
    if ("value" in descriptor) descriptor.writable = true;
    Object.defineProperty(target, descriptor.key, descriptor);
  }
}

function _createClass(Constructor, protoProps, staticProps) {
  if (protoProps) _defineProperties(Constructor.prototype, protoProps);
  if (staticProps) _defineProperties(Constructor, staticProps);
  return Constructor;
}

var Point =
  /*#__PURE__*/
  (function() {
    function Point(x, y) {  // 构造函数
      _classCallCheck(this, Point);

      this.x = x;
      this.y = y;
    }

    _createClass(
      Point,
      [
        {
          key: "toString",
          value: function toString() {
            return "(" + this.x + ", " + this.y + ")";
          }
        }
      ],
      [
        {
          key: "classMethod",
          value: function classMethod() {
            return "Point static method";
          }
        }
      ]
    );

    return Point; // 返回构造函数
  })();
  1. _defineProperties(target, props):将 props 内容循环赋值给 target ,props 内部可以设置是否可枚举,利用 Object.defineProperty 写入到 target 上;
  2. _createClass(Constructor, protoProps, staticProps):判断 protoProps、staticProps 是否存在,分别调用 _defineProperties:
    • 将 protoProps(原型方法)设置在 Constructor.prototype(类的原型对象) 上;
    • 将 staticProps(静态方法)设置在 Constructor(类)上,只有通过类才能调用,实例对象不能调用;

extends

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
  
  static classMethod() {
    return 'Point static method';
  }
}

class ColorPoint extends Point {
  constructor(x, y, color ){
    super(x, y)
    this.color = color;
  }
}

babel 转换后:

"use strict";

function _typeof(obj) {
  // 运行环境原生支持 Symbol
  if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
    _typeof = function _typeof(obj) {
      return typeof obj;
    };
  } else {
    // 模拟实现 Symbol
    _typeof = function _typeof(obj) {
      return obj &&
        typeof Symbol === "function" &&
        obj.constructor === Symbol &&
        obj !== Symbol.prototype
        ? "symbol"
        : typeof obj;
    };
  }
  return _typeof(obj);
}

function _possibleConstructorReturn(self, call) {
  // 如果 call 是引用类型,返回 call(即,父类构造函数的返回值)
  if (call && (_typeof(call) === "object" || typeof call === "function")) {
    return call;
  }
  // 否则返回子类实例的 this
  return _assertThisInitialized(self);
}

function _assertThisInitialized(self) {
  if (self === void 0) {
    throw new ReferenceError(
      "this hasn't been initialised - super() hasn't been called"
    );
  }
  return self;
}

function _getPrototypeOf(o) {
  _getPrototypeOf = Object.setPrototypeOf
    ? Object.getPrototypeOf
    : function _getPrototypeOf(o) {
        return o.__proto__ || Object.getPrototypeOf(o);
      };
  return _getPrototypeOf(o);
}

function _inherits(subClass, superClass) {
  if (typeof superClass !== "function" && superClass !== null) {
    throw new TypeError("Super expression must either be null or a function");
  }
  // 继承原型方法:subClass.prototype.__proto__ === superClass.prototype
  subClass.prototype = Object.create(
    superClass && superClass.prototype, 
    {
        constructor: { 
            value: subClass, 
            writable: true, 
            configurable: true
        }
    }
  );
  // 继承静态属性、静态方法:subClass.__proto__ === superClass
  if (superClass) _setPrototypeOf(subClass, superClass);
}

function _setPrototypeOf(o, p) {
  _setPrototypeOf =
    Object.setPrototypeOf ||
    function _setPrototypeOf(o, p) {
      o.__proto__ = p;
      return o;
    };
  return _setPrototypeOf(o, p);
}

function _instanceof(left, right) {
  // ..
}

function _classCallCheck(instance, Constructor) {
  // ..
}

function _defineProperties(target, props) {
  // ..
}

function _createClass(Constructor, protoProps, staticProps) {
  // ..
}

var Point =
  /*#__PURE__*/
  (function() {
    function Point(x, y) {
      _classCallCheck(this, Point);

      this.x = x;
      this.y = y;
    }

    _createClass(
      Point,
      [
        {
          key: "toString",
          value: function toString() {
            return "(" + this.x + ", " + this.y + ")";
          }
        }
      ],
      [
        {
          key: "classMethod",
          value: function classMethod() {
            return "Point static method";
          }
        }
      ]
    );

    return Point;
  })();

var ColorPoint =
  /*#__PURE__*/
  (function(_Point) {
    _inherits(ColorPoint, _Point);

    function ColorPoint(x, y, color) {
      var _this;

      _classCallCheck(this, ColorPoint);

      _this = _possibleConstructorReturn(
        this,
        _getPrototypeOf(ColorPoint).call(this, x, y) // 相当于 _Point.call(this, x, y) 在子类实例上,调用父类构造函数,并传参
      );
      _this.color = color;
      return _this;
    }

    return ColorPoint;
  })(Point);

  1. _typeof(obj):判断类型;
  2. _assertThisInitialized(self):校验子类的构造函数中,必须先调用 super() ;
  3. _possibleConstructorReturn(self, call):判断 call(传参是: _getPrototypeOf(ColorPoint).call(this, x, y) 即,父类构造函数的返回值类型),如果是引用类型返回 call;否则返回子类实例的 this;
  4. _getPrototypeOf(o):获取 o 的__proto__ ;
  5. _setPrototypeOf(o, p):将 o 的__proto__ 设置为 p ;
  6. _inherits(subClass, superClass):实现继承;

子类调用父类静态方法

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
  
  static classMethod() {
    return 'Point static method';
  }
}

class ColorPoint extends Point {
  constructor(x, y, color ){
    super(x, y)
    this.color = color;
  }
  
  toString() {
    return this.color + super.toString();
  }
}

babel 转换后:

"use strict";

function _typeof(obj) {
  // ..
}

function _possibleConstructorReturn(self, call) {
  // ..
}

function _assertThisInitialized(self) {
  // ..
}

// 读取属性
function _get(target, property, receiver) {
  // 支持 Reflect
  if (typeof Reflect !== "undefined" && Reflect.get) {
    _get = Reflect.get;
  } else {
    // 模拟 Relect
    _get = function _get(target, property, receiver) {
      var base = _superPropBase(target, property);
      if (!base) return;
      var desc = Object.getOwnPropertyDescriptor(base, property);
      if (desc.get) {
        return desc.get.call(receiver);
      }
      return desc.value;
    };
  }
  return _get(target, property, receiver || target);
}

// 沿着原型链向上查找,直到找到自身拥有该属性的原型对象(即不是通过继承获得该属性)
function _superPropBase(object, property) {
  while (!Object.prototype.hasOwnProperty.call(object, property)) {
    object = _getPrototypeOf(object);
    if (object === null) break;
  }
  return object;
}

function _getPrototypeOf(o) {
  // ..
}

function _inherits(subClass, superClass) {
  // ..
}

function _setPrototypeOf(o, p) {
  // ..
}

function _instanceof(left, right) {
  // ..
}

function _classCallCheck(instance, Constructor) {
  // ..
}

function _defineProperties(target, props) {
  // ..
}

function _createClass(Constructor, protoProps, staticProps) {
  // ..
}

var Point =
  /*#__PURE__*/
  (function() {
    function Point(x, y) {
      _classCallCheck(this, Point);

      this.x = x;
      this.y = y;
    }

    _createClass(
      Point,
      [
        {
          key: "toString",
          value: function toString() {
            return "(" + this.x + ", " + this.y + ")";
          }
        }
      ],
      [
        {
          key: "classMethod",
          value: function classMethod() {
            return "Point static method";
          }
        }
      ]
    );

    return Point;
  })();

var ColorPoint =
  /*#__PURE__*/
  (function(_Point) {
    _inherits(ColorPoint, _Point);

    function ColorPoint(x, y, color) {
      var _this;

      _classCallCheck(this, ColorPoint);

      _this = _possibleConstructorReturn(
        this,
        _getPrototypeOf(ColorPoint).call(this, x, y)
      );
      _this.color = color;
      return _this;
    }

    _createClass(ColorPoint, [
      {
        key: "toString",
        value: function toString() {
          return (
            this.color +
            _get(_getPrototypeOf(ColorPoint.prototype), "toString", this).call(this) 
            // 这里的 this 是 ColorPoint ,即 在子类上调用; 
            // Point.prototype 的原型链上自身拥有 toString 方法的原型对象是 Point 构造函数;
            // 所以实现了在子类上调用父类的静态方法。
          );
        }
      }
    ]);

    return ColorPoint;
  })(Point);
  1. _get(target, property, receiver):读取属性;
  2. _superPropBase(object, property):沿着原型链向上查找,查找自身拥有该属性的原型对象

总结一下:

  1. 执行 _inherits(ColorPoint, Point),建立 ColorPoint 和 Point 的两条原型链关系,即:
    • Object.setPrototypeOf(ColorPoint.prototype, Point.prototype)
    • Object.setPrototypeOf(ColorPoint, Point);
  2. 调用 Point.call(this, x, y),并根据 Point 构造函数的返回值类型,确定子类构造函数 this 的初始值 _this;
  3. 根据子类构造函数的内容,给 _this 添加实例属性,然后返回该值;
  4. 调用 _createClass(Constructor, protoProps, staticProps):分别调用 _defineProperties
    • _defineProperties(Constructor.prototype, protoProps),将 protoProps(原型方法)设置在 Constructor.prototype(类的原型对象) 上;
    • _defineProperties(Constructor, staticProps),将 staticProps(静态方法)设置在 Constructor(类)上;
  5. 最后,返回生成好的 ColorPoint 构造函数。

18. class

ES6 的 class 可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};

var p = new Point(1, 2);

用 ES6 的class改写,就是下面这样:

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}
var p = new Point(1, 2);

ES6 的类,完全可以看作构造函数的另一种写法。

class Point {
  // ...
}
typeof Point // "function"
Point === Point.prototype.constructor // true

上面代码表明,类的数据类型就是函数,类本身就指向构造函数

构造函数的prototype属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面。

class Point {
  constructor() {
    // ...
  }
  toString() {
    // ...
  }
  toValue() {
    // ...
  }
}

// 等同于
Point.prototype = {
  constructor() {},
  toString() {},
  toValue() {},
};

由于类的方法都定义在prototype对象上面,所以类的新方法可以添加在prototype对象上面。Object.assign方法可以很方便地一次向类添加多个方法。

class Point {
  constructor(){
    // ...
  }
}

Object.assign(Point.prototype, {
  toString(){},
  toValue(){}
});

另外,类的内部所有定义的方法,都是不可枚举的(non-enumerable)。

class Point {
  constructor(x, y) {
    // ...
  }
  toString() {
    // ...
  }
}

Object.keys(Point.prototype) // 获取对象自身的所有可枚举属性(不含Symbol属性);
// []
Object.getOwnPropertyNames(Point.prototype) // 对象自身的所有可枚举和不可枚举属性(不含Symbol属性)
// ["constructor","toString"]

上面代码中,toString方法是Point类内部定义的方法,它是不可枚举的。这一点与 ES5 的行为不一致

18.1 constructor

  • constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。如果子类没有定义constructor方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor方法。
    class Point {
    }
    // 等同于
    class Point {
      constructor() {}
    }
    
    class ColorPoint extends Point {
    }
    // 等同于
    class ColorPoint extends Point {
      constructor(...args) {
        super(...args);
      }
    }
    
  • constructor方法默认返回实例对象(即this),也可以指定返回另外一个对象。
    class Foo {
      constructor() {
        return Object.create(null);
      }
    }
    new Foo() instanceof Foo // false
    
    上面代码中,constructor函数返回一个全新的对象,结果导致实例对象不是Foo类的实例。
  • 在子类的构造函数中,必须调用 super() 方法,而且只有调用 super() 之后,才可以使用this关键字,否则会报错。

18.2 super 关键字

super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。

注意,使用super的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。

class A {}
class B extends A {
 constructor() {
   super();
   console.log(super); // 报错
 }
}

由于对象总是继承其他对象的,所以可以在任意一个对象中,使用super关键字。

var obj = {
  toString() {
    return "MyObject: " + super.toString();
  }
};

obj.toString(); // MyObject: [object Object]

作为函数调用

第一种情况,super作为函数调用时,代表父类的构造函数。作为函数时,super()只能用在子类的构造函数之中。

注意,super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B的实例,因此 super() 在这里相当于A.prototype.constructor.call(this)。

class A {
  constructor() {
    console.log(new.target.name);
  }
}
class B extends A {
  constructor() {
    super();
  }
}
new A() // A
new B() // B

作为对象

第二种情况,super作为对象时:

  • 在普通方法中,指向父类的原型对象
  • 在静态方法中,指向父类
class Parent {
  static myMethod(msg) {
    console.log('static', msg);
  }

  myMethod(msg) {
    console.log('prototype', msg);
  }
}

class Child extends Parent {
  static myMethod(msg) {
    super.myMethod(msg);
  }

  myMethod(msg) {
    super.myMethod(msg);
  }
}

Child.myMethod(1); // static 1

var child = new Child();
child.myMethod(2); // prototype 2

在普通方法中

class A {
  p() {
    return 2;
  }
}

class B extends A {
  constructor() {
    super();
    console.log(super.p()); // 2
  }
}

let b = new B();

上面代码中,子类B当中的super.p(),就是将super当作一个对象使用。这时,super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()。

由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的:

class A {
  constructor() {
    this.p = 2;
  }
}

class B extends A {
  get m() {
    return super.p;
  }
}

let b = new B();
b.m // undefined

上面代码中,p是父类A实例的属性,super.p就引用不到它。

ES6 规定,在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例。

class A {
  constructor() {
    this.x = 1;
  }
  print() {
    console.log(this.x);
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
  }
  m() {
    super.print();
  }
}

let b = new B();
b.m() // 2

实际上执行的是super.print.call(this)。

由于this指向子类实例,所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性。

class A {
  constructor() {
    this.x = 1;
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
    super.x = 3;
    console.log(super.x); // undefined
    console.log(this.x); // 3
  }
}

let b = new B();

上面代码中,super.x赋值为3,这时等同于对this.x赋值为3。而当读取super.x的时候,读的是A.prototype.x,所以返回undefined。

在静态方法之中

在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例。

class A {
  constructor() {
    this.x = 1;
  }
  static print() {
    console.log(this.x);
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
  }
  static m() {
    super.print();
  }
}

B.x = 3;
B.m() // 3

上面代码中,静态方法B.m里面,super.print指向父类的静态方法。这个方法里面的this指向的是B,而不是B的实例。

18.3 实例

  • 类必须使用new调用,否则会报错。
  • 与 ES5 一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。
    class Point {
      constructor(x, y) {
        this.x = x;
        this.y = y;
      }
      toString() {
        return '(' + this.x + ', ' + this.y + ')';
      }
    }
    
    var point = new Point(2, 3);
    point.toString() // (2, 3)
    
    point.hasOwnProperty('x') // true
    point.hasOwnProperty('y') // true
    point.hasOwnProperty('toString') // false
    point.__proto__.hasOwnProperty('toString') // true
    

实例属性

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

class Counter {
  count = 0;    // 这里
  increment() {
    this.count++;
  }
}

// 相当于
class Counter {
  constructor() {
    this.count = 0;
  }
  increment() {
    this.count++;
  }
}

18.4 get/set

与 ES5 一样,在“类”的内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

存值函数和取值函数是设置在属性的 Descriptor 对象上的

class CustomHTMLElment {
 constructor(element){
   this.element = element
 }
 get html(){
   return this.element.innerHTML
 }
 set html(value){
   this.element.innerHTML = value
 }
}

var descriptor = Object.getOwnPropertyDescriptor(
    CustomHTMLElment.prototype,
    'html'
)
console.log(descriptor) // {enumerable: false, configurable: true, get: ƒ, set: ƒ}
//  存值函数和取值函数是定义在html属性的描述对象上面
console.log('get' in descriptor)//true
console.log('set' in descriptor)//true

18.5 static

静态方法

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

class Foo {
  static classMethod() {
    return 'hello';
  }
}

Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod() // TypeError: foo.classMethod is not a function
  • 静态方法中的this指的是类,而不是实例;
  • 静态方法可以与非静态方法重名;
    class Foo {
      static bar() {
        this.baz();
      }
      static baz() {
        console.log('hello');
      }
      baz() {
        console.log('world');
      }
    }
    
    Foo.bar() // hello
    
  • 父类的静态方法,可以被子类继承;
  • 也可以在子类中的 super 对象上调用父类的静态方法;
    class Foo {
      static sayHi() {
        return 'hi';
      }
    }
    
    class Bar extends Foo {
      static sayHiBar() {
        return super.sayHi() + ', bar';
      }
    }
    
    Bar.sayHiBar() // "hi, bar"
    Bar.sayHi() // "hi"
    

静态属性

静态属性指的是 Class 本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性。-- 提案阶段

// 老写法
class Foo {
  // ...
}
Foo.prop = 1;

// 新写法
class Foo {
  static prop = 1;
}

18.6 私有方法和私有属性

在属性名、方法之前,使用 # 表示私有,私有属性只能在类的内部使用,如果在类的外部使用,就会报错。-- 提案阶段

class Foo {
  #a;
  #b;
  constructor(a, b) {
    this.#a = a;
    this.#b = b;
  }
  #sum() {
    return #a + #b;
  }
  printSum() {
    console.log(this.#sum());
  }
}

私有属性也可以设置 get 和 set 方法。

18.7 new.target 属性

ES6 为 new 命令引入了一个 new.target 属性,该属性一般用在构造函数之中,返回 new 命令作用于的那个构造函数。

function Person(name) {
  if (new.target === Person) {
    this.name = name;
  } else {
    throw new Error('必须使用 new 命令生成实例');
  }
}

var person = new Person('张三'); // 正确
var notAPerson = Person.call(person, '张三');  // 报错

Class 内部调用new.target,返回当前 Class。

class Rectangle {
  constructor(length, width) {
    console.log(new.target === Rectangle);
    this.length = length;
    this.width = width;
  }
}

class Square extends Rectangle {
  constructor(length) {
    super(length, length);
  }
}

var obj1 = new Rectangle(1, 2); // 输出 true
var obj2 = new Square(3, 4); // 输出 false

需要注意的是,子类继承父类时,new.target会返回子类。利用这个特点,可以写出不能独立使用、必须继承后才能使用的类。

class Shape {
  constructor() {
    if (new.target === Shape) {
      throw new Error('本类不能实例化');
    }
  }
}

class Rectangle extends Shape {
  constructor(length, width) {
    super();
    // ...
  }
}

var x = new Shape();  // 报错
var y = new Rectangle(3, 4);  // 正确

18.8 注意事项

  1. 类和模块的内部,默认就是严格模式
  2. 类不存在变量提升(hoist),这一点与 ES5 完全不同。
  3. 如果某个方法之前加上星号(*),就表示该方法是一个 Generator 函数。
  4. 在函数外部,使用new.target会报错。

19. module

// ES6模块
import { stat, exists, readFile } from 'fs';

上面代码的实质是从 fs 模块加载 3 个方法,其他方法不加载。

  • 这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载;
  • 由于 ES6 模块是编译时加载,使得静态分析成为可能;
  • ES6 的模块自动采用严格模式。ES6 模块之中,顶层的 this 指向 undefined,即不应该在顶层代码使用this。

19.1 用法

export

// 报错
function f() {}
export f;

// 写法一
export var m = 1;
export function f() {};

// 写法二
var m = 1;
function f() {}
export {m, f};

// 写法三:重命名
var m = 1;
function f() {}
export {m as n, f as foo};

需要特别注意的是,export 命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

import

1) 提升效果

import 命令具有提升效果,会提升到整个模块的头部,首先执行。

foo();
import { foo } from 'my_module';

这种行为的本质是,import命令是编译阶段执行的,在代码运行之前。

2) 不能使用表达式和变量

由于 import 是静态执行,所以不能使用表达式和变量

// 报错
import { 'f' + 'oo' } from 'my_module';

3) 加载 & 执行

  • import 语句会执行所加载的模块
    import 'lodash';
    
    上面代码仅仅执行lodash模块,但是不输入任何值。
  • import 多次,会加载两次,但只执行一次
    import 'lodash';
    import 'lodash';
    
    上面代码加载了两次lodash,但是只会执行一次。
  • import 语句是 Singleton 模式
    import { foo } from 'my_module';
    import { bar } from 'my_module';
    
    上面代码中,虽然 foo 和 bar 在两个语句中加载,但是它们对应的是同一个 my_module 实例。
  • import命令做不到动态加载
    const path = './' + fileName;
    const myModual = require(path);
    
    上面的语句就是动态加载,require到底加载哪一个模块,只有运行时才知道。因为require是运行时加载模块,import命令无法取代require的动态加载功能。

4) 整体加载(即,import * as)

// circle.js
export function area(radius) {
  return Math.PI * radius * radius;
}

export function circumference(radius) {
  return 2 * Math.PI * radius;
}
// main.js
import * as circle from './circle';

console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));

5) export default

为模块指定默认输出:

// export-default.js
export default function () {
  console.log('foo');
}

上面代码是一个模块文件export-default.js,它的默认输出是一个函数。

其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。

// import-default.js
import customName from './export-default';
customName(); // 'foo'

需要注意的是,这时 import 命令后面,不使用大括号。

  • 一个模块只能有一个默认输出,因此 export default 命令只能使用一次
  • 本质上,export default就是输出一个叫做default的变量或方法;
  • export default 也可以用来输出类
    // MyClass.js
    export default class { ... }
    
    // main.js
    import MyClass from 'MyClass';
    let o = new MyClass();
    

import()

一个提案,用于实现动态加载import() 返回一个 Promise 对象

const main = document.querySelector('main');

import(`./section-modules/${someVariable}.js`)
  .then(module => {
    module.loadPageInto(main);
  })
  .catch(err => {
    main.textContent = err.message;
  });
  • import() 类似于 Node 的 require 方法,区别主要是 import() 是异步加载,require 是同步加载

  • import() 加载模块成功以后,这个模块会作为一个对象,当作 then 方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口:

    import('./myModule.js')
    .then(({export1, export2}) => {
      // ...·
    });
    
  • 如果模块有 default 输出接口,可以用参数直接获得:

    import('./myModule.js')
    .then(myModule => {
      console.log(myModule.default);
    });
    

    上面的代码也可以使用具名输入的形式。

    import('./myModule.js')
    .then(({default: theDefault}) => {
      console.log(theDefault);
    });
    
  • 同时加载多个模块

    Promise.all([
      import('./module1.js'),
      import('./module2.js'),
      import('./module3.js'),
    ])
    .then(([module1, module2, module3]) => {
       ···
    });
    
  • 在 async 函数中使用

    async function main() {
      const myModule = await import('./myModule.js');
      const {export1, export2} = await import('./myModule.js');
      const [module1, module2, module3] =
        await Promise.all([
          import('./module1.js'),
          import('./module2.js'),
          import('./module3.js'),
        ]);
    }
    main()
    

19.3 Module 加载

加载规则

默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到<script>标签就会停下来,等到执行完脚本,再继续向下渲染。如果是外部脚本,还必须加入脚本下载的时间。

传统方式

如果脚本体积很大,下载和执行的时间就会很长,因此造成浏览器堵塞,用户会感觉到浏览器“卡死”了,没有任何响应。这显然是很不好的体验,所以浏览器允许脚本异步加载,下面就是两种异步加载的语法:

<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>

ES6 方式

浏览器加载 ES6 模块,也使用<script>标签,但是要加入type="module"属性

<script type="module" src="./foo.js"></script>

  • 浏览器对于带有type="module"的<script>,都是异步加载,等同于打开了<script>标签的defer属性。
    <script type="module" src="./foo.js"></script>
    
    等同于
    <script type="module" src="./foo.js" defer></script>
    
  • 与 defer 不同,允许使用在内嵌脚本上
    <script type="module">
      import utils from "./utils.js";
      // ..
    </script>
    

Module 与 CommonJS 的区别

它们有两个重大差异。

  • ==CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。==
    // lib.js
    var counter = 3;
    function incCounter() {
      counter++;
    }
    module.exports = {
      counter: counter,
      incCounter: incCounter,
    };
    
    
    // main.js
    var mod = require('./lib');
    
    console.log(mod.counter);  // 3
    mod.incCounter();
    console.log(mod.counter); // 3
    
    这是因为mod.counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值:
    // lib.js
    var counter = 3;
    function incCounter() {
      counter++;
    }
    module.exports = {
      get counter() {
        return counter
      },
      incCounter: incCounter,
    };
    
    而ES6 模块输入的变量counter是活的,不会缓存运行结果,而是动态地去被加载的模块取值,并且变量总是绑定同一实例上。
    // lib.js
    export let counter = 3;
    export function incCounter() {
      counter++;
    }
    
    // main.js
    import { counter, incCounter } from './lib';
    console.log(counter); // 3
    incCounter();
    console.log(counter); // 4
    
  • ==CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。==

第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

20. Proxy

20.1 概述

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

var proxy = new Proxy(target, handler);

Proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。其中,new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。 例如:

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


obj.count = 1
//  setting count!
++obj.count
//  getting count!
//  setting count!
//  2

注意,要使得Proxy起作用,必须针对Proxy实例(上例是proxy对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。如果handler没有设置任何拦截,那就等同于直接通向原对象。

Proxy 支持的拦截操作一览,一共 13 种:

  1. get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy['foo']。

  2. set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值。严格模式下,set代理如果没有返回true,就会报错。

  3. has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。has方法不论一个属性是对象自身的属性,还是继承的属性,都会拦截。注意:has拦截对for...in循环不生效。

  4. deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。

  5. ownKeys(target):拦截对象自身属性的读取操作。返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。具体来说,拦截以下操作:

    • Object.getOwnPropertyNames(proxy)
    • Object.getOwnPropertySymbols(proxy)
    • Object.keys(proxy)
    • for...in循环
  6. getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。

  7. defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。

  8. preventExtensions(target):拦截 Object.preventExtensions(proxy),返回一个布尔值。只有目标对象不可扩展时(即 Object.isExtensible(proxy)为 false),proxy.preventExtensions 才能返回 true,否则会报错。

  9. getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。

  10. isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。

  11. setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。

  12. apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。直接调用Reflect.apply方法,也会被拦截。

  13. construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。

20.2 拦截多个操作

同一个拦截器函数,可以设置拦截多个操作:

var handler = {
  get: function(target, name) {
    if (name === 'prototype') {
      return Object.prototype;
    }
    return 'Hello, ' + name;
  },

  apply: function(target, thisBinding, args) {
    return args[0];
  },

  construct: function(target, args) {
    return {value: args[1]};
  }
};

var fproxy = new Proxy(function(x, y) {
  return x + y;
}, handler);

fproxy(1, 2) // 1
new fproxy(1, 2) // {value: 2}
fproxy.prototype === Object.prototype // true
fproxy.foo === "Hello, foo" // true

20.3 Proxy.revocable()

Proxy.revocable方法返回一个可取消的 Proxy 实例。

let target = {};
let handler = {};

let {proxy, revoke} = Proxy.revocable(target, handler);

proxy.foo = 123;
proxy.foo // 123

revoke();
proxy.foo // TypeError: Revoked

Proxy.revocable方法返回一个对象,该对象的proxy属性是Proxy实例,revoke属性是一个函数,可以取消Proxy实例。上面代码中,当执行revoke函数之后,再访问Proxy实例,就会抛出一个错误。

Proxy.revocable的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。

20.4 this 指向

在 Proxy 代理的情况下,目标对象内部的this关键字会指向 Proxy 代理。

const target = {
  m: function () {
    console.log(this === proxy);
  }
};
const handler = {};

const proxy = new Proxy(target, handler);

target.m() // false
proxy.m()  // true

21. Reflect

21.1 概述

Reflect 对象与 Proxy 对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect对象的设计目的有这样几个:

  1. 将 Object 对象的一些明显属于语言内部的方法(比如 Object.defineProperty ),放到 Reflect 对象上;有了Reflect对象以后,很多操作会更易读:
    // 老写法
    Function.prototype.apply.call(Math.floor, undefined, [1.75]) // 1
    
    // 新写法
    Reflect.apply(Math.floor, undefined, [1.75]) // 1
    
  2. 修改某些 Object 方法的返回结果,让其变得更合理:
    // 老写法
    try {
      Object.defineProperty(target, property, attributes);
      // success
    } catch (e) {
      // failure
    }
    
    // 新写法
    if (Reflect.defineProperty(target, property, attributes)) {
      // success
    } else {
      // failure
    }
    
  3. 让Object操作都变成函数行为:
    // 老写法
    'assign' in Object // true
    
    // 新写法
    Reflect.has(Object, 'assign') // true
    
  4. Reflect 对象的方法与 Proxy 对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。
    var loggedObj = new Proxy(obj, {
      get(target, name) {
        console.log('get', target, name);
        return Reflect.get(target, name);
      },
      deleteProperty(target, name) {
        console.log('delete' + name);
        return Reflect.deleteProperty(target, name);
      },
      has(target, name) {
        console.log('has' + name);
        return Reflect.has(target, name);
      }
    });
    

21.2 静态方法

Reflect 对象一共有 13 个静态方法,与 Proxy 对象的方法是一一对应的:

  1. Reflect.apply(target, thisArg, args):等同于 Function.prototype.apply.call(func, thisArg, args),用于绑定 this 对象后执行给定函数;
  2. Reflect.construct(target, args)
  3. Reflect.get(target, name, receiver)
  4. Reflect.set(target, name, value, receiver)
  5. Reflect.defineProperty(target, name, desc)
  6. Reflect.deleteProperty(target, name)
  7. Reflect.has(target, name)
  8. Reflect.ownKeys(target)
  9. Reflect.isExtensible(target)
  10. Reflect.preventExtensions(target)
  11. Reflect.getOwnPropertyDescriptor(target, name)
  12. Reflect.getPrototypeOf(target)
  13. Reflect.setPrototypeOf(target, prototype)

21.3 使用 Proxy 实现观察者模式

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

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;
}


const person = observable({
  name: '张三',
  age: 20
});

function print() {
  console.log(`${person.name}, ${person.age}`)
}

observe(print);
person.name = '李四';
// 输出
// 李四, 20

上面代码中,先定义了一个 Set 集合,所有观察者函数都放进这个集合。然后,observable 函数返回原始对象的代理,拦截赋值操作。拦截函数 set 之中,会自动执行所有观察者。

引用链接:

  1. Object.assign()
  2. 如何实现一个深拷贝
  3. 深拷贝的终极探索
  4. 如何写出一个惊艳的深拷贝?
  5. JS原型链与继承别再被问倒了
  6. Object.create()
  7. 《JavaScript高级程序设计》
  8. ECMAScript 6 入门
  9. ES6之类class的原理解析
  10. Babel编译:类继承
  11. Class Fields