原文链接:Common Misconceptions About Inheritance in JavaScript
作者:Eric Elliott
翻译:野草
本文首发于前端早读课【第911期】
什么鬼!!!当程序员遇到违反“最小惊讶原则”,违反他们直觉的时候,他们会不由自主地发出这句感叹!
举个栗子:
.1 + .2
0.30000000000000004什么鬼!!!(+﹏+)~@
当我遇到一些资深前端开发,却不知道JavaScript原型继承(Prototypal inheritance)的时候,我也情不自禁地发出“什么鬼!!!”的感叹。原型继承可是计算机科学史上最重大的变革之一,也是JavaScript语言两大支柱之一。这对我来说就像,一位专业摄影师却还没学会曝光三角形(Exposure triangle)原理一样不可思议,这可是控制照片风格的基础啊。简而言之:
如果你不懂原型,那你根本就不懂JavaScript。
++问:类继承和原型继承不是同一回事儿吗,只是风格选择而已?++
++答:不是!++
类继承和原型继承不论从本质上还是从语法上来说,都是两个截然不同的概念。
二者之间有着区分彼此的本质性特征。要完全看懂本文,你必须牢牢记住以下几点:
类继承中,实例继承自模版(类),并且创建子类关系。换言之,你不能像使用实例一样使用类。实例由类创建出来,并且能调用类的方法,但是你不能直接在类上调用本身的方法。你必须创建一个实例,然后在实例上应用那些方法。
原型继承中,实例继承自其他的实例。它们使用的是原型委托(将实例的原型对象指向一个模板对象),这种方式被Kyle Simpson(你不知道的JS系列作者)称为对象关联(OLOO, Objects Linking to Other Objects)。使用这种关联继承,你只是将模板对象的属性拷贝到新的实例中而已。
理解上述区别至关重要。类继承的机制在创建子类的同时,也不小心创建了类的层级。
原型继承却可以避免创建类似的层级。建议原型链越短越好,其实很容易将很多原型扁平化为一个单委托原型。
总结:
类是一个抽象的模版。
原型是一个具体的对象实例。
++问:JavaScript中类不是创建对象的正确方式吗?++
++答:不是!++
JavaScript中创建对象有几种方式。最常见的一种是对象字面量方式。看个例子,用ES6语法写的对象:
// ES6 或称 ES2015, 因为发布于2015.
let mouse = {
furColor: 'brown',
legs: 4,
tail: 'long, skinny',
describe () {
return `A mouse with ${this.furColor} fur,
${this.legs} legs, and a ${this.tail} tail.`;
}
};
当然,对象字面量方式比ES6出来早多了,但之前的写法缺少对象中函数方法的简写方式,以及定义变量时你只能用var
而用不了let
。对了,describe()
方法中的模板字符串在ES5中也是不能用的。
我们可以利用ES5中的Object.create()
附上对象的委托原型:
let animal = {
animalType: 'animal',
describe () {
return `An ${this.animalType}, with ${this.furColor} fur,
${this.legs} legs, and a ${this.tail} tail.`;
}
};
let mouse = Object.assign(Object.create(animal), {
animalType: 'mouse',
furColor: 'brown',
legs: 4,
tail: 'long, skinny'
});
让我们仔细分析这个例子。animal
是委托原型,mouse
是实例。当你尝试获取mouse
对象上没有的属性时,JavaScript将会在animal
(委托对象)上寻找这个属性。
Object.assign()
是ES6的新特性,由Rick Waldron提出。其实它早已在一些知名的库中被实现,比如jQuery中的$.extend()
,Underscore中的_.extend()
,还有Lodash中的assign()
。该方法传入一个目标对象,以及任何多个用逗号隔开的源对象,它将会从最后一个源对象开始拷贝所有的可枚举属性到目标对象。若存在属性名冲突,前者会被后者覆盖。
Douglas Crockford提出了ES5中的Object.create()
,它能使我们在不用构造器和new
关键词的情况下,设置对象的委托原型。
本文不涉及到构造函数,因为我非常不推荐这种方式。太多滥用构造函数的例子,以及太多由此引起的麻烦。值得一提的是,很多聪明人并不同意我的看法。没关系,聪明人想怎么做就怎么做。
总有明智的人会听取Douglas Crockford的意见:
如果某个特性有时会不靠谱,而且存在一个更好的选择,那么还是选择那个更好的方式。
++问:难道不需要构造函数来定义实例的行为,以及进行实例化吗?++
++答:不需要!++
任何函数均可创建并返回对象。当该函数不是用作构造函数来创建时,它被称为工厂函数(factory function)。
更佳选择
let animal = {
animalType: 'animal',
describe () {
return `An ${this.animalType} with ${this.furColor} fur,
${this.legs} legs, and a ${this.tail} tail.`;
}
};
let mouseFactory = function mouseFactory () {
return Object.assign(Object.create(animal), {
animalType: 'mouse',
furColor: 'brown',
legs: 4,
tail: 'long, skinny'
});
};
let mickey = mouseFactory();
通常我不会将函数命名为factory
,那只是一个形象的比喻。一般我就简单称之为mouse()
。
++问:不需要用构造函数来创造私有变量或者属性吗?++
++答:不需要++
JavaScript中,当你返回一个函数,该函数可以访问外部函数的变量。当你使用这个函数的时候,JS引擎创建了一个闭包。闭包是JavaSript中非常常见的模式,它通常用来创建私有变量。
闭包并不是构造函数独有的。任何函数均可创建闭包:
let animal = {
animalType: 'animal',
describe () {
return `An ${this.animalType} with ${this.furColor} fur,
${this.legs} legs, and a ${this.tail} tail.`;
}
};
let mouseFactory = function mouseFactory () {
let secret = 'secret agent';
return Object.assign(Object.create(animal), {
animalType: 'mouse',
furColor: 'brown',
legs: 4,
tail: 'long, skinny',
profession () {
return secret;
}
});
};
let james = mouseFactory();
未完待续。