JavaScript 常被描述为一种基于原型的语言 (prototype-based language) ——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain) ,它解释了为何一个对象会拥有定义在其他对象中的属性和方法。、
一、什么是原型(prototype)
在 JavaScript 中函数也是对象,可以拥有属性。其中,每个函数都有一个特殊的属性叫作原型(prototype)。只要创建一个函数,就会按特定规则为这个函数创建一个 prototype 属性。
简单来说:原型(prototype)就是函数对象的一个属性,该属性是一个对象,包含特定引用类型的实例共享的属性和方法。
1.1 原型(prototype)、构造函数(constructor)、实例对象的关系
每个构造函数都有一个原型(prototype)属性(指向原型对象),原型对象有一个属性(constructor)指回构造函数。此外,通过构造函数创建的实例对象有一个内部属性(__proto__)指向原型对象。
上述这段话非常重要,但是感觉很绕,但是不要怕,下面从代码和图两种方式来解释这段话。先看如下一段代码:
// 定义构造函数Fn
function Fn(name) {
this.name = name;
}
// 通过构造函数 Fn 新建一个实例对象
let obj = new Fn();
// 构造函数的 prototype 属性
console.log(Fn.prototype)
// Fn 的原型对象的 constructor 属性指回构造函数
console.log(Fn.prototype.constructor === Fn) // true
// 通过 Fn 创建出的实例对象内部属性 __proto__ 指向原型对象
console.log(obj.__proto__ === Fn.prototype) // true
// 实例对象的 constructor 指向构造函数
console.log(obj.constructor === Fn)
console.log(obj.constructor === Fn.prototype.constructor)
上述代码中呈现出的关系如下图:
图中只是简单展示了构造函数、原型、实例对象之间的关系,并未完全涉及到原型链,相关知识会在后文介绍。
1.2 对象原型 和 构造函数 prototype 属性的区别
虽然,当实例对象是由对应构造函数创建时,对象原型和构造函数 prototype 属性指向同一个对象,但是两者之间并非同一个东西。
(1)对象原型
对象原型时每个实例对象都拥有的属性,可以如下两种方式获得:
- 通过
Object.getPrototypeOf(obj)方法 - 实例对象的
__proto__属性(已弃用)。
let obj = {};
console.log(obj.__proto__) // {constructor: ƒ, …}
console.log(Object.getPrototypeOf(obj)) // {constructor: ƒ, …}
(2)prototype 属性
prototype 属性是构造函数特有的属性。
// 构造函数Foo
function Foo(name) {
this.name = name;
}
// foo的实例对象
let obj = new Foo();
console.log(Foo.prototype); // {constructor: f}
console.log(obj.prototype); // undefined
上述代码中 obj.prototype 的值为 undefined, 说明实例对象obj并不包含 prototype 属性。
此外,需要注意的一点,构造函数也是一个对象,所以构造函数也有对象原型。(其实在JavaScript中,每个构造函数都是Function实例对象,这个在后面原型链中详述)
// 定义构造函数foo
function Foo(name) {
this.name = name;
}
// 构造函数的对象原型
console.log(Object.getPrototypeOf(Foo)) // ƒ () { [native code] }
(3)小结
对象原型是所有实例对象(包括构造函数)的属性,而prototype 是构造函数的属性。
当实例对象是由对象构造函数创建的,那么实例对象的原型和构造函数的prototype指向同一个对象。如:
Object.getPrototypeOf(new foo()) 和 foo.prototype 指向同一个对象。
实例对象和构造函数原型之间有直接的联系,但实例与构造函数之间没有。
二、构造函数 和 new 运算符
在介绍原型链之前,为了更好理解第一节中的一些概念,这里下介绍一下构造函数和 new 运算符
2.1 构造函数
不同于其他面向对象的编程语言,JavaScript 的构造函数并不是作为类的一个特定方法存在的;当任意一个普通函数用于初始化实例对象,他就被称作构造函数 或 构造器。构造函数都是和 new 一起使用的。
严格来说,一个函数作为真正意义上的构造函数,一般需要满足以下条件:
- 默认函数首字母大写
- 在函数内部对新对象(this)的属性进行设置,通常是添加属性和方法。
- 一般构造函数没有返回值,new 操作符会自动创建并返回。若构造函数中有返回值,返回值是一个对象,会代替新创建的对象实例返回,返回值是一个原始类型,则会被忽略。
此外需要注意的一点:构造函数也是函数,所以也可以被直接调用,当没有返回值并不会自动创建对应的对象实例并返回。而且函数中的
this在非严格模式下为全局对象,严格模式下为undefined。
// 构造函数Foo
function Foo(name) {
this.name = name;
}
let obj = new Foo("haha");
// new 关键字会自动创建对象
console.log(obj); // Foo {name: 'haha'}
console.log(typeof obj); // object
// 非严格模式下,直接调用构造函数,其中this为全局对象
Foo('dali');
console.log(name); // dali
/*****************************************************************************/
// 当构造函数返回值为原始值时被忽略
function Foo(name) {
this.name = name;
return 1;
}
let obj = new Foo("haha");
console.log(obj); // Foo {name: 'haha'}
/*****************************************************************************/
// 当构造函数返回值为对象时,替换新创建的对象实例返回
function Foo(name) {
this.name = name;
return {};
}
let obj = new Foo("haha");
console.log(obj); // {}
所以,综上所述:
构造函数是是一种用于初始化对象实例的特殊函数,与 new 运算符一起使用。
2.2 new 运算符
构造函数都是和 new 运算符一起使用的,那么 new 的到底执行了哪些操作呢。
(1)官方定义
new运算符创建一个用户定义的对象类型的实例 或 具有构造函数的内置对象的实例。
(2)语法
new constructor[([arguments])]
constructor:一个指定对象实例的类型的类或函数。arguments:一个用于被constructor调用的参数列表。
(3)new 关键字执行的操作
- 创建一个空的简单JavaScript对象(即{})
- 为 1 中创建的对象添加
__proto__属性,并将该属性链接至构造函数的原型对象。 - 将 1 中创建的对象作为
this上下文 - 如果该函数有返回值,且对象类型,则返回该对象。否则返回
this。
(4)手动实现
function _new(fn, ...args) {
// 创建一个JavaScript空对象,并将其__proto__ 属性链接到构造函数的原型
let instance = Object.create(fn.prototype);
// 不使用 Object.create()
// let instance = {}
// instance.__proto__ = fn.prototype
// 改变构造函 fn 的 this 指向
let res = fn.apply(instance, args);
// 如果 fn 返回值为对象,则返回该对象,否则返回this(指的是构造函数中的this,已经绑定为instance)
return typeof(res) === 'object' ? res : instance;
}
三、原型链
前两节中我们知道了原型(prototype)其实就是函数对象的一个特殊的属性,但是它是如何做到共享属性和方法的呢?这就要引出原型链了,它解释了为何一个对象会拥有定义在其他对象中的属性和方法。
ECMA-262 把原型链定义为 ECMAScript 的主要继承方式。其主要思想就是通过原型链继承多个引用类型的属性和方法。
3.1 原型链是什么
(1)官方术语
首先我们看MDN中的几段话:
(1)JavaScript 只有一种结构:对象。每个实例对象(object)都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(proto),层层向上直到一个对象的原型对象为
null。根据定义,null没有原型,并作为这个原型链中的最后一个环节。
(2)几乎所有JavaScript中的对象都是位于原型链顶端的 Object 的实例。
(3)JavaScript 对象是动态的属性“包”(指其自己的属性)。JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
(1)告诉我们通过每个对象的私有属性__proto__构成了原型链,并且原型链的末端是null。(2)告诉我们,原型链的顶端是Object,几乎所有对象通过原型链都能找到Object,即:Object.prototype.__proto__ === null。(3)告诉我们原型链的查找规则,即如何共享属性和方法的。
(2)代码示例
// 构造函数 Father
function Father(name) {
this.name = name;
}
Father.prototype.sayName = function() {
console.log(this.name);
}
// 构造函数 Son
function Son(name) {
this.name = name;
}
// 设置Son的原型对象Father(即继承自Father)
Son.prototype = new Father;
let father = new Father('father');
let son = new Son('son');
console.log(son.__proto__ === Son.prototype) // true
console.log(Son.prototype.__proto__ === Father.prototype) // true
console.log(Father.prototype.__proto__ === Object.prototype) // true
console.log(Object.prototype.__proto__ === null) // true
对于上述代码,从Son对象实例son开始分析,son是通过构造函数Son初始化的对象实例,所以son.__proto__指向构造函数Son的原型Son.prototype,因为 Son.prototype = new Father;将构造函数Son的原型属性设置为Father对象实例,所以Son.prototype.__proto__指向构造函数的Father的原型 Father.prototype,构造函数Father的原型prototype并未显示设置指向哪个对象,所以默认指向Object的原型,即Father.prototype.__proto__指向Object.prototype,原型链的顶端是Object对象,所以Object原型的原型指向null,即Object.prototype._proto__指向null。具体关系如下图所示:
father.sayName(); // father
console.log(father.toString()) // [object Object]
son.sayName(); // son
console.log(son.toString()); // [object Object]
接着看这段代码中,Son对象实例son中并没有定义sayName()方法,而且son和father中也没有定义toString()方法,但为啥可以访问呢?
这是因为:
JavaScript 中,访问一个对象的属性时,不仅搜寻当前的对象,还会在该对象所在的原型链上进行搜寻,直到找到标识符匹配的属性或到达原型链末尾。
所以访问son.sayName时,会首先搜寻son,未找到,接着搜寻son.__proto__还未找到,再搜索son.__proto__.__proto__(即Father.prototype)找到了sayName。同理因为Object.prototype中定义了toString方法,而且son和father的原型链上都包含Object.prototype,所以都能访问到toString。
(3)属性遮蔽
根据上述 JavaScript 中访问对象属性的规则,若遇到匹配的标识符就会停止搜寻。所以:
给对象实例添加一个属性,这个属性就会遮蔽原型对象上的同名属性,但不会修改原型对象上的同名属性,仅仅屏蔽对他的访问。
Foo.prototype.say = function() {
console.log("我是原型上的say方法");
}
let foo1 = new Foo();
let foo2 = new Foo();
foo2.say = function() {
console.log("我是foo2上的say方法");
}
foo1.say(); // 我是原型上的say方法
foo2.say(); // 我是foo2上的say方法
如上代码,Foo对象实例foo2上添加了say属性,其原型对象上包含同名属性,但是访问foo2.say遮蔽了对原型属性的访问。
(4)prototype 属性:继承成员被定义的地方
继承的属性和方法是定义在 prototype 属性之上的,prototype 属性的值是一个对象,我们希望被原型链下游的对象继承的属性和方法,都被储存在其中。
所以只有 prototype 对象内的成员才会被对象实例继承。
function Foo() {
}
Foo.f = function() {
console.log("非原型上的方法");
}
let foo1 = new Foo();
foo1.f(); // TypeError
Foo.f(); // 非原型上的方法
3.2 图解原型链
:图中实例与构造函数存在一个constructor的指向,是为了方便理解实例是由哪个构造函数初始化得到的,但实际上实例中并不包含 constructor 属性,而是继承自原型上的constructor
图中的箭头看起来有点杂乱,下面我将用对应代码解释上图:
(1)首先自定义一个构造函数Person,并初始化一个对象实例person
// 普通构造函数
function Person() {
}
// 对象实例
let person = new Person();
(2)图的顶部表示了构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。
// 原型 constructor 属性指回构造函数
console.log(Person.prototype.constructor=== Person) // true
// 实例内部属性 __proto__ 指向原型
console.log(person.__proto__ === Person.prototype) // true
(3)再看图的右边,展示了一个完整的原型链。即:
person -> Person.prototype -> Object.prototype -> null。
console.log(person.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true
(4)其实(2)(3)已经展示了原型链了,但是构造函数本身也是一个对象,除了拥有prototype函数之外,也拥有内部属性__proto__。那么构造函数的内部属性__proto__指向哪呢?从图中可以看出,构造函数的内部属性__proto__指向Function.prototype。
因为,每个JavaScript 函数实际上都是一个
Function对象。
所以看如下代码:
console.log(Person.__proto__ === Function.prototype); // true
console.log(Function.__proto__ === Function.prototype); // true
console.log(Object.__proto__ === Function.prototype); // true
console.log(Function.prototype.__proto__ === Object.prototype) // true
Person、Function、Object都是函数,所以都是Function对象实例,那么他们的内部属性__proto__都是指向Function.prototype。但是原型链的顶层永远都是Object,所以Function构造函数原型的原型(Function.prototype.__proto__)是Objcet.prototype。
(5)上图中容易混淆的两个点在于:
- Function 本身也是Function对象实例,所以
Function内部属性__protp__指向Function.protype,并非Object.prototype。Function原型(prototype)的原型(__proto__)才指向Object.prototype
console.log(Function.__proto__ === Function.prototype); // true
console.log(Function.constructor === Function) // true
console.log(Function.prototype.__proto__ === Object.prototype) // true
- Object 其实也是 Function 对象实例,所以
Object.__proto__是Function.protype,并非null,Object原型的原型(Object.prototype.__proto__)才是null。
console.log(Object.__proto__ === Function.prototype); // true
console.log(Object.prototype.__proto__ === null); // true
- Objcet() 和 Objcet 不同,Object()是Object对象实例,而Objcet是一个函数对象。
即:JavaScript 中的所有函数还是自定义函数还是内置函数,都是Function 对象实例,所以他们的内置属性
__proto__都指向Function.prototype。
而由构造函数初始化的实例,其内部属性__proto__都指向对应构造函数的prototype属性。Object.prototype几乎存在于每个JavaScript对象实例的原型链上(由Object.create(null)创建 或者 设置F.prototype = null除外)。所以几乎最终都会指向Object.prototype
小结
所以,对于原型链要清楚以下几点:
- 构造函数、原型、实例的关系
- 原型也是一个对象,也有自己的原型。
- 构造函数
prototype属性 和 对象内部属性__proto__的区别 - 原型链顶端对象是
Objcet,Object原型的原型是null,即原型链的末尾 - 构造函数本质上是一个
Function对象。
四、原型相关问题的七嘴八舌
4.1 判断对象属性是否存在于原型上
(1)in 操作符
in 操作符会在可以通过对象访问指定属性时返回true,无论属性是在实例上还是在原型上。
// 普通构造函数
function Person() {
}
Person.prototype.name = ""
// 对象实例
let person = new Person();
person.age = 22;

// 原型上的属性
console.log('name' in person) // true
// 实例上的属性
console.log('age' in person) // true
(2)Objective.prototype.hasOwnProperty()
hasOwnProperty() 方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键)。只有属性是存在于实例上才会返回true。
// 普通构造函数
function Person() {
}
Person.prototype.name = ""
// 对象实例
let person = new Person();
person.age = 22;
console.log(person.hasOwnProperty('name')) // false
console.log(person.hasOwnProperty('age')) // true
(3)判断一个属性是否为原型属性
综上所述:
- in 操作符返回 true
- hasOwnProperty() 返回 false
那么该属性就是一个原型属性,例如上述代码中的
name属性。即
'property' in obj === true
obj.hasOwnProperty('property') === false
但是需要注意的是:若实例存在和原型的同名属性时,并不能通过该方法来判断。
4.2 原型和实例的关系判断
(1)instanceof
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
object instanceof constructor
补充:instanceof 手动实现
function _instanceof(obj, Func) {
// 获取 obj 对象原型
let proto = Object.getPrototypeOf(obj);
// 原型链末尾是 null
while(proto !== null) {
// 如果对象原型匹配函数原型属性
if (proto === Func.prototype) {
return true;
}
// 继续沿着原型链查找
proto = proto.getPrototypeOf(proto);
}
return false;
}
上述手动实现的方法和原生instanceof 有两个区别:
- 错误处理
// 检验right 是否是一个对象(Object)
if (right is not Object){
throw new TypeError(" Uncaught TypeError: Right-hand side of 'instanceof' is not Object")
}
// 检验 right 是否可被调用
if (right is not callable) {
throw new TypeError(" Uncaught TypeError: Right-hand side of 'instanceof' is not callable")
}
- 对原始值类型的判断
console.log(_instanceof(1, Number)) // true
console.log(1 instanceof Number) // false
可以尝试在上述实现中添加以下代码:
// 原始值类型都返回false
if (typeof obj != 'function' && typeof obj != 'object') {
return false;
}
(2)Object.prototype.isPropertyOf()
isPrototypeOf() 方法用于测试一个对象是否存在于另一个对象的原型链上。
prototypeObj.isPrototypeOf(object)
instanceof 运算符 和 Object.prototype.isPropertyOf() 的作用差不多,只是**“顺序”**不同。instanceof判断一个对象是否为一个构造函数的实例,prototypeObj.isPrototypeOf(object)判断一个对象是否为另一个对象的原型。
4.3 不同方法创建对象以及生成的原型链情况
(1)语法结构创建(字面量)
- 对象字面量
let obj = {}
console.log(obj.__proto__ === Object.prototype) // true
console.log(Object.prototype.__proto__ === null) // true
即 obj -> Object.prototype -> null
- 数组字面量
let arr = []
console.log(arr.__proto__ === Array.prototype) // true
console.log(Array.prototype.__proto__ === Object.prototype) // true
console.log(Object.prototype.__proto__ === null) // true
即arr -> Array.prototype -> Object.prototype -> null
- 函数声明
function foo() {
}
console.log(foo.__proto__ === Function.prototype) // true
console.log(Function.prototype.__proto__ === Object.prototype) // true
console.log(Object.prototype.__proto__ === null) // true
即foo -> Function.prototype -> Object.prototype -> null
(2)使用构造函数(constructor)创建
常见的对象定义模式是,在构造器(函数体)中定义属性、在 prototype 属性上定义方法。如此,构造器只包含属性定义,而方法则分装在不同的代码块,代码更具可读性
// 构造器及其属性定义
function Test(a,b,c,d) {
// 属性定义
};
// 定义第一个方法
Test.prototype.x = function () { ... }
// 定义第二个方法
Test.prototype.y = function () { ... }
// 等等……
若test为Test对象实例,那么原型链为:
test -> Test.prototype -> Object.prototype -> null
因为原型会导致所有实例默认都取得相同的属性值,所以一般在构造器(函数体)中定义属性、在
prototype属性上定义方法。
例如:Person对象拥有name属性,对于每个实例应该拥有不同的name, 如果将name定义在原型上,所有实例都默认同一个name值,不符合实际。
(3)使用 Object.create() 创建
Object.create() 方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。即新对象的原型就是传入的第一个参数
Object.create(proto,[propertiesObject])
var a = {a: 1};
// a ---> Object.prototype ---> null
var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (继承而来)
var c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null
var d = Object.create(null);
// d ---> null
console.log(d.hasOwnProperty); // undefined, 因为d没有继承Object.prototype
补充:Object.create()的手动实现
function objectCreate(proto, propertiesObject=undefined){
// 构造函数
function F() {
}
// 构造函数原型 prototype 链接到proto对象
F.prototype = proto;
// 创建对象
const obj = new F();
// 若参数 propertiesObject 被指定且不为 undefined
if (propertiesObject !== undefined) {
// 新创建的对象添加指定的属性值和对应的属性描述符。
Object.defineProperties(obj, propertiesObject);
}
return obj;
}
(4)使用 class 关键字创建
class 是 ES6引入的新特性,其实就是一个语法糖。其继承思想仍然基于原型,这里抛出一个例子,暂且不细说了。
class Polygon {
constructor(height, width) {
this.height = height;
this.width = width;
}
}
class Square extends Polygon {
constructor(sideLength) {
super(sideLength, sideLength);
}
get area() {
return this.height * this.width;
}
set sideLength(newLength) {
this.height = newLength;
this.width = newLength;
}
}
var square = new Square(2);
4.4 性能相关问题
- 在原型链上查找属性比较耗时,对性能有副作用,这在性能要求苛刻的情况下很重要。另外,试图访问不存在的属性时会遍历整个原型链。 所以注意代码中原型链的长度,并在必要时将其分解,以避免可能的性能问题
- 此外,不要扩展
Object.prototype或其他内置原型
以上就是个人学习原型和原型链的一些杂七杂八的记录了,大部分也是官方文档的摘抄,有错误也请大佬们指出。其中存在很多冗余的部分,也是个人了解不够透彻,就先这样了。后续学习了更多再更新。
主要参考
[1] 《JavaScript高级程序设计 第四版》
[2] MDN 继承与原型链
[3] MDN 对象原型