amandakelake/blog

深入原型和原型链

amandakelake opened this issue · 4 comments

525140ed-2f51-44d6-93c7-e10fa7b9c4f1
这图很经典,希望先试着解读每个箭头的含义

对象都是通过函数创建的,函数是对象(剪不断,理还乱,是离愁,别是一番滋味在心头)

一、Prototype

每个函数都有这属性,有且仅有函数才拥有该属性
但有个例外:

// 用这个方法创建的函数是不具备prototype属性的
let fun = Function.prototype.bind()

Prototype属性里面默认有一个属性constructor,对应着构造函数

constructor是一个公有的而且不可枚举的属性,一旦改写了函数的Prototype,新对象就没有这个属性了
4e7d3878-1b22-44d9-a3bc-26965bbbf9e9

二、__proto__

每个对象都有一个隐藏属性__proto__,这个属性引用了创建这个对象的构造函数的原型
fn.__proto__ === Fn.prototype

__proto__ 将对象和原型连接起来组成了原型链

有三个要注意的地方

1、Object.prototype.__proto__ === null

对象的原型的__proto__属性是null

2、Object.__proto__ === Function.prototype

既然对象是函数创建的,那么对象的__proto__要指向创建它的构造函数的原型

3、Function的特殊性Function.__proto__ === Function.prototype

函数也是被Function创建的,那么函数的__proto__也应该指向Function的原型,这是一个环形结构,函数的prototype__proto__属性指向同一个对象

Function.prototypeObject.prototype 是两个特殊的对象,他们由引擎来创建

三、new

除了以上Function.prototypeObject.prototype两个特殊对象,其他对象都是通过构造器 new 出来的

先看看new的实现过程

  • 声明一个中间对象;
  • 将该中间对象的原型指向构造函数的原型;
  • 将构造函数的this,指向该中间对象;
  • 返回该中间对象,即返回实例对象。
function create() {
  // 创建一个新的对象
  let obj = new Object();
  // 取出第一个参数,该参数就是我们将会传入的构造函数
  // arguments会被shift去除第一个参数
  let Constructor = [].shift.call(arguments);
  // 将obj的原型指向构造函数,此时obj可以访问构造函数原型中的属性
  obj.__proto__ = Constructor.prototype;
  // 改变构造函数的this的指向,使其指向obj
  // 此时obj也可以访问构造函数中的属性了
  let result = Constructor.apply(obj, arguments);
  // 确保 new 出来的是个对象
  // 返回的值是什么就return什么出来
  return typeof result === 'object' ? result : obj 
}

所以平时更推荐使用字面量的方式创建对象,因为使用new Object()需要通过作用域链往上层层寻找Object

因为存在我们创建了一个同名的构造函数Object()的可能,当调用Object()的时候,解析器需要顺着作用域链从当前作用域开始查找,如果在当前作用域找到了名为Object()的函数就执行,如果没找到,就继续顺着作用域链往上照,直到找到全局Object()构造函数为止

四、原型链

上面说__proto__ 将对象和原型连接起来组成了原型链

具体起来说就是:访问一个对象的属性时,先在基本属性中查找,如果没有,再沿着__proto__这条链向上找,这就是原型链

来看个例子

function Foo() {};
let f1 = new Foo();

f1.a = 1;
Foo.prototype.a = 10;
Foo.prototype.b = 20;

// 打印自身属性和原型属性
for(const item in f1) {
  console.log(item)
}

// 打印自身属性
for (const item1 in f1) {
  if (f1.hasOwnProperty(item1)) {
    console.log(item1);  
  }
}

2dc87a25-f091-429d-940c-a7b11c668f03

稍等一下,hasOwnProperty这个方法从哪找的?
f1Foo.prototype都没有啊

很好,它从Object.prototype继承而来的,由于__proto__的存在,可以查找下面这条链条
f1 => Foo.prototype => Object.prototype
最终找到了Object.prototype上定义的各种原生方法
e149e038-97b0-4d6e-9eb0-0e5cf47ab1b4

那我们平时看见的callapply,还有数组的一些方法呢?
它们分别在Function.prototypeArray.prototype上面,我们日常使用的原生方法都继承于这几个对象,可以尝试去浏览器控制台打印这两看看

console.dir(Function.prototype);
console.dir(Array.prototype)

原型、原型链我说清楚了吗?

参考资料
深入理解javascript原型和闭包(1)——一切都是对象 - 王福朋 - 博客园
深度解析原型中的各个难点 · Issue #2 · KieSun/Blog · GitHub

加个微信吧!

eyea commented
Prototype属性里面默认有一个属性constructor,对应着构造函数

这句话读了两三遍,这样可能更好一点

构造函数的原型属性里面默认有一个constructor属性,指想构造函数
eyea commented
很好,它从Object.prototype继承而来的,由于__proto__的存在,可以查找下面这条链条
f1 => Foo.prototype => Object.prototype

这个应该是

很好,它从Object.prototype继承而来的,由于__proto__的存在,可以查找下面这条链条
f1 => Foo.__proto__=> Object.prototype
很好,它从Object.prototype继承而来的,由于__proto__的存在,可以查找下面这条链条
f1 => Foo.prototype => Object.prototype

这个应该是

很好,它从Object.prototype继承而来的,由于__proto__的存在,可以查找下面这条链条
f1 => Foo.__proto__=> Object.prototype

能加一下你微信吗?