原型链,相信使用过JavaScript的同道中人都或多或少的听说过,它是JavaScript语言中相当重要的核心概念,有了它,我们才能在遍地都是对象的JavaScript中实现继承,大大提升了我们编程的便利性和程序的精简度。理解
prototype
、__proto__
和constructor
以及它们三者之间的关系是掌握原型链的必经之路。
1、原型链设计的始末
1.1 历史背景
- 1990年代初期,互联网刚刚兴起,但是当时的只能查看静态的网页,只能显示html和图片,缺乏与用户之间的交互。
- 1994年网景公司退出
Netscape Navigator
浏览器,为了更好的占据市场,它们希望网页可以"动"起来,所以急需一门直接嵌入html的语言。 - 1995年5月网景公司招募的程序员
Brendan Eich
仅用了十天时间设计出了JavaScript
语言,但在设计它的继承机制的时候确实花了点功夫。
1.2 借鉴java和c++语言的new操作符
在设计JavaScript
语言的继承机制的时候,Eich
为了满足轻便、易上手 的设计理念,没有选择引入类(calss) 的概念。但是它借鉴了java
和c++
语言实例化类的机制:使用new
操作符,但是没有类,new
出来的是什么呢?
- 了解一点
java
和c++
语言的同学们都知道,在类被实例化的时候都会触发类的构造函数constructor ,既然没有类,Eich
就直接让new
操作构造函数。
1.3 引入prototype原型对象
但是new
操作符是有缺陷的,通过new
创建的对象没法共享公共的方法和属性,这样只是创建了一个新对象,没有达到和传统面向对象语言一样的继承目的。于是prototype
原型对象就此诞生,它专门用于存放公共的属性与方法。
2、prototype
-
属于构造函数(函数对象) :只有构造函数才有
prototype
属性 -
作用:为构造函数定义公共的属性和方法,方便实例对象使用。
-
例子:
javascriptfunction animal(name) { this.name = name; } animal.prototype.showName = funciton() { console.log(`我是:${this.name}`) } const cat = new animal('猫'); const fish = new animal('鱼'); cat.showName(); // 我是:猫 fish.showName(); // 我是:鱼
在例子中,
animal
是构造函数,animal.prototype
是原型对象
,所有animal
的实例对象(cat和fish
)共享了原型对象中的showName
方法。
3、__proto__
-
属于实例对象 :所有对象(包括函数对象)都有
__proto__
属性。 -
作用 :指向创建该对象的构造函数的原型对象(即
constructor.prototype
)。 -
例子:
javascript// cat 是 animal 的实例 console.log(cat.__proto__ === animal.prototype); // true
cat.__proto__
指向animal.prototype
。
-
注意:
__proto__
是浏览器实现的非标准属性,现代代码中应使用Object.getPrototypeOf(obj)
替代。- 原型链的终点是
Object.prototype.__proto__
(即null
)。
4、constructor
-
属于原型对象 :每个原型对象(
prototype
)都有一个constructor
属性。 -
作用:指向该原型对象关联的构造函数。
-
例子:
javascriptconsole.log(animal.prototype.constructor === animal); // true console.log(cat.constructor === animal); // true(通过原型链继承)
- 实例对象本身没有
constructor
属性,但会通过原型链找到animal.prototype.constructor
。
- 实例对象本身没有
5、它们之间的关系

如上图所示,它们之间的关系是这样的:
-
构造函数的
prototype
属性指向原型对象; -
实例对象的
__proto__
也指向原型对象; -
原型对象的
constructor
属性又指向构造函数本身javascriptcat.__protp__ === animal.prototype; // 实例对象的__protp__指向构造函数的原型对象 animal.prototype.constructor === animal; // 原型对象的constructor指向构造函数本身 // 构造函数的原型对象的__proto__指向内置对象Object的原型对象 animal.prototype.__proto__ === Object.prototype; Object.prototype.__proto__ === null; //(原型链的终点)
6、原型链的优缺点
优点
-
内存高效:共享属性和方法,通过原型链所有的实例共享原型对象中的方法和属性,避免重复创建,节约内存。
javascriptfunction animal(name) { this.name = name; } animal.prototype.showName = funciton() { console.log(`我是:${this.name}`) } const cat = new animal('猫'); const fish = new animal('鱼'); cat.showName(); // 我是:猫 fish.showName(); // 我是:鱼
-
动态扩展性:运行时修改原型,原型对象可以动态修改,一次修改,所有实例受益,无论新旧实例。
javascriptanimal.prototype.sayHello = function() { console.log('Hello') } cat.sayHello(); // Hello 虽然实例对象已经被创建了,但也能使用原型对象中的新方法 fish.sayHello(); // Hello
-
灵活的继承模式:根据不同的场景,可通过多种方式实现继承。
javascript// 组合继承 function dog(name, grade) { animal.call(this, name); // 构造函数继承属性 this.grade = grade; } dog.prototype = Object.create(animal.prototype); // 原型链继承方法
缺点
-
性能问题:深层次原型链查找,属性和方法的查找需要逐层遍历原型链,直至顶层原型链为止,过长的原型链会影响其性能
javascriptcat.toString(); 查找线路:cat -> animal.prototype -> Object.prototype -> null
-
共享属性的副作用:原型对象的共享属性可能会被所有实例对象意外修改
javascriptanimal.prototype.family = []; cat.family.push('dog'); // 其实修改的是原型对象中的family console.log(fish.family); // ["dog"]
-
污染作用域 :构造函数不规范的使用,即不通过
new
操作符调用,this
指向全局作用域,会导致污染全局作用域。javascriptconst dog = animal('dog');// this.name 泄露到全局作用域 console.log(window.name); // "dog"
-
理解成本高 :隐式引用,原型对象中的额公共属性很难被发现,容易混淆
prototype、__proto__、constructor
这三者之间的关系javascriptconsole.log(cat.__proto__ === animal.prototype); // true console.log(animal.prototype.constructor === animal); // true
总结
原型链的设计主要是为了让JavaScript
语言支持继承机制,但是JavaScript
的继承机制又与传统面向对象语言不太一样,它没有设计"类"的概念,继承是基于原型对象(prototype
)实现,实例对象通过__proto__
对象找到创建该实例的构造函数的原型对象,原型对象的constructor
属性又指向构造函数本身,确实有点绕,容易混淆,可以看一下这个表格,会清晰一些:
属性 | 归属 | 作用 | 示例关系 |
---|---|---|---|
prototype |
构造函数 | 定义实例共享的原型对象 | Person.prototype |
__proto__ |
实例对象 | 指向构造函数的原型对象(形成原型链) | alice.__proto__ === Person.prototype |
constructor |
原型对象 | 指向关联的构造函数 | Person.prototype.constructor === Person |
JavaScript
的继承机制也是相当灵活,在运行状态下,能修改通过同一构造函数(或指向同一原型对象)创建的多个实例对象的共享属性,这是其他面向对象语言办不到的。
但没有完美的语言设计,JavaScript
的原型链设计也会有很多问题,如原型链过长导致查找的性能问题,理解成本高、共享属性副作用、污染全局变量。
最后,运用JavaScript
,我们应当去其糟粕,取其精华,将它的优点最大化。