|
| 1 | +# JavaScript深入之继承 |
| 2 | + |
| 3 | +## 写在前面 |
| 4 | + |
| 5 | +本文讲解JavaScript各种继承方式和优缺点。 |
| 6 | + |
| 7 | +注意: |
| 8 | + |
| 9 | +跟《JavaScript深入之创建对象》一样,更像是笔记,哎,再让我感叹一句:《JavaScript高级程序设计》写得真是太好了! |
| 10 | + |
| 11 | +## 1.原型链继承 |
| 12 | + |
| 13 | +```js |
| 14 | +function Parent () { |
| 15 | + this.name = 'kevin'; |
| 16 | +} |
| 17 | + |
| 18 | +Parent.prototype.getName = function () { |
| 19 | + console.log(this.name); |
| 20 | +} |
| 21 | + |
| 22 | +function Child () { |
| 23 | + |
| 24 | +} |
| 25 | + |
| 26 | +Child.prototype = new Parent(); |
| 27 | + |
| 28 | +var child1 = new Child(); |
| 29 | + |
| 30 | +console.log(child1.getName()) // kevin |
| 31 | +``` |
| 32 | + |
| 33 | +问题: |
| 34 | + |
| 35 | +1.引用类型的属性被所有实例共享,举个例子: |
| 36 | + |
| 37 | +```js |
| 38 | +function Parent () { |
| 39 | + this.names = ['kevin', 'daisy']; |
| 40 | +} |
| 41 | + |
| 42 | +function Child () { |
| 43 | + |
| 44 | +} |
| 45 | + |
| 46 | +Child.prototype = new Parent(); |
| 47 | + |
| 48 | +var child1 = new Child(); |
| 49 | + |
| 50 | +child1.names.push('yayu'); |
| 51 | + |
| 52 | +console.log(child1.names); // ["kevin", "daisy", "yayu"] |
| 53 | + |
| 54 | +var child2 = new Child(); |
| 55 | + |
| 56 | +console.log(child2.names); // ["kevin", "daisy", "yayu"] |
| 57 | + |
| 58 | +``` |
| 59 | + |
| 60 | +2.在创建 Child 的实例时,不能向Parent传参 |
| 61 | + |
| 62 | +## 2.借用构造函数(经典继承) |
| 63 | + |
| 64 | +```js |
| 65 | +function Parent () { |
| 66 | + this.names = ['kevin', 'daisy']; |
| 67 | +} |
| 68 | + |
| 69 | +function Child () { |
| 70 | + Parent.call(this); |
| 71 | +} |
| 72 | + |
| 73 | +var child1 = new Child(); |
| 74 | + |
| 75 | +child1.names.push('yayu'); |
| 76 | + |
| 77 | +console.log(child1.names); // ["kevin", "daisy", "yayu"] |
| 78 | + |
| 79 | +var child2 = new Child(); |
| 80 | + |
| 81 | +console.log(child2.names); // ["kevin", "daisy"] |
| 82 | +``` |
| 83 | + |
| 84 | +优点: |
| 85 | + |
| 86 | +1.避免了引用类型的属性被所有实例共享 |
| 87 | + |
| 88 | +2.可以在 Child 中向 Parent 传参 |
| 89 | + |
| 90 | +举个例子: |
| 91 | + |
| 92 | +```js |
| 93 | +function Parent (name) { |
| 94 | + this.name = name; |
| 95 | +} |
| 96 | + |
| 97 | +function Child (name) { |
| 98 | + Parent.call(this, name); |
| 99 | +} |
| 100 | + |
| 101 | +var child1 = new Child('kevin'); |
| 102 | + |
| 103 | +console.log(child1.name); // kevin |
| 104 | + |
| 105 | +var child2 = new Child('daisy'); |
| 106 | + |
| 107 | +console.log(child2.name); // daisy |
| 108 | + |
| 109 | +``` |
| 110 | + |
| 111 | +缺点: |
| 112 | + |
| 113 | +方法都在构造函数中定义,每次创建实例都会创建一遍方法。 |
| 114 | + |
| 115 | +## 3.组合继承 |
| 116 | + |
| 117 | +原型链继承和经典继承双剑合璧。 |
| 118 | + |
| 119 | +```js |
| 120 | +function Parent (name) { |
| 121 | + this.name = name; |
| 122 | + this.colors = ['red', 'blue', 'green']; |
| 123 | +} |
| 124 | + |
| 125 | +Parent.prototype.getName = function () { |
| 126 | + console.log(this.name) |
| 127 | +} |
| 128 | + |
| 129 | +function Child (name, age) { |
| 130 | + |
| 131 | + Parent.call(this, name); |
| 132 | + |
| 133 | + this.age = age; |
| 134 | + |
| 135 | +} |
| 136 | + |
| 137 | +Child.prototype = new Parent(); |
| 138 | + |
| 139 | +var child1 = new Child('kevin', '18'); |
| 140 | + |
| 141 | +child1.colors.push('black'); |
| 142 | + |
| 143 | +console.log(child1.name); // kevin |
| 144 | +console.log(child1.age); // 18 |
| 145 | +console.log(child1.colors); // ["red", "blue", "green", "black"] |
| 146 | + |
| 147 | +var child2 = new Child('daisy', '20'); |
| 148 | + |
| 149 | +console.log(child2.name); // daisy |
| 150 | +console.log(child2.age); // 20 |
| 151 | +console.log(child2.colors); // ["red", "blue", "green"] |
| 152 | + |
| 153 | +``` |
| 154 | + |
| 155 | +优点:融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。 |
| 156 | + |
| 157 | +## 4.原型式继承 |
| 158 | + |
| 159 | +```js |
| 160 | +function createObj(o) { |
| 161 | + function F(){} |
| 162 | + F.prototype = o; |
| 163 | + return new F(); |
| 164 | +} |
| 165 | +``` |
| 166 | + |
| 167 | +就是 ES5 Object.create 的模拟实现,将传入的对象作为创建的对象的原型。 |
| 168 | + |
| 169 | +缺点: |
| 170 | + |
| 171 | +包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样。 |
| 172 | + |
| 173 | +```js |
| 174 | +var person = { |
| 175 | + name: 'kevin', |
| 176 | + friends: ['daisy', 'kelly'] |
| 177 | +} |
| 178 | + |
| 179 | +var person1 = createObj(person); |
| 180 | +var person2 = createObj(person); |
| 181 | + |
| 182 | +person1.name = 'person1'; |
| 183 | +console.log(person2.name); // kevin |
| 184 | + |
| 185 | +person1.firends.push('taylor'); |
| 186 | +console.log(person2.friends); // ["daisy", "kelly", "taylor"] |
| 187 | +``` |
| 188 | + |
| 189 | +注意:修改`person1.name`的值,`person2.name`的值并未发生改变,并不是因为`person1`和`person2`有独立的 name 值,而是因为`person1.name = 'person1'`,给`person1`添加了 name 值,并非修改了原型上的 name 值。 |
| 190 | + |
| 191 | +## 5. 寄生式继承 |
| 192 | + |
| 193 | +创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象。 |
| 194 | + |
| 195 | +```js |
| 196 | +function createObj (o) { |
| 197 | + var clone = object.create(o); |
| 198 | + clone.sayName = function () { |
| 199 | + console.log('hi'); |
| 200 | + } |
| 201 | + return clone; |
| 202 | +} |
| 203 | +``` |
| 204 | + |
| 205 | +缺点:跟借用构造函数模式一样,每次创建对象都会创建一遍方法。 |
| 206 | + |
| 207 | +## 6. 寄生组合式继承 |
| 208 | + |
| 209 | +为了方便大家阅读,在这里重复一下组合继承的代码: |
| 210 | + |
| 211 | +```js |
| 212 | +function Parent (name) { |
| 213 | + this.name = name; |
| 214 | + this.colors = ['red', 'blue', 'green']; |
| 215 | +} |
| 216 | + |
| 217 | +Parent.prototype.getName = function () { |
| 218 | + console.log(this.name) |
| 219 | +} |
| 220 | + |
| 221 | +function Child (name, age) { |
| 222 | + Parent.call(this, name); |
| 223 | + this.age = age; |
| 224 | +} |
| 225 | + |
| 226 | +Child.prototype = new Parent(); |
| 227 | + |
| 228 | +var child1 = new Child('kevin', '18'); |
| 229 | + |
| 230 | +console.log(child1) |
| 231 | +``` |
| 232 | + |
| 233 | +组合继承最大的缺点是会调用两次父构造函数。 |
| 234 | + |
| 235 | +一次是设置子类型实例的原型的时候: |
| 236 | + |
| 237 | +```js |
| 238 | +Child.prototype = new Parent(); |
| 239 | +``` |
| 240 | + |
| 241 | +一次在创建子类型实例的时候: |
| 242 | + |
| 243 | +```js |
| 244 | +var child1 = new Child('kevin', '18'); |
| 245 | +``` |
| 246 | + |
| 247 | +回想下 new 的模拟实现,其实在这句中,我们会执行: |
| 248 | + |
| 249 | +```js |
| 250 | +Parent.call(this, name); |
| 251 | +``` |
| 252 | + |
| 253 | +在这里,我们又会调用了一次 Parent 构造函数。 |
| 254 | + |
| 255 | +所以,在这个例子中,如果我们打印 child1 对象,我们会发现 Child.prototype 和 child1 都有一个属性为`colors`,属性值为`['red', 'blue', 'green']`。 |
| 256 | + |
| 257 | +那么我们该如何精益求精,避免这一次重复调用呢? |
| 258 | + |
| 259 | +如果我们不使用 Child.prototype = new Parent() ,而是间接的让 Child.prototype 访问到 Parent.prototype 呢? |
| 260 | + |
| 261 | +看看如何实现: |
| 262 | + |
| 263 | +```js |
| 264 | +function Parent (name) { |
| 265 | + this.name = name; |
| 266 | + this.colors = ['red', 'blue', 'green']; |
| 267 | +} |
| 268 | + |
| 269 | +Parent.prototype.getName = function () { |
| 270 | + console.log(this.name) |
| 271 | +} |
| 272 | + |
| 273 | +function Child (name, age) { |
| 274 | + Parent.call(this, name); |
| 275 | + this.age = age; |
| 276 | +} |
| 277 | + |
| 278 | +// 关键的三步 |
| 279 | +var F = function () {}; |
| 280 | + |
| 281 | +F.prototype = Parent.prototype; |
| 282 | + |
| 283 | +Child.prototype = new F(); |
| 284 | + |
| 285 | + |
| 286 | +var child1 = new Child('kevin', '18'); |
| 287 | + |
| 288 | +console.log(child1); |
| 289 | +``` |
| 290 | + |
| 291 | +最后我们封装一下这个继承方法: |
| 292 | + |
| 293 | +```js |
| 294 | +function object(o) { |
| 295 | + function F() {} |
| 296 | + F.prototype = o; |
| 297 | + return new F(); |
| 298 | +} |
| 299 | + |
| 300 | +function prototype(child, parent) { |
| 301 | + var prototype = object(parent.prototype); |
| 302 | + prototype.constructor = child; |
| 303 | + child.prototype = prototype; |
| 304 | +} |
| 305 | + |
| 306 | +// 当我们使用的时候: |
| 307 | +prototype(Child, Parent); |
| 308 | +``` |
| 309 | + |
| 310 | +引用《JavaScript高级程序设计》中对寄生组合式继承的夸赞就是: |
| 311 | + |
| 312 | +这种方式的高效率体现它只调用了一次Parent构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。 |
| 313 | + |
| 314 | +## 相关链接 |
| 315 | + |
| 316 | +[《JavaScript深入之从原型到原型链》](https://github.com/mqyqingfeng/Blog/issues/2) |
| 317 | + |
| 318 | +[《JavaScript深入之call和apply的模拟实现》](https://github.com/mqyqingfeng/Blog/issues/11) |
| 319 | + |
| 320 | +[《JavaScript深入之new的模拟实现》](https://github.com/mqyqingfeng/Blog/issues/13) |
| 321 | + |
| 322 | +[《JavaScript深入之创建对象》](https://github.com/mqyqingfeng/Blog/issues/15) |
| 323 | + |
| 324 | +## 深入系列 |
| 325 | + |
| 326 | +JavaScript深入系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 |
| 327 | + |
| 328 | +JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。 |
| 329 | + |
| 330 | +如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。 |
| 331 | + |
| 332 | + |
| 333 | + |
| 334 | + |
0 commit comments