前端基础:原型与原型链

455 阅读5分钟

前言

为什么要明白原型原型链?只有明白了原型与原型链,才会明白prototype、constructor与__proto__的指向关系,进而才会明白new关键字原理,call()、apply()、bind()是如何改变this指向的。 跟着我的这篇文章一起学习吧,不懂得欢迎交流✌

定义

什么是原型

原型prototype), 常用于javascript的继承

什么是原型链

引用MDN上的解释:

对于继承时,js只有一种结构就是对象,每个实例对象都有一个私有属性(__proto__)指向构造函数原型对象(prototype),该原型对象也有自己的私有属性(__proto__)指向它的构造函数的原型对象,层层向上,直到一个对象的原型对象为 null,根据定义null没有原型,就作为原型链的最后一个环节。

对于原型需要知道几个知识点__proto__prototypeconstructor 举例:

let Func = function () {
    // 添加函数自有属性
    this.a = 10;
    this.b = 20;
};
// 在原型上添加属性
Func.prototype.a2 = 30;
Func.prototype.b2 = 40;

let fn = new Func(); // {a: 10, b: 20}
fn.a2; // 30

对于上面的例子,先要知道这些预备知识:

  1. 通过new调用的函数叫构造函数(constructor), 即 Func 可以叫构造函数。
  2. 通过new函数产生的值不叫返回值而是叫实例对象, 即 fn。
  3. 每个实例对象都有__proto__constructor, 即 fn 有__proto__,constructor。
  4. 函数也是对象,所以函数也有__proto__constructor, 即 Func 也会有__proto__,constructor。

1. __proto__ 属性

实例对象有的属性。它的作用是找到构造函数的prototype,就是找到谁 new 出来的prototype

但这有什么用呢?当你访问一个属性的时候,浏览器会先查找自身是否有这个属性,然后在__proto__上查找这个属性(也就是构造函数的.prototype),如果有则返回,否则,继续通过实例的__proto____proto__查找。否则 继续通过实例的__proto____proto____proto__查找,直到找到或者__proto__的值为nullundfined查找则停止。

看下实例对象的打印:

function Func(){}
Func.prototype.foo = "bar";

let funcInstancing = new Func()
funcInstancing.name = 'haha'
console.log(funcInstancing)
/*
输出:
{
    name: "haha",
    __proto__: {
        foo: "bar",
        constructor: ƒ Func(),
        __proto__: {
            constructor: ƒ Object(),
            hasOwnProperty: ƒ hasOwnProperty(),
            isPrototypeOf: ƒ isPrototypeOf(),
            propertyIsEnumerable: ƒ propertyIsEnumerable(),
            toLocaleString: ƒ toLocaleString(),
            toString: ƒ toString(),
            valueOf: ƒ valueOf()
        }
    }
}
*/

根据对象的 proto 指向构造函数的 prototype,测试__proto__prototype的关系:

let Func = function () {}; // 等价于  let Func = new Function()
let fn = new Func(); 

// fn是通过Func new出来的,所以fn的构造函数是Func
fn.__proto__ == Func.prototype // true

// Func也是对象,所以Func也有__proto__, Func 是通过 new Function 出来的,所以Func的构造函数是Function
Func.__proto__ == Function.prototype // true

__proto__只要记住一句话:它的作用是找到构造函数的prototype

2. prototype 属性

原型,是函数独有的属性。它的作用是希望被原型链下游的对象继承的属性和方法,都被储存在其中。

在你创建函数的时候会默认给你的函数挂载了一个prototype,prototype的值是一个对象。是对象就会有__proto__constructor,prototype也不例外。

看下函数.prototype输出:

function Func () {  this.a = 10; this.b = 20; };
// 在原型上添加属性
Func.prototype.a2 = 30;
Func.prototype.b2 = 40;

console.log(Func.prototype) 
/*
输出:
{
    a2: 30,
    b2: 40, 
    constructor: ƒ Func(),
    __proto__: {..}
}
*/

可以看到 prototype 的值好比是一个对象,这个对象上有我们自己添加的属性,还有constructor__proto__属性,所以我理解是这个对象类似 new Object() 这样帮我们创建的,所以猜测函数原型的 __proto__ 是指向 Object.prototype

MDN说法:

默认情况下,任何函数的原型属性 __proto__ 都是 window.Object.prototype

意思就是 函数 . prototype.__proto__ 指向的是 Object.prototype

测试一下:

Func.prototype.__proto__ == Object.prototype // true

let fn = new Func();
fn.a2 // 30
fn.toString()  //'[object Object]'
  • fn.a2 是 fn proto 找Func的原型上继承到的
  • fn.toString(),浏览器会先找fn自身发现没有通过__proto__找到Func.prototype,发现这上面也没有,继续通过Func.prototype.__proto__找到Object.prototype 发现该原型上含有toString方法即调用Object的toString方法。

先总结一下:

因为JavaScript中万物皆对象即都含有`__proto__`

先找实例本身 -> 实例.__proto__上查找 -> 实例.__proto__.__proto__ 上查找 -> null

先找实例本身 -> 构造函数.prototype上查找 -> Object.prototype上查找 -> null

3. constructor 属性

MDN官方说法:

每个实例对象都从原型中继承了一个constructor属性,该属性指向了用于构造此实例对象的构造函数。

可理解为:实例对象的constructor都是从构造函数的prototype的constructor上获取的

看代码:

function Func () {}

let fn = new Func()
console.log(fn.constructor)  // ƒ Func () { }
console.log(Func.prototype.constructor)  // ƒ Func () { }
fn.constructor == Func.prototype.constructor // true

// 仅仅为了测试
Func.prototype.constructor = 'hahah'
console.log(fn.constructor) // hahah

// 并不是 实例对象.constructor 直接指向 构造函数
// 因为
fn.constructor == Func.prototype.constructor // true
Func.prototype.constructor == Func // true

// 所以 实例对象.constructor 才会 指向构造函数
fn.constructor == Func // true

还记得,按照之前推理的 函数的prototype是一个对象相当于 new Object(...) `这样创建出来的。

所以函数.prototype.constructor 应该是 Object 函数吧?,但为什么是 Func.prototype.constructor == Func // true 这样呢

因为内部又做了 A.prototype.constructor=A 这样的修正。才使得构造函数的prototype的constructor指向自身。

所以这两句话才是真正的constructor:

构造函数的prototype的constructor指向自身,实例对象的constructor都是从构造函数的prototype的constructor上获取的。

总结

预备知识:

  1. 通过new调用的函数叫构造函数(constructor)。
  2. 通过new函数产生的叫实例对象
  3. 每个实例对象都有__proto__constructor
  4. 函数也是对象,所以函数也有__proto__constructor

原型链知识:

  • __proto__ 指向构造函数的prototype
  • prototype是函数独有的,函数.prototype 的值也是对象
  • 函数.prototype.proto 指向 Object.prototype
  • constructor指向构造函数的.prototype.constructor
  • A.prototype.constructor=A

探索使用new有什么不一样?

查看示例
let f = function () {
    // 添加函数自有属性
    this.a = 10;
    this.b = 20;
};

f.prototype.a2 = 10;
f.prototype.b2 = 20;

// 不通过new 
let res1 = f(); // undefined
res1.a  // 报错: TypeError: Cannot read properties of undefined (reading 'a')
res1.a2 // 报错:TypeError: Cannot read properties of undefined (reading 'a2')

// 通过new 
let res2 = new f(); // {a: 10, b: 20}
res2.a  // 10
res2.a2 // 10

// 返回对象
let f =  function () {
    ...
    return {}
};

// 通过new 
let res3 = new f(); // {}
res3.a  // undefined
res3.a2 // undefined

new的实现原理

  • new操作符返回的是一个对象。
  • 对象的原型,指向的是构造函数的原型
  • 如果构造函数有return的话,需要对return的进行判断,如果是对象,那么用函数return的,如果不是对象,那么直接返回新创建的对象
查看答案
function MyNew (fn, ...args) {
    let obj = {}
    obj.__proto__ = fn.prototype
    let res = fn.apply(obj, args)
    return res instanceof Object ? res : obj;
}
// 不推荐直接更改 __proto__
//优化后:
function MyNewPro (fn, ...args) {
    let obj = Object.create(fn.prototype)
    let res = fn.apply(obj, args)
    return res instanceof Object ? res : obj;
}

相关