原型链详解

原型链详解

原型链详解

一、什么是原型链

1994 年,网景公司(Netscape)发布了 Navigator 浏览器 0.9 版后,意识到 Web 需要变得更加动态。该公司的创始人马克·安德森(Marc Andreessen)认为 HTML 需要一种“胶水语言”,Web 设计人员和兼职程序员可以轻松地使用它们来组装诸如图像和插件之类的组件,这些代码可以直接在网页中编写。 1995 年,该公司招募了 Brendan Eich,其目标是创建一个脚本语言将补充 Java,并且应具有相似的语法。艾克(Eich)在 1995 年 5 月的 10 天内就发明了一个。尽管该语言是以 Mocha 的名称开发的,但该语言在 1995 年 9 月首次发布于 Netscape Navigator 2.0 的 Beta 版本中时正式被称为 LiveScript,但当它于 12 月在 Netscape Navigator 2.0 beta 3 中进行部署时被重新命名为 JavaScript。

Javascript 和基于类的语言(Java、C#)不同,它参考了 Self 语言(一种基于原型的面向对象程序设计语言),通过原型来实现继承(方法或属性的共享),确切地说是委托。

当在某个对象上的方法或属性不存在时,会在它的原型上去查找,如还不存在就会去它原型的原型上查找,这样形成一条链路就是原型链。

可以理解为下文要提到的 person.proto.proto.proto

二、JS 创建对象的三种方式

有人说 JS 一切皆对象,是错误的!原始值就不是。
那么对象如何创建呢?

2.1 使用 new 加 函数创建对象

1
2
3
4
5
var person = new Object();
person.name = "lili";
person.sayHello = function () {
console.log("Hello!");
};

2.2 使用字面量创建对象

1
2
3
4
5
6
var person = {
​ name: 'lili',
​ sayHello: function () {
console.log('Hello!')
​ }
}

注:var person = {} 等同于 var person = new Object()。

2.3 使用 Object.create()方法创建对象

1
2
3
4
5
6
7
8
9
var person = {
​ name: 'lili',
​ sayHello: function () {
console.log('Hello!')
​ }
}

var me = Object.create(person);
me.name = 'wangwang';

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__,即: me.__proto__=== person。

三、构造函数

3.1 构造函数是什么?

通过 new 函数名来实例化对象的函数叫构造函数。

任何的函数都可以作为构造函数。

之所以有构造函数与普通函数之分,主要从功能上进行区别的,构造函数的主要功能为初始化对象,特点是和 new 一起使用。构造函数可以为初始化的对象添加属性和方法。

下面我们从例子看下构造函数。

1
2
3
function Person() {}
var person1 = new Person();
var person2 = new Person();

上面的 person1 和 person2 都是通过 Person 函数实例化出来的。这个 Person 函数就是当前 person1 和 person2 的构造函数。对象上的 constructor 属性可以指明这个对象的构造函数是什么。

1
2
person1.constructor === Person; // true
person2.constructor === Person; // true

对于上面的 Person 函数来说,本身也是一个对象,那么这个对象是如何实例化出来的呢? 它的 constructor 又指向谁呢?

那就要说到创建函数对象的方法。

1
2
3
4
5
6
function foo() {
console.log("你好");
}
var foo1 = new Function('console.log("你好2")');
foo();
foo1();

Person 函数是由 JS 内置函数 Function 函数实例化,Person.constructor === Function。而 Function 本身还是个构造函数,它的 constructor 是本身。

3.2constructor 属性在哪里

当你打开编辑器,按我上面的例子去打印上面的例子中的 person1,查看它的 constructor 属性,会发现看不见 constructor 属性呢?打印如下:

为什么呢,明明可以访问到啊(person2.constructor === Person),这个问题先留着,接着往下看。

四、prototype 是什么,有什么用呢

4.1prototype 的意义

上面的通过 new 创建实例的方法中我们如何做到共享属性和方法呢?

比如,所有的实例都具有黑色头发这个属性,都有可以说话这个方法,那么我们通过上面的方法创建实例,如何添加这个属性和方法呢。代码如下:

1
2
3
4
5
6
7
8
9
10
11
function Person() {}
var person1 = new Person();
var person2 = new Person();
person1.hairColor = "black";
person2.hairColor = "black";
person1.sayHello = function () {
console.log("Hello!");
};
person2.sayHello = function () {
console.log("Hello!");
};

如果,我现在要修改,所有这些实例的对象的头发都是红色,怎么办?又去每个对象都修改为红色?代码冗余。有人说,那我可以把属性的复制放在构造函数中完成,比如:

1
2
3
4
5
6
function Person() {
this.hairColor = "balck";
this.sayHello = function () {
console.log("Hello!");
};
}

没有问题。那么我再提另一个问题,假如这些人都共享居所,或者资金,比如北京有一套房。那么,如果其中有一个人赚了一套房,这些人是可以共享的,那么上面的方法如何做到呢?

同时,每次实例化都要为属性和方法开辟新的内存空间,那如果实例多个对象的话,非常浪费内存空间。

那么基于上面的问题,就需要说到 prototype 属性,给构造函数设置一个 prototype 属性。这个属性是一个对象,所有实例对象需要共享的属性和方法,都放在这个对象里面。代码示例如下:

1
2
3
4
5
6
7
function Person() {}
Person.prototype.hairColor = "back";
Person.prototype.sayHello = function () {
console.log("Hello!");
};
var person1 = new Person();
var person2 = new Person();

用上面的方法,可以解决内存浪费问题,所有共享属性和方法都放在 prototype 中,只需要开辟其相对应的内存,同时也能实现数据共享和继承。那么 Function 作为 Person 函数的构造函数,是不是也应该有个 prototype 属性,存放 Person 函数可以从 Function 那里共享的属性和方法呢。

上面已经说了 prototype 的作用,所有函数都可以是构造函数,所以,所有函数都具有 prototype 属性,里边可存放所有可供其实例继承的共享属性和方法

总结:
1.所有函数都具有 prototype 属性。
2.prototype 存放了实例的共享属性和方法。
3.prototype 是一个对象,有的人称它为显式原型。

4.2constructor 的真正位置

那么说到这里,我们解决下 3 留下的问题,为什么实例打印的时候没有显示它的 constructor 这个属性呢。假如每个实例都给赋值了一个 constructor 属性,类似于我们 3 中说的那样,那是不是就遇到了我们 3 说的问题呢,会不断开辟内存去存放,浪费内存,所以,同理,实例的 constructor 存放在这个实例的构造函数的 prototype 中共享。

五、__proto__是什么,有什么用?

5.1 [[Prototype]]属性

我们上面说到了 prototype 存放了共享的属性和方法,那么我们的实例是如何继承这些共享的属性和方法。我们自然会想到在每个实例对象内部创建一个属性等于自己的原型(构造函数上的 prototype)。是不是就可以通过对象去获取到共享的属性和方法了呢?(及 person1.这个属性 === Person.prototype,又因为 Person.prototype 里存放了 hairColor 属性,所以,通过 person1.这个属性.hairColor 就可以获取这个共享的 hairColor 的值是多少。)

[[Prototype]]就是所说的这个对象属性,指向同构造函数上的 prototype,也是这个实例真正意义上的原型对象,这个属性是内部隐藏属性,不对外提供访问,所以我们通过 对象.[[Prototype]] 无法查看和修改原型上的属性和方法。

那么说到这里,结合前面说到问题,是不是有人会问, 既然对象.[[Prototype]]=== 构造函数.prototype。那我是不是只要获取到构造函数的 prototype,就可以查看原型对象和修改原型对象呢。没错,是的,只要你知道这个对象的构造函数。那你可能接着问,那还不简单,前面不是说了嘛,对象.constructor === 对象的构造函数。那我是不是就可以通过对象.constructor 找到对象的构造函数呢。那么,我们接着往下看。

5.2 constructor 不可信

修改上面相关列子,如果定义一个 Dog 函数,Dog 函数共享的头发颜色是红色, person1.constructor = Dog,那么这个时候 person1 的 hairColor 颜色是什么呢。代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person() {}
Person.prototype.hairColor = "back";
Person.prototype.sayHello = function () {
console.log("Hello!");
};
var person1 = new Person();
var person2 = new Person();
function Dog() {}
person1.constructor = Dog;
Dog.prototype.hairColor = "red"; // 在Dog.prototype上定义hairColor属性
console.log(person1.constructor); // Dog
console.log(person2.constructor); // Person
console.log(person1.hairColor); // balck

所以从上面的例子可以看出 person1.constructor 是 Dog,但是头发颜色是 balck 而不是 red,所以通过 constructor 去获取实例的构造函数,然后获取共享属性(hairColor)的方法不可取。

那我想获取到原型对象,查看共享的属性和方法,以及实现修改共享的属性和方法咋办啊? [[Prototype]]属性我又得不到。接着往下看。

5.3 __proto__

为了实现上面我们说的问题,后来许多浏览器厂商实现了 __proto__属性 ,(最开始是火狐浏览器提供的__proto__)暴露了对象的[[Prototype]]。__proto__指向了[[Prototype]],我们就可以通过对象.__proto__得到对象原型对象上的属性和方法,同样也可以去修改。

对象.[[Prototype]] = 创建自己的构造函数内部的 prototype(原型对象)

对象.__proto__= 对象.[[Prototype]]

对象.__proto__ = 创建自己的构造函数内部的 prototype(原型对象)

注意:__proto__在 ES6 以前不是 JS 标准,是浏览器给提供的。由于越来越流行,运用广泛,在 es6 规范中被标准化为传统功能,以确保 Web 浏览器的兼容性。它已被不推荐使用, 现在更推荐使用 Object.getPrototypeOf,Object.setPrototypeOf。

那么,上面的图中 Function 函数也是一个对象,它的__proto__指向了什么呢?我们说了对象.__proto__ = 对象的构造函数.prototype。由于上面讲到过的,那么 Function 的构造函数是其本身,所以 Function.__proto__ === Function.prototype。

六、原型模式

  1. JavaScript 中除了基础类型外的数据类型,都是对象(引用类型)。但是由于其没有类(class,ES6 引入了 class,但其只是语法糖)的概念,如何将所有对象联系起来就成立一个问题,于是就有了原型和原型链的概念。
  2. 所有的引用类型(数组、对象、函数)都有一个 __proto__属性(隐式原型属性),本质是个对象。
  3. 所有的函数,都有一个 prototype(显式原型)属性,存放了其实例可共享的属性和方法。
  4. 对象的__proto__等于实例这个对象的构造函数的 prototype。
  5. Object.prototype 没有 __proto__,这也是原型链的终点。

上面总结了下原型链的基本知识,下面我们通过例子详细说明。

七、通过例子详细说明原型链的查找

7.1 原型链上查找某个对象是否具有某个属性

所谓的原型链上去查找,其实就是通过对象的__proto__去查找。(这个链可以理解为用__proto__去连接)

1.先查看实例上是否具有该属性。及对象.属性是否有值有就找到了。

2.如果 1 中没找到,就去实例的原型对象(proto )找有没有该属性。及**对象.__proto__**上是否有值 。

3.如果 2 中没找到,就对象.__proto__.__proto__**找有没有该属性。一直通过.__proto__链接下去,直到终点。**

我们下面通过详细的例子说明原型链的查找,找下这个人的头发是什么颜色。

7.2 例子 1

1
2
3
4
5
6
7
function Person(name) {
this.name = name;
}
var person1 = new Person("lili");
person1.hairColor = "red";
console.log(person1);
console.log(person1.hairColor);

person1 被 Person 构造函数实例化,我们给这个 person1 加了属性 hairColor,这个时候 person1 上具有 hairColor 的(person1.hairColor = ‘red’),就是我们后来给他赋值的颜色,红色。打印如下:

7.3 例子 2

1
2
3
4
5
6
7
8
function Person(name) {
this.name = name;
}
Person.prototype.hairColor = "black";
var person1 = new Person("lili");
console.log(person1);
console.dir(Person);
console.log(person1.hairColor);

第一步:person1 被构造函数实例化的时候,是否初始化了 hairColor 的值呢。我们可以从上面的代码中看见,构造函数只初始化了一个属性 name 的值,所以 personOne 没有 hairColor 这个值。打印如下图:

第二步:这个对象是否有像例子 1 中(person1.hairColor = ‘red’),给实例对象赋值。显然也没有。对象实例上没有找到 hairColor 这个属性。

第三步:找这个对象的原型对象及 person1.__proto__是否有 hairColor 的值;

person1.__proto__ === person1 的构造函数.prototype 及 Person.prototype;

Person.prototype 有没有 hairColor 这个属性呢?看代码 Person.prototype.hairColor = ‘black’,是有的,黑色,原型链查找结束。所以我们知道了 person1 的原型的头发是黑色的,所以 person1 继承这个头发的颜色,也是黑色的。如下图:

所以 person1.hairColor === ‘black’;

那么有个问题,原型链不断向上查找的头是哪里呢?

我们再看个例子,去掉上个例子中 Person.prototype.hairColor = ‘black’,如下:

7.4 例子 3

1
2
3
4
5
6
7
function Person(name) {
this.name = name;
}
var person1 = new Person("lili");
console.log(person1);
console.dir(Person);
console.log(person1.hairColor);

还是查找 person1.hairColor 的值。

第一步:如上个例子的第一步,不细说了。

第二步:如上个例子第二步,不细说了。

第三步:如上个例子第三步,这个时候我们发现,Person.prototype 也没有 hairColor 的值。 及 person1.proto上也没找到 hairColor 的值。

第四步:(1)查找 person1.__proto__.__proto__,那么 person1.__proto__.__proto__是什么呢?

(2)第三步中我们知道了 person1.__proto__=== Person.prototype,person1.__proto__.__proto__ === Person.prototype.__proto__

(3)那么 Person.prototype 是什么呢?是 Person 函数的原型,本身是个对象,那么对象.__proto__=== 对象的构造函数的 prototype,那这个对象的构造函数是什么呢,是 JS 的内置 Object 函数实例化的。所以上面的问题就变成 Person.prototype.__proto__=== Object.prototype。查找 Object 函数的 prototype 上是否有 hairColor 属性吗?显然也没有。继续查找。

第四步:查找 person1.__proto__.__proto__.__proto__,及 Object.prototype 的__proto__,而 Object.prototype 是没有__proto__,所以到此结束。

八、JS 的内置函数

8.1Array

​ Array.prototype // []

​ Array.__proto__ // Function.prototype Array.__proto__.__proto__ {}.__proto__ === Object.prototype

8.2 Object

​ Objecy.prototype // {}

​ Objecy.__proto__ // Function.prototype

8.3Function

​ Function.prototype // ƒ ()

​ Function.__proto__ // Function.prototype

8.4String

​ String.prototype // 空字符串

​ String.__proto__ // Function.prototype

就这些呢,其他自己看吧。

九、实战

9.1 题 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    function A() {}
function B(a) {
this.a = a;
}
function C(a) {
if (a) {
this.a = a;
}
}
A.prototype.a = 1;
B.prototype.a = 1;
C.prototype.a = 1;
console.log(new A().a);
console.log(new B().a);
console.log(new C(2).a);

9.2 题 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var F = function () {};
Object.prototype.a = function () {
console.log("a()");
};
Function.prototype.b = function () {
console.log("b()");
};
var f = new F();
F.a();
F.b();
f.a();
f.b();

console.log(new A().a);

console.log(new B().a);

console.log(new C(2).a);

答案:a() b() a() f.b is not a function

参考文章:https://juejin.im/post/6844903837623386126#heading-4

作者

王丽

发布于

2020-10-19

更新于

2023-08-05

许可协议

CC BY-NC-SA 4.0

评论