深入javascript之原型和原型链及继承机制得设计思想
http://www.ruanyifeng.com/blog/2011/06/designing_ideas_of_inheritance_mechanism_in_javascript.html 作者: 阮一峰
Javascript继承机制的设计思想
- 它没有"子类"和"父类"的概念,也没有"类"(class)和"实例"(instance)的区分,全靠一种很奇特的"原型链"(prototype chain)模式,来实现继承。
Brendan Eich的选择
如果真的是一种简易的脚本语言,其实不需要有"继承"机制。但是,Javascript里面都是对象,必须有一种机制,将所有对象联系起来。所以,Brendan Eich最后还是设计了"继承"。
但是,他不打算引入"类"(class)的概念,因为一旦有了"类",Javascript就是一种完整的面向对象编程语言了,这好像有点太正式了,而且增加了初学者的入门难度。
他考虑到,C++和Java语言都使用new命令,生成实例。
C++的写法是:
ClassName *object = new ClassName(param);
Java的写法是:
Foo foo = new Foo();
因此,他就把new命令引入了Javascript,用来从原型对象生成一个实例对象。但是,Javascript没有"类",怎么来表示原型对象呢?
这时,他想到C++和Java使用new命令时,都会调用"类"的构造函数(constructor)。他就做了一个简化的设计,在Javascript语言中,new命令后面跟的不是类,而是构造函数.
举例来说,现在有一个叫做DOG的构造函数,表示狗对象的原型。
function DOG(name){
this.name = name;
}
//对这个构造函数使用new,就会生成一个狗对象的实例。
var dogA = new DOG('大毛');
alert(dogA.name); // 大毛
console.log(Object.getOwnPropertyDescriptors(DOG.prototype));
// 打印输出 会输出 constructor 并且指向 DOG 构造函数
{
constructor: {
value: [Function: DOG],
writable: true,
enumerable: false,
configurable: true
}
}
//注意构造函数中的this关键字,它就代表了新创建的实例对象。
- new运算符的缺点
用构造函数生成实例对象,有一个缺点,那就是无法共享属性和方法。
比如,在DOG对象的构造函数中,设置一个实例对象的共有属性species。
function DOG(name){
this.name = name;
this.species = '犬科';
}
//然后,生成两个实例对象:
var dogA = new DOG('大毛');
var dogB = new DOG('二毛');
//这两个对象的species属性是独立的,修改其中一个,不会影响到另一个。
dogA.species = '猫科';
alert(dogB.species); // 显示"犬科",不受dogA的影响
//每一个实例对象,都有自己的属性和方法的副本。这不仅无法做到数据共享,也是极大的资源浪费。
- prototype属性的引入
考虑到这一点,Brendan Eich决定为构造函数设置一个prototype属性。
这个属性包含一个对象(以下简称"prototype对象"),所有实例对象需要共享的属性和方法,都放在这个对象里面;那些不需要共享的属性和方法,就放在构造函数里面。
实例对象一旦创建,将自动引用prototype对象的属性和方法。也就是说,实例对象的属性和方法,分成两种,一种是本地的,另一种是引用的。
还是以DOG构造函数为例,现在用prototype属性进行改写:
function DOG(name){
this.name = name;
}
DOG.prototype = { species : '犬科' };
var dogA = new DOG('大毛');
var dogB = new DOG('二毛');
alert(dogA.species); // 犬科
alert(dogB.species); // 犬科
//现在,species属性放在prototype对象里,是两个实例对象共享的。只要修改了prototype对象,就会同时影响到两个实例对象。
DOG.prototype.species = '猫科';
alert(dogA.species); // 猫科
alert(dogB.species); // 猫科
总结
由于所有的实例对象共享同一个prototype对象,那么从外界看起来,prototype对象就好像是实例对象的原型,而实例对象则好像"继承"了prototype对象一样。
这就是Javascript继承机制的设计思想。
以上就是 阮一峰 大佬的 总结
涉及面试题:如何理解原型?如何理解原型链?
- 把所有对象共用得属性放在一个堆内存对象上(共用属性组成得对象),然后让每一个对象得
__propto__
存储这个【共用属性组成得对象】得地址,而这个公用属性就是原型 - 原型得出现就是为了减少不必要得内存消耗,而原型链就是对象通过
__proto__
向当前实例所属类得原型上查找属性或方法得之,如果找到Object
得原型上还是没有找到想要得属性或者方法则查找结束,最终会返回undefined
- 所有引用类型(函数,数组,对象)都拥有__proto__属性(隐式原型)
- 所有函数拥有
prototype
属性(显式原型)(仅限函数) - 原型对象:拥有prototype属性的对象,在定义函数时就被创建
原型链类
-
创建对象有几种方法
-
字面量
var o1 = {name:'xzt'}
var o11 = new Object({name:'o11'})
- 通过构造函数
function Person() {
}
var person = new Person();
person.name = 'Kevin';
console.log(person.name) // Kevin
-
构造函数 才有
prototype
并指向原型对象 -
new
一个构造函数 生成一个实例 -
实例 通过
__proto__
找到原型对象
图中由相互关联的原型组成的链状结构就是原型链,也就是蓝色的这条线。
总结
Object
是所有对象的爸爸,所有对象都可以通过__proto__
找到它Function
是所有函数的爸爸,所有函数都可以通过__proto__
找到它- 函数的
prototype
是一个对象 - 对象的
__proto__
属性指向原型,__proto__
将对象和原型连接起来组成了原型链
涉及面试题:原型如何实现继承?Class 如何实现继承?Class 本质是什么?
首先先来讲下 class
,其实在 JS 中并不存在类,class
只是语法糖,本质还是函数。
class Person {}
Person instanceof Function // true
组合继承
function Person(value){
this.value = value
}
Parent.prototype.getValue = function() {
console.log(this.val)
}
function Child(value) {
Parent.call(this, value)
}
Child.prototype = new Parent()
const child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
以上继承的方式核心是在子类的构造函数中通过 Parent.call(this)
继承父类的属性,然后改变子类的原型为 new Parent()
来继承父类的函数。
这种继承方式优点在于构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数,但是也存在一个缺点就是在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费。
寄生组合继承
这种继承方式对组合继承进行了优化,组合继承缺点在于继承父类函数时调用了构造函数,我们只需要优化掉这点就行了。
function Parent(value) {
this.val = value
}
Parent.prototype.getValue = function() {
console.log(this.val)
}
function Child(value) {
Parent.call(this, value)
}
Child.prototype = Object.create(Parent.prototype, {
constructor: {
value: Child,
enumerable: false,
writable: true,
configurable: true
}
})
const child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
以上继承实现的核心就是将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数。
Class 继承
以上两种继承方式都是通过原型去解决的,在 ES6 中,我们可以使用 class
去实现继承,并且实现起来很简单
class Parent {
constructor(value) {
this.val = value
}
getValue() {
console.log(this.val)
}
}
class Child extends Parent {
constructor(value) {
super(value)
this.val = value
}
}
let child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
class
实现继承的核心在于使用 extends
表明继承自哪个父类,并且在子类构造函数中必须调用 super
,因为这段代码可以看成 Parent.call(this, value)
。
当然了,之前也说了在 JS 中并不存在类,class
的本质就是函数。