前言
其他编程语言如 Java 等使用 new
命令时,都会调用“类”的构造函数。但是,JavaScript没有“类”,本身并不提供一个 class
实现(虽然在ES6中提供了class
关键字,但其只是语法糖,JavaScript仍然是基于原型的)。于是,JavaScript作了一个简化的思想,new
命令后面跟的不是类,而是构造函数,用构造函数生成实例对象,但其缺点是无法共享属性和方法。于是,就为构造函数设置了一个 prototype
属性,这个属性包含一个对象(prototype
对象)。所有实例对象需要共享的属性和方法都放在这个对象里,那些不需要共享的属性和方法就放在构造函数里。
💡温馨提示:本文全文1986
个字,推荐阅读时间为10m
,加油老铁!
一、显式原型(prototype)
1.1 介绍
每个函数在创建之后都会有一个名为prototype
属性:
function Parent() {
}
Parent.prototype.name = 'kite';
let child = new Parent();
console.log(child.name);
这个属性指向函数的原型对象(通过Function.prototype.bind
方法构造出来的函数是个例外,它没有prototype
属性),即调用构造函数创建的实例的原型,也就是例子中child的原型。
何为原型:每一个JavaScript对象(null
除外)都会和另一个对象相关联,这个对象就是原型,上面也提到每个对象都会从原型中“继承属性”。
原型表明了构造函数和实例原型的关系。
1.2 作用
显式原型用来实现基于原型的继承与属性的共享。
二、隐式原型(__proto__)
2.1 介绍
JS中任意对象都具有一个内置属性[[prototype]],在ES5之前没有标准方法访问,大多数浏览器通过__proto__
来访问。ES5中有了对这个内置属性标准的get方法:Object.getPrototypeOf
(Object.prototype
是个例外,它的__proto__
为 null
)。
function Parent() {
}
let child = new Parent();
console.log(child.__proto__ === Parent.prototype); // true
2.2 作用
隐式原型构成原型链,同样用于实现基于原型的继承。举例来说:
当我们访问对象中的某个属性时,如果在对象中找不到,就会一直沿着__proto__
(原型的原型)依次查找,直到找到最顶层为止。
function Parent() {
}
Parent.prototype.name = 'dave';
let child = new Parent();
child.name = 'kite';
console.log(child.name); // 'kite'
delete child.name;
console.log(child.name); // 'dave'
上面的例子中,给对象child
添加name
属性,当访问name
属性时,找到了对象本身的属性值kite。删除name
属性之后,再次访问name
属性,在对象中找不到该属性,再在原型中寻找,找到dave
。假设属性在原型中也没有找到该属性,则会再去原型的原型中查找。
我们知道原型是个对象,既然是对象就可以用最原始的方式创建:
let obj = new Object();
原型就是通过Object
构造对象生成的。那 Object.prototype
的原型又是什么?
Object.prototype.__proto__ = null; // true
null
表示没有对象,表明在此处可以停止查找了。
2.3 指向
__proto__
指向创建这个对象的函数的显式原型,其关键在于找到创建这个对象的构造函数。创建对象有三种形式:对象字面量;new
(class
);ES5的Object.create()
。本质只有一种new
。
2.3.1 对象字面量
let obj = {
name: 'ctt'
}
对象字面量声明的对象继承自 Object
,和 new Object
一样,其原型为Object.prototype
。而 Object.prototype
不继承任何属性和方法。
2.3.2 new
使用构造函数创建的对象,它的属性继承自构造函数。
具体分以下几种情况:
- 内建对象
如Array()
,它继承于Array.prototype
,Array.prototype
为一个对象,这个对象由Object()
这个构建函数创建。因此,Array.prototype.__proto__ === Object.prototype
,原型链为:Array.prototype -> Object.prototype -> null
。 - 自定义对象
默认情况下:
function Foo() {}; let foo = new Foo(); Foo.prototype.__proto__ === Object.prototype;
其他情况:
// 想让Foo继承Bar function Bar() { } Foo.prototype = new Bar(); Foo.prototype.__prototype__ = Bar.prototype; // 重新定义Foo.prototype Foo.prototype = { a: 1, b: 2 }; Foo.prototype.__proto__ = Object.prototype;
以上两种情况改写了Foo.prototype
,所以Foo.prototype.constructor
跟着改变,constructor
和原构造函数 Foo
切断了联系。
- 构造函数
构造函数就是Function()
的实例,因此构造函数的隐式原型指向Function.prototype
。引擎创建了Object.prototype
,而后又创建了Function.prototype
,通过__proto__
将两者联系起来。
Function.prototype === Function.__proto__
,其他所有的构造函数都可以通过原型链找到 Function.prototype
,并且 function Function()
本质也是一个函数,为了不产生混乱,将 function Function
的 __proto__
联系到了Function.prototype
上。
2.3.3 class
在ES5中,每个对象都有一个 __proto__
属性,指向对应构造函数的prototype
属性,而 class
作为构造函数的语法糖,同时具有 prototype
属性和 __proto__
属性,所以同时存在两条继承链。
- 子类的
__proto__
表示构造函数的继承,总是指向父类; - 子类的
prototype
属性的__proto__
表示方法的继承,总是指向父类的prototype
。
class Parent {
}
class Child extends Parent {
}
Child.__proto__ === Parent;
Child.prototype.__proto__ === Parent.prototype;
作为一个对象,子类 Child
的原型(__proto__
)是父类 Parent
;作为一个构造函数,子类的原型对象(prototype
)是父类原型对象(prototype
)的实例。
2.3.4 Object.create()
Object.create()
是ES5的方法,可以调用这个方法来创建一个新对象。新对象的原型就是传入的第一个参数。
三、构造函数(constructor)
上面说明构造函数和实例都可以指向原型,接下来讲的这个属性就是让原型指向构造函数的(没有指向实例的属性,因为构造函数可以生成多个实例),它就是constructor
属性,每个原型都有一个constructor
属性指向构造函数。
function Parent() {
}
console.log(Parent === Parent.prototype.constructor);
关于原型的各类关系总结如图:
四、this 和 prototype 定义方法的区别
- 利用
this
实现的方法,可以访问类中的私有变量和私有方法。而利用原型对象实现的方法,无法访问类中的私有变量和方法。 - 实例访问对象的属性或者方法时,将按照搜索原型链
prototype chain
的规则进行。首先查找自身的静态属性、方法,继而查找构造上下文的可访问属性、方法,最后查找构造的原型链。 this
和prototype
定义的另一个不同点是在内存中占用空间不同。使用“this”关键字,实例初始化时为每个实例开辟构造方法所包含的所有属性、方法和所需空间,而使用prototype
定义的,由于prototype
实际上是指向父级的引用,因此在初始化和存储上比“this”节约资源。
五、总结
- 每个函数在创建之后都会有一个名为
prototype
属性,这个属性指向函数的原型对象(显式原型),所有实例对象需要共享的属性和方法都放在这个对象里; - 任意对象都具有一个内置属性
__proto__
指向创建这个对象的函数的显式原型; - class 作为构造函数的语法糖,同时具有
prototype
属性和__proto__
属性,作为一个对象,子类Child
的原型(__proto__
)是父类Parent
;作为一个构造函数,子类的原型对象(prototype
)是父类原型对象(prototype
)的实例。 - 每个原型都有一个
constructor
属性指向构造函数,即Parent === Parent.prototype.constructor
参考
JavaScript - 原型、原型链 · Issue #13 · cttin/cttin.github.io · GitHub
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。