资讯专栏INFORMATION COLUMN

继承的实现方式及原型概述 | JavaScript 随笔

chenjiang3 / 854人阅读

摘要:每一个对象直接量都是的子类,即构造函数中的构造函数与普通函数并没有什么两样,只不过在调用时,前面加上了关键字,就当成是构造函数了。由于没有传入变量,在调用的构造函数时,会出错这个问题可以通过一个空对象来解决改自。

对于 OO 语言,有一句话叫“Everything is object”,虽然 JavaScript 不是严格意义上的面向对象语言,但如果想要理解 JS 中的继承,这句话必须时刻铭记于心。

JS 的语法非常灵活,所以有人觉得它简单,因为怎么写都是对的;也有人觉得它难,因为很难解释某些语法的设计,谁能告诉我为什么 typeof null 是 object 而 typeof undefined 是 undefined 吗?并且这是在 null == undefined 的前提下。很多我们自认为“懂”了的知识点,细细琢磨起来,还是会发现有很多盲点,“无畏源于无知”吧……

1. 简单对象

既然是讲继承,自然是从最简单的对象说起:

var dog = {
  name: "tom"
}

这便是对象直接量了。每一个对象直接量都是 Object 的子类,即

dog instanceof Object; // true
2. 构造函数

JS 中的构造函数与普通函数并没有什么两样,只不过在调用时,前面加上了 new 关键字,就当成是构造函数了。

    function Dog(name) {
      this.name = name;
    }


var dog = new Dog("tom");

dog instanceof Dog; // true

两个问题,第一,不加 new 关键字有什么后果?

那么 Dog 函数中的 this 在上下文(Context)中被解释为全局变量,具体在浏览器端的话是 window 对象,在 node 环境下是一个 global 对象。

第二,dog 的值是什么?很简单,undefined 。Do>g 函数没有返回任何值,执行结束后,dog 的值自然是 undefined 。

关于 new 的过程,这里也顺便介绍一下,这个对后面理解原型(prototype)有很大的帮助:

创建一个空的对象,仅包含 Object 的属性和方法。
将 prototype 中的属性和方法创建一份引用,赋给新对象。
将 this 上的属性和方法新建一份,赋给新对象。
返回 this 对象,忽略 return 语句。
需要明确的是,prototype 上的属性和方法是实例间共享的,this 上的属性和方法是每个实例独有的。

3. 引入 prototype

现在为 Dog 函数加上 prototype,看一个例子:

function Dog(name) {
  this.name = name;
  this.bark = function() {};
}

Dog.prototype.jump = function() {};
Dog.prototype.species = "Labrador";
Dog.prototype.teeth = ["1", "2", "3", "4"];

var dog1 = new Dog("tom"),
    dog2 = new Dog("jerry");

dog1.bark !== dog2.bark; // true
dog1.jump === dog2.jump; // true

dog1.teeth.push("5");
dog2.teeth; // ["1", "2", "3", "4", "5"]

看到有注释的那三行应该可以明白“引用”和“新建”的区别了。

那么我们经常说到的“原型链”到底是什么呢?这个术语出现在继承当中,它用于表示对象实例中的属性和方法来自于何处(哪个父类)。好吧,这是笔者的解释。

- Object
  bark: Dog/this.bark()
  name: "tom"
- __proto__: Object
    jump: Dog.prototype.jump()
    species: "Labrador"
  + teeth: Array[4]
  + constructor: Dog()
  + __proto__: Object  

上面的是 dog1 的原型链,不知道够不够直观地描述“链”这个概念。

其中,bark 和 name 是定义在 this 中的,所以最顶层可以看到它俩。
然后,每一个对象都会有一个 proto 属性(IE 11+),它表示定义在原型上的属性和方法,所以 jump、species 和 teeth 自然就在这儿了。

最后就一直向上找 proto 中的属性和方法。

4. 继承的几种实现 4.1 通过 call 或者 apply

继承在编程中有两种说法,一个叫 inherit,另一个是 extend 。前者是严格意义上的继承,即存在父子关系,而后者仅仅是一个类扩展了另一个类的属性和方法。那么 call 和 apply 就属于后者的范畴。怎么说?

function Animal(gender) {
  this.gender = gender;
}

function Dog(name, gender) {
  Animal.call(this, gender);
  this.name = name;
}

var dog = new Dog("tom", "male");

dog instanceof Animal; // false

虽然在 dog 对象中有 gender 属性,但 dog 却不是 Animal 类型。甚至,这种方式只能“继承”父类在 this 上定义的属性和方法,并不能继承 Animal.prototype 中的属性和方法。

4.2 通过 prototype 实现继承

要实现继承,必须包含“原型”的概念。下面是很常用的继承方式。

function Dog(name) {
  Animal.call(this);
}

Dog.prototype = new Animal(); // 先假设 Animal 函数没有参数
Dog.prototype.constructor = Dog;

var dog = new Dog("tom");

dog instanceof Animal; // true

继承的结果有两个:

获得父类的属性和方法;
正确通过 instanceof 的测试。
prototype 也是对象,它是创建实例时的装配机,这个在前面有提过。new Animal() 的值包含 Animal 实例所有的属性和方法,既然它赋给了 Dog 的 prototype,那么 Dog 的实例自然就获得了父类的所有属性和方法。

并且,通过这个例子可以知道,改变 Dog 的 prototype 属性可以改变 instanceof 的测试结果,也就是改变了父类。

然后,为什么要在 Dog 的构造函数中调用 Animal.call(this)?

因为 Animal 中可能在 this 上定义了方法和函数,如果没有这句话,那么所有的这一切都会给到 Dog 的 prototype 上,根据前面的知识我们知道,prototype 中的属性和方法在实例间是共享的。

我们希望将这些属性和方法依然保留在实例自身的空间,而不是共享,因此需要重写一份。

至于为什么要修改 constructor,只能说是为了正确的显示原型链吧,它并不会影响 instanceof 的判断。或者有其他更深的道理我并不知道……

4.3 利用空对象实现继承

上面的继承方式已经近乎完美了,除了两点:

Animal 有构造参数,并且使用了这些参数怎么办?
在 Dog.prototype 中多了一份定义在 Animal 实例中冗余的属性和方法。

function Animal(name) {
  name.doSomething();
}

function Dog(name) {
  Animal.call(this, name);
}

Dog.prototype = new Animal(); // 由于没有传入name变量,在调用Animal的构造函数时,会出错
Dog.prototype.constructor = Dog;

这个问题可以通过一个空对象来解决(改自 Douglas Crockford)。

function DummyAnimal() {}
DummyAnimal.prototype = Animal.prototype;

Dog.prototype = new DummyAnimal();
Dog.prototype.constructor = Dog;

他的原始方法是下面的 object:

function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

Dog.prototype = object(Animal.prototype);
Dog.prototype.constructor = Dog;
4.4 利用 proto 实现继承

现在就只剩下一个问题了,如何把冗余属性和方法去掉?

其实,从第 3 小节介绍原型的时候就提到了 proto 属性,instanceof 运算符是通过它来判断是否属于某个类型的。

所以我们可以这么继承:

function Dog() {
  Animal.call(this);
}

Dog.prototype = {
  __proto__: Animal.prototype,
  constructor: Dog
};

如果不考虑兼容性的话,这应该是从 OO 的角度来看最贴切的继承方式了。

4.5 拷贝继承

这个方式也只能称之为 extend 而不是 inherit,所以也没必要展开说。

像 Backbone.Model.extend、jQuery.extend 或者 _.extend 都是拷贝继承,可以稍微看一下它们是怎么实现的。(或者等我自己再好好研究之后过来把这部分补上吧)

5. 个人小结

当我们在讨论继承的实现方式时,给我的感觉就像孔乙己在炫耀“茴香豆”的“茴”有几种写法一样。继承是 JS 中占比很大的一块内容,所以很多库都有自己的实现方式,它们并没有使用我认为的“最贴切”的方法,为什么?JS 就是 JS,它生来就设计得非常灵活,所以我们为什么不利用这个特性,而非得将 OO 的做法强加于它呢?

通过继承,我们更多的是希望获得父类的属性和方法,至于是否要保证严格的父类/子类关系,很多时候并不在乎,而拷贝继承最能体现这一点。对于基于原型的继承,会在代码中看到各种用 function 定义的类型,而拷贝继承更通用,它只是将一个对象的属性和方法拷贝(扩展)到另一个对象而已,并不关心原型链是什么。

当然,在我鼓吹拷贝继承多么多么好时,基于原型的继承自然有它不可取代的理由。所以具体问题得具体分析,当具体的使用场景没定下来时,就不存在最好的方法。

个人见解,能帮助大家更加理解继承一点就最好,如果有什么不对的,请多多指教!

文章来自:http://1ke.co/course/393

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/91511.html

相关文章

  • [JavaScript 随笔] 关于 this 你必须知道这几点

    摘要:关于中的坑大家都踩过。那这里的和是严格相等的。这里介绍的是通过创建对象时的。提示一下,数组对象的函数本身就是有这个功能的,也就是说可以达到要求。事件有两种记法,一个是也是类似,那么在中出现的表示触发该事件的元素,也就是。 TL;DR: this 指向调用该方法的对象,只有函数执行时,this 才有定义。 关于 JavaScript 中 this 的坑大家都踩过。像本文开头的这句话,道理...

    邹强 评论0 收藏0
  • JS对象随笔

    摘要:原型对象对象的原型对象实质上是对象的构造函数的原型对象。构造函数所有的对象都是通过构造函数实例化出来的。即一个对象,如果沿着原型链找下去,最终都会找到构造函数原型对象相互之间纠缠不休,你中有我,我中有你。 JS中的对象 JS中对象(若无特殊说明,本文中的对象都为对象实例,即使是空对象实例)可谓是一个核心的概念,纵观整个JS的数据结构如String、Number、Array、Boolea...

    Lin_YT 评论0 收藏0
  • 前端文档收集

    摘要:系列种优化页面加载速度的方法随笔分类中个最重要的技术点常用整理网页性能管理详解离线缓存简介系列编写高性能有趣的原生数组函数数据访问性能优化方案实现的大排序算法一怪对象常用方法函数收集数组的操作面向对象和原型继承中关键词的优雅解释浅谈系列 H5系列 10种优化页面加载速度的方法 随笔分类 - HTML5 HTML5中40个最重要的技术点 常用meta整理 网页性能管理详解 HTML5 ...

    jsbintask 评论0 收藏0
  • 前端文档收集

    摘要:系列种优化页面加载速度的方法随笔分类中个最重要的技术点常用整理网页性能管理详解离线缓存简介系列编写高性能有趣的原生数组函数数据访问性能优化方案实现的大排序算法一怪对象常用方法函数收集数组的操作面向对象和原型继承中关键词的优雅解释浅谈系列 H5系列 10种优化页面加载速度的方法 随笔分类 - HTML5 HTML5中40个最重要的技术点 常用meta整理 网页性能管理详解 HTML5 ...

    muddyway 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<