《JavaScript 高级程序设计》 读书笔记--从原型链复习继承

这一篇进入正题来复习一下 JavaScript 中对象的继承。“高程”中一共列举了 6 种继承的方式。看起来是有些吓人,但仔细梳理就能发现其中也是有一个演变过程的。这篇笔记就是我本身对这个过程的理解。若是有不足的地方,还但愿各位能够指出。java

再次安利一下“高程”,真的写得很是棒。有必定基础和项目经验的同窗绝对要去看一看,能提升很多。浏览器

基础概念

在进入继承以前,咱们再把一些基本概念复习一下。数据结构

0. 构造函数、原型对象、实例

谈到对象,必定会出现这三个概念。在 JavaScript 中,原型对象不须要咱们手动去定义,当咱们定义一个类或者构造函数后,JavaScript 会自动生成对应的原型对象,咱们能够经过 prototype 属性访问;咱们写的 function A(){} 就是构造函数;而咱们new 调用构造函数返回的值就是对象的实例函数

// 构造函数:其实就是通常的函数。函数名大写只是一个约定规则而已。
// 事实上除了 new 以外,咱们也能够像调用通常的函数同样使用。
// 在 ES6 中就是 Class 里面的 constructor
function A(){
  this.a = 'a'
}

// 原型对象:当咱们定义对象的时候,就会生成一个对应的原型对象。
// 该函数的 prototype 属性就指向原型对象,这个就是原型链的精髓。
A.prototype

// 实例: 用 new 调用构造函数的返回值。
var a = new A()
复制代码

1. 原型链

下面再来看一下原型链的概念。原型链的概念咱们必定不会陌生。那么就很少说了,直接上图。这是一个最基本的原型链,咱们要仔细理解这张图而且搞清楚 实例对象原型构造函数以及与 Object 之间的关系。 学习

在复习完上面两个概念以后(特别是原型链),相信在后面理解继承的时候会有所帮助。若是在后面有所困惑的话,不妨回来看看基础概念。this

下面就开始进入正题。spa


JavaScript 中的继承

1. 原型链继承

咱们对上面基础的例子作一个拓展。再加入一个对象 B,咱们一样画成图。 prototype

AB 是互相独立的两个对象(类),而且它们都是 继承 了 JavaScript 的对象之祖——Object对象。请注意,在通常的对象中,就已经存在了一个 对象Object对象 的继承关系了。3d

那么如今咱们要让对象B去继承对象A,就能够模仿对象和Object的关系,修改B的原型对象的指向。 code

这么一来,B就能够顺着原型链访问到A了。因为如今B.prototype = A.prototype了,那么在B.prototype上作的任何修改都会影响到A.prototype了。因此咱们再稍微调整一下,让 B.prototype = new A()

这样,完整的原型链继承的关系图就出来了。

简单的示例代码以下,小伙伴们能够在 Chrome 中试玩一下。

function A(){ this.a = 'a' }
function B(){ this.b = 'b' }

// 不要这么作,由于修改 B 的 prototype 会影响 A 的 prototype
// B.prototype = A.prototype
B.prototype = new A()
B.prototype.constructor = B
复制代码

对象的 constructor

在上面的示例代码中,最后一行咱们对对象 B 的构造函数从新进行了赋值。这是由于,当咱们改变了 B.prototype 的时候,会切断原来 B 的构造函数与 B.prototype 之间的联系。

虽然这个属性对咱们的继承关系没有影响(instanceof 方法结果仍然正确)。可是从代码含义上来讲,咱们最好仍是修改称为正确的指向。

另外,对于实例来讲 a.constructor.prototype === A.prototype // true。即咱们能够经过实例的构造函数去给对象原型添加属性和方法。尽管没人会推荐咱们这么去作,但让属性指向正确的值会比较好。

和对象的建立同样,原型链的方法是比较简单的。可是也有一个明显的缺陷,就是“没法”对父类传不一样的值。即 B.prototype = new A(xx) 时以后全部的 B 的示例都会带上这个值,所以就产生了局限性。

回想一下在建立对象时,咱们是怎么解决这个问题的?

2. 构造函数继承

在建立对象时,咱们知道不一样的实例在建立时只要向构造函数中传入不一样的值,就会获得不一样的值。那么回到继承上,为了解决原型链继承没法向父类传递不一样值的问题,咱们一样也须要借助构造函数。

在进入正题前,咱们再看一下构造函数,而后想一下若是不用 new 调用构造函数会是怎样? 下面是一个 Person 的构造函数。通常来讲咱们使用 new 关键字建立 Person 的实例。可是有没有想过,构造函数也是函数,若是咱们不用 new 而是普通地调用会是怎么状况呢?

function Person(name, age, sex){
  this.name = name
  this.age = age
  this.sex = sex
}

// 直接调用会是怎么状况呢?
Person('Kizunaai', 2, 'female')
复制代码

熟悉 this 特性的小伙伴确定能反应过来。独立调用函数时,若在非严格模式下,this 指向的是 window(浏览器环境)。那么咱们看一下 window

window 中果真就有了 age 这个属性,而且值为 2。也就是说,直接调用构造函数就至关于把构造函数中的属性赋给调用它的对象了。

好,趁热打铁,咱们直接来看代码。

function Person(name, age, sex){
  this.name = name
  this.age = age
  this.sex = sex
}

function VTuber(name, age, sex){
  // 调用 Person 的构造函数实际上就是把 Person 的值赋给 VTuber
  // 在 ES6 中就是 super()
  Person.call(this, name, age, sex)
}

var kizunaai = new VTuber('kizunaai', 2, 'female')
var luna = new VTuber('luna', 100, 'female')

kizunaai.name // 'kizunaai'
luna.name // 'luna'
复制代码

咱们在 Chrome 中分别打印一下以前的实例。能够看到 VTuber 的实例只是包含了 Person 的属性而已,而在原型链上二者是没有任何关系的。

因此再想一下,构造函数的方法其实真的是“继承”吗?由于对于“子类”来讲,是没有办法调用父类原型上的方法的。而在用构造函数建立对象时咱们就已经知道,把方法写在构造函数里显然不是一个好的解决方法。

3. 组合式继承

既然原型链和构造函数正好能弥补互相之间的缺陷,组合起来咱们能愉快地进行继承了。也没什么新的知识点,就直接上代码了。

function Person(name, age, sex){
  this.name = name
  this.age = age
  this.sex = sex
}

Person.prototype.sayHello = function(){
  return `${this.name} say hello ~`
}

function VTuber(name, age, sex){
  // 构造函数保证了不一样值的传递
  Person.call(this, name, age, sex)
}

// 原型链保证了方法的传递(还有意义上)
VTuber.prototype = new Person()
VTuber.prototype.constructor = VTuber

var kizunaai = new VTuber('kizunaai', 2, 'female')
var luna = new VTuber('luna', 100, 'female')

kizunaai.name // 'kizunaai'
luna.sayHello() // 'luna say hello ~'
复制代码

4. 寄生组合式继承

组合式继承是咱们最经常使用的继承方法,几乎能够说是知足了咱们的需求。硬要挑刺的话,也就是父类的构造函数调用两次的问题了。

// 以以前的代码为例
// 第一次在子类的构造函数中调用
Person.call(this, name, age, sex)

// 第二次在创建原型链时调用
VTuber.prototype = new Person()
复制代码

其中第一次是必定省不掉的,要下功夫的话就是在第二次创建原型链的时候了。仍是之前面的代码为例,咱们就这么继承,数据结构会是怎么样的?

能够看到在 VTuber.prototype 上也有 name, age, sex 三个属性,但实际上这三个属性根本没有意义。那么解决的思路就有了,咱们须要借助一个空的对象来搭一座桥。(千万别说让 VTuber.prototype = Person.prototype 了,理由参考原型链那部分)

function Person(name, age, sex){
  this.name = name
  this.age = age
  this.sex = sex
}

Person.prototype.sayHello = function(){
  return `${this.name} say hello ~`
}

function VTuber(name, age, sex){
  // 构造函数保证了不一样值的传递
  Person.call(this, name, age, sex)
}

// 咱们要借用一个空对象做为过渡
// VTuber.prototype = new Person()
function A(){}
A.prototype = Person.prototype
VTuber.prototype = new A()

VTuber.prototype.constructor = VTuber

var kizunaai = new VTuber('kizunaai', 2, 'female')
var luna = new VTuber('luna', 100, 'female')

kizunaai.name // 'kizunaai'
luna.sayHello() // 'luna say hello ~'
复制代码

上面咱们借用了一个 A 来打了个桥。这样一来就在原型链上就没有多余的属性了。(其实两次构造函数是确定要调的,只是第二次调谁的问题)

而这种搭桥的方式,在“高程”中也被称为是原型式继承。其中关于原型式和寄生式分别和原型链、构造函数相对应,感受没有这两种直观并且也不经常使用(我的感受)因此就不作展开了,有兴趣的小伙伴仍是推荐去阅读“高程”。

小结

至此,有关于继承的笔记就到此为止。这一篇顺着对象的原型链的概念开始,介绍了 JavaScript 中对象继承的几种方式。其中组合继承的方式是咱们最多见也是用的最广的,咱们须要好好了解一下。

在看书的过程当中,像这样给本身抛点问题,找一找方法间的演变脉络即颇有趣也很容易理解。不知道各位小伙伴有什么好的学习方法呢?不妨互相交流一下吧~

相关文章
相关标签/搜索