浅读原型链~

254 阅读6分钟

写在开头

在刷掘金的时候,看到大佬在一篇文章下的评论,好精辟

  • 所有的构造函数都是 Function 的实例
  • 所有原型对象都是 Object 的实例,除了 Object.prototype

概念

  1. 构造函数、原型和实例的关系

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

  2. 原型链

    在对象向上查找过程中,prototype 在这个规则中充当链接的作用,于是我们把这种实例与原型的链条称为原型链

四个规则

引用类型的四个规则

  • 引用类型,都具有对象特性,即可自由扩展属性。

    const obj = {}
    const arr = []
    const fn = function () {}
    
    obj.a = 1
    arr.a = 1
    fn.a = 1
    
    console.log(obj.a) // 1
    console.log(arr.a) // 1
    console.log(fn.a)  // 1
    
  • 引用类型,都有一个隐式原型 __proto__ 属性,属性值是一个普通的对象

    const obj = {}
    const arr = []
    const fn = function () {}
    
    console.log('obj.__proto__', obj.__proto__)
    console.log('arr.__proto__', arr.__proto__)
    console.log('fn.__proto__', fn.__proto__)
    

img.png

  • 引用类型,隐式原型 __proto__ 的属性值指向他的构造函数的显式原型 prototype 属性值

    const obj = {}
    const arr = []
    const fn = function () {}
     
    console.log(obj.__proto__ === Object.prototype) // true
    console.log(arr.__proto__ === Array.prototype) // true
    console.log(fn.__proto__ === Function.prototype) // true
    
  • 当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么他会去查询它的隐式原型 __proto__ 中寻找

    const obj = { a: 1 }
    obj.toString
    // ƒ toString() { [native code] }
    

    obj 本身没有 toString 属性,所以会去查询它的隐式原型 __proto__ 中寻找,在 Objectprototype 找到了,所以 obj.toString 的值就是 toString 属性的值。

引用类型:Object、Array、Function、Date、RegExp。

一个特例

function Person(name) {
    this.name = name
}

let nick = new Person('nick')
console.log(nick.toString) // ƒ toString() { [native code] }

正常来说,nickPerson 构造函数生成的实例,而 personprototype 并没有 toString 方法,但是为什么 nick 能获取到 toString 方法呢?

这里就引出了 原型链 的概念了,nick 的实例先从自身出发检讨自己,发现并没有 toString 方法。找不到,就往上找,找 Person 构造函数的 prototype 属性值,还是没有找到。构造函数的 prototype 也是一个对象,对象的构造函数是 Object, 所以就找到了 Object.prototype 下的 toString 方法。

img_1.png

一张图片

img_2.png

原型上的方法

instanceof 运算符用于测试构造函数的 prototype 属性是否出现在对象原型链中的任何位置。

手写 instanceof

// 变量 right 的原型是否存在于 left 的原型链上
function instance_of(left, right) {
    // 验证如果为基本数据类型,则直接返回 false
    const baseType = ['string', 'number', 'boolean', 'undefined', 'symbol']
    if (baseType.includes(typeof left)) return false

    let RightP = right.prototype;
    let LeftP = left.__proto__;

    while (true) {
        if (LeftP === null) {
            return false // 代表找到了最顶层
        }
        if (LeftP === RightP) { // 严格相等
            return true
        }
        LeftP = LeftP.__proto__ // 继续向上查找
    }
}

再来看一段代码

function Foo(name) {
    this.name = name
}

let f = new Foo('nick')

f instanceof Foo // true
f instanceof Object // true

尝试理解一下判断流程

  1. f instanceof Foo: f 的隐式原型 __proto__Foo.prototype 是同一个对象,所以返回 true
  2. f instanceof Object: f 的隐式原型 __proto__Object.prototype 不等,所以继续向上查找,f 的隐式原型 __proto__ 指向 Foo.prototype,所以继续用 Foo.prototype 的隐式原型 __proto__ 去对比 Object.prototype,所以返回 true

hasOwnProperty

hasOwnProperty 方法用于检测一个对象是否含有特定的自身属性(即,不能从原型上继承的属性)。

console.log(instance1.hasOwnProperty('name')) // true

isPrototypeOf

isPrototypeOf 方法用于测试该对象是否为指定对象的原型。

console.log(Father.prototype.isPrototypeOf(instance1)) // true

原型链的问题

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

可以尝试弥补原型链的不足

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

在子类型构造函数的内部调用超类型构造函数。

function Father() {
    this.colors = ['red', 'blue', 'green'];
}

function Son() {
    Father.call(this); // 继承了 Father 的属性和方法, 且可以向父类型传递参数
}

let instance1 = new Son();
instance1.colors.push('black');
console.log(instance1.colors); // ["red", "blue", "green", "black"]

let instance2 = new Son();
console.log(instance2.colors); // ["red", "blue", "green"]

借用构造函数解决了原型链的两大问题

  • 保证了原型链中引用类型的值的独立,不再被所有实例所共享。
  • 可以向超类型构造函数中传递参数。

组合继承

使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。 既通过在原型上定义方法实现了函数复用,又能保证每个实例都有她自己的属性。

function Father(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

Father.prototype.sayName = function () {
    console.log(this.name);
}

function Son(name, age) {
    Father.call(this, name); // 继承示例属性,第一次调用 Father()
    this.age = age;
}

Son.prototype = new Father(); // 继承父类方法,第二次调用 Father()
Son.prototype.sayAge = function () {
    console.log(this.age);
};

let instance1 = new Son('nick', 18);
instance1.colors.push('black');
console.log(instance1.colors); // ["red", "blue", "green", "black"]
instance1.sayName(); // nick
instance1.sayAge(); // 18

let instance2 = new Son('jack', 20);
console.log(instance2.colors); // ["red", "blue", "green"]
instance2.sayName(); // jack
instance2.sayAge(); // 20

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

原型继承

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

function createObject(o) {
    function F() {
    }

    F.prototype = o;
    return new F();
}

本质上讲,createObject() 函数就是返回了一个引用传入对象的新对象。这样可能会有共享数据的问题。

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

Object.create() 方法接受两个参数:

  • 一个用作新对象原型(prototype)的对象
  • (可选) 一个为新对象定义额外属性的对象,这个对象与 Object.defineProperties() 方法的参数对象格式相同。以这种方式指定的任何属性都会覆盖新对象原型对象上的同名属性。

原型继承中,包含引用类型值的属性始终都会共享相应的值

寄生式继承

寄生式继承就是创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再返回对象。

寄生组合式继承

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

基本思想:不必为了指定子类型的原型而调用超类型的构造函数

function extend(subClass, superClass) {
    // 创建了一个新对象,这个对象的原型是父类的 superClass.prototype, 也就是说 `prototype` 现在是一个继承了 superClass.prototype 的新对象。
    let prototype = Object.create(superClass.prototype);
    // 默认情况下,使用 Object.create() 创建的对象的 constructor 会指向 superClass,因此我们需要将它重新设置回 subClass,保持构造函数引用的正确性。
    prototype.constructor = subClass;
    // 把 subClass.prototype 设置为构建好的 prototype,这样就完成了继承关系的建立。
    subClass.prototype = prototype;
}

new 运算符

let obj = {};
obj.__proto__ = F.prototype;
F.call(obj);
  • 第一行,创建了一个空对象obj。
  • 第二行,我们将这个空对象的__proto__设置为了F.prototype。
  • 第三行,我们将F函数对象的this指向了obj,然后再调用F函数。

在 new 操作符的作用下,实际上发生了以下事情:

  • 创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型。
  • 属性和方法被添加到 this 引用的对象上。
  • 新创建的对象由 this 所引用,并且最后隐式的返回 this。