这一节梳理对象的继承。
我们主要使用继承来实现代码的抽象和代码的复用,在应用层实现功能的封装。
javascript 的对象继承方式真的是百花齐放,属性继承、原型继承、call/aplly继承、原型链继承、对象继承、构造函数继承、组合继承、类继承… 十几种,每一种都细讲需要花很多时间,这里大致梳理常用的几种。 javascript 中的继承并不是明确规定的,而是通过模仿实现的。下面我们简单梳理几种有代表性的继承。
原型继承
ECMAScript 5 中引入了一个新方法: Object.create
。可以调用这个方法来创建一个新对象。新对象的原型就是调用 create
方法时传入的参数:
let too = {
a: 1
}
let foo = Object.create(too)
console.log(foo.a) // 1
通过使用Object.create
方法, 对象 too 会被自动加入到 foo 的原型上。我们可以手动模拟实现一个Object.create
相同功能的函数:
let too = {
a: 1
}
function create (prot) {
let o = function () {}
o.prototype = prot
return new o()
}
let foo = create(too)
console.log(foo.a) // 1
或者用更简单直白的方式来写:
function Foo() {}
Foo.prototype = {
a: 1
}
let too = new Foo()
console.log(too.a) // 1
原型继承是基于函数的prototype
属性
原型链的继承
function Foo (id) {
this.a = 1234
this.b = id || 0
}
Foo.prototype.showData = function () {
console.log(`${this.a}, id: ${this.b}`)
}
function Too (id) {
Foo.apply(this, arguments)
}
Too.prototype = new Foo()
let bar = new Too(999)
bar.showData() // 1234, id: 999
上面构造函数TOO
通过重新指定prototype
属性,指向了构造函数Foo
的一个实例,然后在Too
构造函数中调用Foo
的构造函数,从而完成对构造函数Foo
功能的继承。实例bar
通过属性__proto__
来访问原型链上的共享属性和方法。
class继承
javascript 中的 class继承又称模拟类继承。ES6中正式引入了 class 关键字来实现类语言方式创建对象。从此我们也可以使用抽象类的方式来实现继承。
// 父类
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);
在JavaScript中没有类的概念,只有对象。虽然我们使用class
关键字,这让 JavaScript
看起来似乎是拥有了”类”,可表面看到的不一定是本质,class
只是语法糖,实质还是原型链那一套。因此,JavaScript中的继承只是对象与对象之间的继承。反观继承的本质,继承便是让子类拥有父类的一些属性和方法,在JavaScript中便是让一个对象拥有另一个对象的属性和方法。
继承的实现是有很多种,这里不一一列举。需要注意的是 javascript 引擎在原型链上查找属性是比较耗时的,对性能有副作用。与此同时我们遍历对象时,原型上的属性也会被枚举出来。要识别属性是在对象上还是从原型上继承的,我们可以使用对象上的hasOwnProperty
方法:
let foo = {
a: 1
}
foo.hasOwnProperty('a') // true
foo.hasOwnProperty('toString') // false
使用hasOwnProperty
方法检测属性是否直接存在于该对象上并不会遍历原型链。
javascript 支持的是实现继承,不支持接口继承,实现继承主要依赖的是原型链。
思考
前面我们讲到的基本是 javascript 怎么实现面向对象编程的一些知识点。
不从概念来讲,简单来说当我们有属性和方法需要被重复使用,或者属性需要被多个对象共享时就需要去考虑继承的问题。在函数层面,大家通常的做法是使用作用域链来实现内层作用域对外层作用域属性或函数的共享访问。举个栗子吧~~
function car (userName) {
let color = 'red'
let wheelNumber = 4
let user = userName
let driving = function () {
console.log(`${user} 的汽车,${wheelNumber}个轮子滚啊滚...`)
}
let stop = function () {
console.log(`${user} 的汽车,${wheelNumber}个轮子滚不动了,嘎。。。`)
}
return {
driving: driving,
stop: stop
}
}
var maruko = car('小丸子')
maruko.driving() // 小丸子 的汽车,4个轮子滚啊滚...
maruko.stop() // 小丸子 的汽车,4个轮子滚不动了,嘎。。。
var nobita = car('大雄')
nobita.driving() // 大雄 的汽车,4个轮子滚啊滚...
nobita.stop() // 大雄 的汽车,4个轮子滚不动了,嘎。。。
这。。。什么鬼。是不是有种似曾相识的感觉,这其实就是经典的闭包
,jquery 年代很多插件 js 库都采用这种方式去封装独立的功能。说闭包也是继承是不是有点勉强,但是 javascript 里函数也是对象,闭包利用函数的作用域链来访问上层作用域的属性和函数。当然像闭包这样不使用this
去实现私有属性比较麻烦, 闭包只适合单实例的场景。再举一个栗子:
function GoToBed (name) {
console.log(`${name}, 睡觉了...`)
}
function maruko () {
let name = '小丸子'
function dinner () {
console.log(`${name}, 吃完晚餐`)
GoToBed(name)
}
dinner()
}
function nobita () {
let name = '大雄'
function homework () {
console.log(`${name}, 做完作业`)
GoToBed(name)
}
homework()
}
maruko()
nobita()
// 小丸子, 吃完晚餐
// 小丸子, 睡觉了...
// 大雄, 做完作业
// 大雄, 睡觉了...
像上面栗子中这样,以面向过程的方式将公共方法抽离到上层作用域的用法比较常见, 至少我很长时间都是这么干的。将GoToBed
函数抽离到全局对象中,函数maruko
、nobita
内部直接通过作用域链查找GoToBed
函数。这种松散结构的代码块组织其实跟上面闭包含义是差不多的。
所以依据作用域链来进行公共属性、方法的管理严格意义上不能算是继承, 只能算是 javascript 面向过程的一种代码抽象分解的方式,一种编程范式。这种范式编程是基于作用域链
,与前面讲的继承是基于原型链
的本质区别是属性查找方式的不同。
全局对象 window 形成一个闭合上下文,如果我们将整个 window 对象假设为一个全局函数,所有创建的局部函数都是该函数的内部函数。当我们使用这个假设时很多问题就要清晰多了,全局函数在页面被关闭前是一直存在的,且在存活期间为内嵌函数提供执行环境,所有内嵌函数都共享对全局环境的读写权限。
这种函数调用时命令式的,函数组织是嵌套的,使用闭包(函数嵌套)的方式来组织代码流是无模式的一种常态。