Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

谈js继承 #5

Open
Open
Labels
@wython

Description

谈js继承

js没有提供面向对象编程的语法特性,也没有类的概念。在es6中可以使用class去定义类和继承,但是实际上却没有面向对象多态的这些特性。其底层依然是基于js的原型链去实现的。

js可以基于Object创建对象(如下),但是Object本质更像是键值对的哈希对象,所以基于Object字面量和简单的方式创建对象往往无法满足我们要求。

let o = new Object();
let p = { name: 'person' };

通过函数方式创建对象

通过函数方式也可以很好的创建对象,而且从代码复用性和封装性都比以上方式好。

function Animal(type, name) {
 this.type = type || 'animal';
 this.name = name || 'anonymity'
 this.sayName = function (){
 console.log(`${this.type}: ${this.name}`);
 }
}
// Animal('person'); 与普通函数无异
// new Animal('person'); 返回对象

通过new操作符,经历里一下步骤

  1. 函数创建一个新对象
  2. 将函数内部上下文赋予新对象
  3. 执行函数代码
  4. 返回新建对象

所以说,通过new方式,我们获得的是这个新创建的对象。因此,可以很方便的创造各种实例,如下:

const person = new Animal('person');
const dog = new Animal('dog');
person.sayName(); // 输出 person: anonymity
dog.sayName(); // 输出 dog: anonymity

但是其实很快就有新的问题,

person.sayName === dog.sayName
// 输出false

我们发现对于某些可以复用的代码,实际上重复执行了,说白了就是,sayName函数实际上只需要存在一份即可,但是实际运行过程中,不同实例都声明了不同的函数。这实际不是一个优雅的方式。所以引出下一个概念,原型链。

原型对象

希望读者先区分构造函数和实例两个不同的名词,首先,如果细心就会发现,对于元生对象如Object, Array之类的(本质依然是function)构造函数,都具有prototype属性。像我们自己定义的Animal函数,也具有一个prototype属性。
这个prototype属性指向的是原型对象的引用。每一个函数在创建过程中,会生成一个原型对象,同时,构造函数本身的prototype属性指向这个原型对象。细心点也会发现,通过new创建的实例person,dog具有__proto__属性。这是一个已废弃的属性,但是它的存在说明了实例上面也有指向原型对象的引用。不推荐用__proto__访问原型对象,那有什么办法可以访问实例的原型对象。可以通过Object.getPrototypeOf(person)方式获得实例的原型对象,实例的原型对象和构造函数的原型对象是不是一致的,答案是肯定的。

Object.getPrototypeOf(person) === Animal.prototype
// true

同时,原型对象上有constructor属性是指向构造函数本身的。

Animal.prototype.constructor === Animal
// true

所以可以总结为,当函数被创建时,会有一个prototype属性指向一个生成的原型对象, 并且原型对象具有指回构造函数的引用contructor属性。当通过构造函数创建实例时,实例也拥有指向原型对象的属性(即实例可以追溯原型对象)。

原型对象作用之共用公共属性

在别的语言里可以通过static声明公共的属性,js的原型对象有这样用处。因为实例访问属性的过程中,会遍历自身属性,如果没有会追溯原型链的属性。也就是说,实例可以访问到原型对象上的属性。同时所有实例共享同一个原型对象。

Object.getPrototypeOf(person) === Object.getPrototypeOf(dog);
// true

这也就达到一份代码可以多实例复用的要求。所以构造函数写成:

function Animal(type, name) {
 this.type = type || 'animal';
 this.name = name || 'anonymity'
}
Animal.prototype.sayName = function (){
 console.log(`${this.type}: ${this.name}`);
}
Animal.prototype.publicProper = 'p';

如果希望构造函数看上去封装性更好,可以优化

function Animal(type, name) {
 this.type = type || 'animal';
 this.name = name || 'anonymity'
 if (typeof this.sayName !== 'function') {
 Animal.prototype.sayName = function (){
 console.log(`${this.type}: ${this.name}`);
 }
 }
}

原型对象作用二之继承

讲了这么久,才到真正要讨论的内容,因为js真正属于面向对象特性的还是继承。有以上基础其实可以很简单的实现继承。最简单,最直接一般都是这样做

function Animal(type, name) {
 this.type = type || 'animal';
 this.name = name || 'anonymity'
 if (typeof this.sayName !== 'function') {
 Animal.prototype.sayName = function () {
 console.log(`${this.type}: ${this.name}`);
 }
 }
}
function Person(type, name) {
 this.name = name | 'anonymity';
}
Person.prototype = new Animal('person');

这样最简单的继承就实现了,但是这种继承方式存在缺点。

  1. 因为子类Person的原型是Animal的实例,所Person创建的实例也包含了Animal实例的属性,Person不同实例共用Animal的实例属性。是不是有办法,当Animal的实例属性继承下来是Person的实例属性。

  2. Animal构造函数是有参数的,这种方式因为是公用的,所以我们继承new Animal时候没有传参数name,因为我们默认Animal的name属性是需要覆盖的。明显这样写法灵活性不够。或者说,如果希望type, name属性依然是实例属性,实际上继承Animal构造函数的代码没有复用到。

  3. 子类新原型对象并没有constructor属性指回构造函数,与原生方式不同。

借用父类构造函数

可以通过在子类中调用父类构造函数,并且改变作用域,将实例属性赋值到子类的实例属性上

function Animal(type, name) {
 this.type = type || 'animal';
 this.name = name || 'anonymity'
 if (typeof this.sayName !== 'function') {
 Animal.prototype.sayName = function () {
 console.log(`${this.type}: ${this.name}`);
 }
 }
}
function Person(name) {
 this.job = null;
 Animal.call(this, 'person', name);
}
Person.prototype = new Animal();
Person.prototype.constructor = Person;
const p = new Person('peter');
p.sayName();
// Person peter

这样实现,大体上是已经很完美了, 但是依然有不足,一个是构造函数执行两次,所以原型属性上和实例属性上重复了。为了解决这个问题,我们最直观的想法时就是创建一个空对象并且,然后将原型上的内容复制到空对象上。实际上,我们一个可以创建一个空白构造函数,并且使其原型为父类原型,再将空白对象实例作为之类原型即可。用代码直观点:

function objectCreate(proto) {
 function F(){};
 F.prototype = proto;
 return new F();
}
// 继承代码改写为
function Animal(type, name) {
 this.type = type || 'animal';
 this.name = name || 'anonymity'
 if (typeof this.sayName !== 'function') {
 Animal.prototype.sayName = function () {
 console.log(`${this.type}: ${this.name}`);
 }
 }
}
function Person(name) {
 this.job = null;
 Animal.call(this, 'person', name);
}
Person.prototype = objectCreate(Animal.prototype);
Person.prototype.constructor = Person;
const p = new Person('peter');
p.sayName();
// Person peter

当然es5中已经提供了object.create()方法。

到此,继承的所有讨论就结束了,在es6,和ts已经这么普及的今天,讨论这一个老生常谈的问题,有必要吗?我觉得还是有必要的,第一,我觉得对于这样问题,并不是所有人都真正的理解其中的含义。第二,我觉得只要浏览器没有完全支持新语法,那么意味着我们最终跑得前端代码依然是很原始的代码,在这个角度去理解这些代码是有益的。最后,我个人也觉得,对于继承这个话题,在红皮书上有各种各样的定义,很多人太拘泥于它的命名,反而忽视了一些内容背后解决的问题,这些命名其实是从英文在通过翻译者翻译,并不是真正需要去理解的东西。

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

        AltStyle によって変換されたページ (->オリジナル) /