浅读原型链~

写在开头

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

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

概念

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

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

  2. 原型链

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

四个规则

引用类型的四个规则

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

    js 复制代码
    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__ 属性,属性值是一个普通的对象

    js 复制代码
    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__)
  • 引用类型,隐式原型 __proto__ 的属性值指向他的构造函数的显式原型 prototype 属性值

    js 复制代码
    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__ 中寻找

    js 复制代码
    const obj = { a: 1 }
    obj.toString
    // ƒ toString() { [native code] }

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

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

一个特例

js 复制代码
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 方法。

一张图片

原型上的方法

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

手写 instanceof

js 复制代码
// 变量 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__ // 继续向上查找
    }
}

再来看一段代码

js 复制代码
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 方法用于检测一个对象是否含有特定的自身属性(即,不能从原型上继承的属性)。

js 复制代码
console.log(instance1.hasOwnProperty('name')) // true

isPrototypeOf

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

js 复制代码
console.log(Father.prototype.isPrototypeOf(instance1)) // true

原型链的问题

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

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

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

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

js 复制代码
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"]

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

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

组合继承

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

js 复制代码
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() 函数内部,先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。

js 复制代码
function createObject(o) {
    function F() {
    }

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

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

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

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

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

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

寄生式继承

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

寄生组合式继承

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

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

js 复制代码
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 运算符

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

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

  • 创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型。
  • 属性和方法被添加到 this 引用的对象上。
  • 新创建的对象由 this 所引用,并且最后隐式的返回 this。
相关推荐
学习2年半几秒前
汇丰eee2
前端·spring
代码续发2 分钟前
Vue进行前端开发流程
前端·vue.js
zpjing~.~5 分钟前
CSS &符号
前端·css
冴羽41 分钟前
SvelteKit 最新中文文档教程(19)—— 最佳实践之身份认证
前端·javascript·svelte
拉不动的猪43 分钟前
ES2024 新增的数组方法groupBy
前端·javascript·面试
huangkaihao1 小时前
单元测试 —— 用Vitest解锁前端可靠性
前端
archko1 小时前
telophoto源码查看记录
java·服务器·前端
倒霉男孩1 小时前
HTML5元素
前端·html·html5
柯南二号2 小时前
CSS 学习提升网站或者项目
前端·css
tryCbest2 小时前
Vue2-实现elementUI的select全选功能
前端·javascript·elementui