JS中常用的继承方案

在前端开发中,我们离不开面向过程和面向对象这两种开发模式。面向过程是指分析出实现需求所需要的步骤,通过函数一步一步地实现这些步骤,接着依次调用。面向对象是指将整个需求按功能、特性划分,将这些存有共性的部分封装成对象,创建了对象不是为了完成某一个步骤,而是描述某个事物在解决问题的步骤中的行为。对于复杂的项目,我们经常会用到面向对象的方式来组织我们的代码。前人早已给我们总结了几种常用的继承方式,熟悉这几种继承方式,对于我们的日常开发会有很大的裨益。

在 ES5 中,JS 没有 class 类,JS 中的继承是通过原型的方式来实现的。每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都会有一个原型对象的指针。先来看一个简单的示例图。


所有的引用类型都继承自 Object.prototype,而且是通过原型链实现的。这些对象都会有 Object 具有的一些默认方法。我们在浏览器控制台里先输入“Object”,可以发现 Object 的类型是 function,然后再输入“Object.prototype”,可以发现这个一个对象,而且里面就放在我们平时常用的一些方法,比如 hasOwnProperty、propertyIsEnumerable、toLocaleString、toString 等。既然 Object.prototype 也是一个对象,那它的原型是什么呢?我们再输入“Object.prototype.proto”,则返回为 null。由此可见,所有对象的原型链最顶层是 null。

高能预警
“__proto__”这个属性已从 Web 标准中删除,虽然一些浏览器目前仍然支持它,但也许会在未来的某个时间停止支持,请尽量不要使用该特性。为了更好的支持,建议只使用 Object.getPrototypeOf()。 而且“__proto__”这个属性的兼容性方面 IE 只有到了 11 的版本才支持。而 getPrototypeOf 的方法在 IE9 就已经支持了。

通过现代浏览器的操作属性的便利性,可以改变一个对象的 [[Prototype]] 属性, 这种行为在每一个 JavaScript 引擎和浏览器中都是一个非常慢且影响性能的操作,使用这种方式来改变和继承属性是对性能影响非常严重的,并且性能消耗的时间也不是简单的花费在 obj.__proto__ = ... 语句上, 它还会影响到所有继承来自该 [[Prototype]] 的对象,如果你关心性能,你就不应该在一个对象中修改它的 [[Prototype]]。相反, 创建一个新的且可以继承 [[Prototype]] 的对象,推荐使用 Object.create()。

原型链继承

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.getName = function () {
  return this.name;
};
Person.prototype.getAge = function () {
  return this.age;
};

function Student() {}

Student.prototype = new Person("junjun", 20);

Student.prototype.study = function () {
  return this.name + " is studying";
};

var student1 = new Student();
var student2 = new Student();

console.log(student1.getName()); //junjun
console.log(student1.study()); //junjun is studying
console.log(student2.getName()); //junjun
console.log(student2.study()); //junjun is studying

Student.prototype.name = "lili";
console.log(student1.getName()); //lili
console.log(student1.study()); //lili is studying
console.log(student2.getName()); //lili
console.log(student2.study()); //lili is studying

student1.name = "weihua";
console.log(student1.getName()); //weihua
console.log(student1.study()); //weihua is studying
console.log(student2.getName()); //lili
console.log(student2.study()); //lili is studying

console.log(Student.prototype.constructor === Student); //false
console.log(Student.prototype.constructor === Person); //true

console.log(student1 instanceof Object); //true
console.log(student1 instanceof Person); //true
console.log(student1 instanceof Student); //true

console.log(Object.prototype.isPrototypeOf(student1)); //true
console.log(Person.prototype.isPrototypeOf(student1)); //true
console.log(Student.prototype.isPrototypeOf(student1)); //true

要点:子类的原型等于父类的实例。
特点:实现简单;可继承构造函数的属性和方法,也可继承原型的属性和方法。
缺点:子类实例共享父类属性,父类属性修改后会影响所有实例;在创建子类的实例时,不能向父类的构造函数中传递参数;在给子类原型添加新的属性和方法要在继承后定义。

借用构造函数

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.getName = function () {
  return this.name;
};
Person.prototype.getAge = function () {
  return this.age;
};

function Student(name, age) {
  Person.call(this, name, age);
  this.job = "study";
}

Student.prototype.study = function () {
  return this.name + " is studying";
};

var student1 = new Student("lili", 19);
var student2 = new Student("huahua", 20);

console.log(student1.getName()); //Uncaught TypeError: student1.getName is not a function
console.log(student1.study()); //lili is studying
console.log(student2.getName()); //Uncaught TypeError: student1.getName is not a function
console.log(student2.study()); //huahua is studying

console.log(Student.prototype.constructor === Student); //true
console.log(Student.prototype.constructor === Person); //false

console.log(student1 instanceof Object); //true
console.log(student1 instanceof Person); //false
console.log(student1 instanceof Student); //true

console.log(Object.prototype.isPrototypeOf(student1)); //true
console.log(Person.prototype.isPrototypeOf(student1)); //false
console.log(Student.prototype.isPrototypeOf(student1)); //true

要点:父类的构造函数在子类构造函数中执行。
特点:可以向父类传参,且不会有原型属性共享的问题;可以继承多个构造函数属性
缺点:只能继承父类的实例属性和方法,不能继承原型属性和方法;每个新实例都有父类构造函数的副本,臃肿

组合继承

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.getName = function () {
  return this.name;
};
Person.prototype.getAge = function () {
  return this.age;
};

function Student(name, age) {
  Person.call(this, name, age);
  this.job = "study";
}

Student.prototype = new Person();
Student.prototype.constructor = Student;

Student.prototype.study = function () {
  return this.name + " is studying";
};

var student1 = new Student("lili", 19);
var student2 = new Student("huahua", 20);

console.log(student1.getName()); //lili
console.log(student1.study()); //lili is studying
console.log(student2.getName()); //huahua
console.log(student2.study()); //huahua is studying

console.log(Student.prototype.constructor === Student); //true
console.log(Student.prototype.constructor === Person); //false

console.log(student1 instanceof Object); //true
console.log(student1 instanceof Person); //true
console.log(student1 instanceof Student); //true

console.log(Object.prototype.isPrototypeOf(student1)); //true
console.log(Person.prototype.isPrototypeOf(student1)); //true
console.log(Student.prototype.isPrototypeOf(student1)); //true

要点:父类的构造函数在子类构造函数中执行。并将子类的原型赋值为父类的实例,之后将该实例的 constructor 指向子类构造函数。使用原型链实现对原型方法的继承,而通过借用构造函数来实现对实例属性的继承。
特点: 可以继承父类实例和原型中的属性和方法。
缺点: 在使用子类创建实例对象时,其原型中会存在两份相同的属性/方法。会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。

原型式继承

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.getName = function () {
  return this.name;
};
Person.prototype.getAge = function () {
  return this.age;
};

function CreateObj(obj, name, age) {
  function F() {
    this.name = name;
    this.age = age;
  }
  F.prototype = obj;
  F.prototype.study = function () {
    return this.name + " is studying";
  };
  return new F();
}

var person = new Person();
var student1 = CreateObj(person, "huahua", 16);
var student2 = CreateObj(person, "weiwei", 14);

console.log(student1.getName()); //huahua
console.log(student1.study()); //huahua is studying
console.log(student2.getName()); //weiwei
console.log(student2.study()); //weiwei is studying

console.log(student1.__proto__); //Person 实例
console.log(student1.__proto__.constructor === Person); //true

console.log(student1 instanceof Object); //true
console.log(student1 instanceof Person); //true

要点:用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象。object.create()就是这个原理。
特点:类似于复制一个对象,用函数来包装。
缺点:原型上的属性和方法都是共享的,所有后面对原型上的父类实例修改会影响所有实例;

寄生式继承

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.getName = function () {
  return this.name;
};
Person.prototype.getAge = function () {
  return this.age;
};

function Student(name, age) {
  Person.call(this, name, age);
  this.job = "study";
}

Student.prototype = Object.create(Person.prototype);
Student.prototype.study = function () {
  return this.name + " is studying";
};

var student1 = new Student("lili", 19);
var student2 = new Student("huahua", 20);

console.log(student1.getName()); //lili
console.log(student1.study()); //lili is studying
console.log(student2.getName()); //huahua
console.log(student2.study()); //huahua is studying

console.log(Student.prototype.constructor === Student); //false
console.log(Student.prototype.constructor === Person); //true

console.log(student1 instanceof Object); //true
console.log(student1 instanceof Person); //true
console.log(student1 instanceof Student); //true

console.log(Object.prototype.isPrototypeOf(student1)); //true
console.log(Person.prototype.isPrototypeOf(student1)); //true
console.log(Student.prototype.isPrototypeOf(student1)); //true

要点:主要思想就是不需要为了指定子类型的原型而去调用超类型的构造函数,我们需要的无非是超类型的原型副本。这样就会断绝父类修改对子类的影响。
特点:父类的修改不会影响到子类的实例。且能共享父类的方法。

ES6 中的继承

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  getName() {
    return this.name;
  }
  getAge() {
    return this.age;
  }
}

class Student extends Person {
  constructor(name, age, job) {
    super(name, age);
    this.job = job;
  }

  study() {
    return this.name + " is studying";
  }
}

var student1 = new Student("lili", 19);
var student2 = new Student("huahua", 20);

console.log(student1.getName()); //lili
console.log(student1.study()); //lili is studying
console.log(student2.getName()); //huahua
console.log(student2.study()); //huahua is studying

console.log(Student.prototype.constructor === Student); //true
console.log(Student.prototype.constructor === Person); //false

console.log(student1 instanceof Object); //true
console.log(student1 instanceof Person); //true
console.log(student1 instanceof Student); //true

console.log(Object.prototype.isPrototypeOf(student1)); //true
console.log(Person.prototype.isPrototypeOf(student1)); //true
console.log(Student.prototype.isPrototypeOf(student1)); //true

特点:除了子类原型的构造函数不同,其他和寄生继承实现的效果一致

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352