JavaScript 继承

原文链接:http://blog.percymong.com/201...

有些知识当时实在看不懂的话,可以先暂且放下,留在以后再看也许就能看懂了。

几个月前,抱着《JavaScript 高级程序设计(第三版)》,啃完创建对象,就开始啃起了继承,然而啃完原型链就实在是看不下去了,脑子越来越乱,然后就把它扔一边了,继续看后面的。现在利用这个暑假搞懂了这个继承,就把笔记整理一下啦。

原型链(Prototype Chaining)

先看一篇文章,文章作者讲的非常不错,并且还配高清套图哦。lol...

链接:[学习笔记] 小角度看JS原型链

从原文中小摘几句

  • 构造函数通过 prototype 属性访问原型对象

  • 实例对象通过 [[prototype]] 内部属性访问原型对象,浏览器实现了 proto 属性用于实例对象访问原型对象

  • 一切对象都是Object的实例,一切函数都是Function的实例

  • Object 是构造函数,既然是函数,那么就是Function的实例对象;Function是构造函数,但Function.prototype是对象,既然是对象,那么就是Object的实例对象

确定原型与实例的关系

有两种方法来检测原型与实例的关系:

  • instanceof:判断该对象是否为另一个对象的实例

instanceof 内部运算机理如下:

function instance_of(L, R) {  //L 表示左表达式,R 表示右表达式
  var O = R.prototype;    // 取 R 的显示原型
  L = L.__proto__;        // 取 L 的隐式原型
  while (true) { 
    if (L === null) 
      return false; 
    if (O === L)   // 这里重点:当 O 严格等于 L 时,返回 true 
      return true; 
    L = L.__proto__; 
  } 
}

上面代码摘自:JavaScript instanceof 运算符深入剖析

  • isPrototypeOf():测试一个对象是否存在于另一个对象的原型链上

这两个方法的不同点请参看:JavaScript isPrototypeOf vs instanceof usage

只利用原型链实现继承

缺点:1. 引用类型值的原型属性会被实例共享; 2. 在创建子类型的实例时,不能向超类型的构造函数中传递参数

function Father(){
  this.name = "father";
  this.friends = ['aaa','bbb'];
}
function Son(){
}
Son.prototype = new Father();
Son.prototype.constructor = Son;

var s1 = new Son();
var s2 = new Son();

console.log(s1.name);  // father
console.log(s2.name);  // father
s1.name = "son";
console.log(s1.name);  // son
console.log(s2.name);  // father

console.log(s1.friends);  // ["aaa", "bbb"]
console.log(s2.friends);  // ["aaa", "bbb"]
s1.friends.push('ccc','ddd');
console.log(s1.friends);  // ["aaa", "bbb", "ccc", "ddd"]
console.log(s2.friends);  // ["aaa", "bbb", "ccc", "ddd"]

只利用构造函数实现继承

实现方法:在子类型构造函数的内部调用超类型构造函数(使用 apply() 和 call() 方法)

  • 优点:解决了原型中引用类型属性的问题,并且子类可以向超类中传参

  • 缺点:子类实例无法访问父类(超类)原型中定义的方法,所以函数复用就无从谈起了。

function Father(name,friends){
  this.name = name;
  this.friends = friends;
}
Father.prototype.getName = function(){
  return this.name;
};

function Son(name){
  // 注意: 为了确保 Father 构造函数不会重写 Son 构造函数的属性,请将调用 Father 构造函数的代码放在 Son 中定义的属性的前面。
  Father.call(this,name,['aaa','bbb']);
  
  this.age = 22;
}

var s1 = new Son('son1');
var s2 = new Son('son2');

console.log(s1.name);  // son1
console.log(s2.name);  // son2

s1.friends.push('ccc','ddd');
console.log(s1.friends);  // ["aaa", "bbb", "ccc", "ddd"]
console.log(s2.friends);  // ["aaa", "bbb"]

// 子类实例无法访问父类原型中的方法
s1.getName(); // TypeError: s1.getName is not a function
s2.getName(); // TypeError: s2.getName is not a function

组合继承(Combination Inheritance)

实现方法:使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。

function Father(name,friends){
  this.name = name;
  this.friends = friends;
}
Father.prototype.money = "100k $";
Father.prototype.getName = function(){
  console.log(this.name);
};

function Son(name,age){
  // 继承父类的属性
  Father.call(this,name,['aaa','bbb']);
  
  this.age = age;
}

// 继承父类原型中的属性和方法
Son.prototype = new Father();
Son.prototype.constructor = Son;

Son.prototype.getAge = function(){
  console.log(this.age);
};

var s1 = new Son('son1',12);
s1.friends.push('ccc');
console.log(s1.friends);  // ["aaa", "bbb", "ccc"]
console.log(s1.money);    // 100k $
s1.getName();             // son1
s1.getAge();              // 12

var s2 = new Son('son2',24);
console.log(s2.friends);  // ["aaa", "bbb"]
console.log(s2.money);    // 100k $
s2.getName();             // son2
s2.getAge();              // 24

组合继承避免了单方面使用原型链或构造函数来实现继承的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式,但是它也是有缺陷的,组合继承的缺陷会在后面专门提到。

原型式继承(Prototypal Inheritance)

实现思路:借助原型基于已有的对象创建新对象,同时不必因此而创建自定义类型。

为了达到这个目的,引入了下面的函数(obj)

function obj(o){
  function F(){}
  F.prototype = o;
  return new F();
}
var person1 = {
  name: "percy",
  friends: ['aaa','bbb']
};
var person2 = obj(person1);
person2.name = "zyj";
person2.friends.push('ccc');

console.log(person1.name);    // percy
console.log(person2.name);    // zyj
console.log(person1.friends); // ["aaa", "bbb", "ccc"]
console.log(person2.friends); // ["aaa", "bbb", "ccc"]

ECMAScript 5 通过新增 Object.create() 方法规范化了原型式继承。在传入一个参数的情况下,Object.create() 和 obj() 方法的行为相同。

var person1 = {
  name: "percy",
  friends: ['aaa','bbb']
};
var person2 = Object.create(person1);
person2.name = "zyj";
person2.friends.push('ccc');

console.log(person1.name);    // percy
console.log(person2.name);    // zyj
console.log(person1.friends); // ["aaa", "bbb", "ccc"]
console.log(person2.friends); // ["aaa", "bbb", "ccc"]

在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,可以选择使用这种继承。

寄生式继承(Parasitic Inheritance)

寄生式继承是与原型式继承紧密相关的一种思路。

实现思路:创建一个仅仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再返回对象。

function obj(o){
  function F(){}
  F.prototype = o;
  return new F();
}
function createPerson(original){    // 封装继承过程
  var clone = obj(original);         // 创建对象
  clone.showSomething = function(){  // 增强对象
    console.log("Hello world!");
  };
  return clone;                      // 返回对象
}

var person = {
  name: "percy"
};
var person1 = createPerson(person);
console.log(person1.name);  // percy
person1.showSomething();    // Hello world!

寄生组合式继承(Parasitic Combination Inheritance)

先来说说我们前面的组合继承的缺陷。组合继承最大的问题就是无论什么情况下,都会调用两次父类的构造函数:一次是创建子类的原型的时候,另一次是在调用子类构造函数的时候,在子类构造函数内部又调用了父类的构造函数。

function Father(name,friends){
  this.name = name;
  this.friends = friends;
}
Father.prototype.money = "100k $";
Father.prototype.getName = function(){
  console.log(this.name);
};

function Son(name,age){
  // 继承父类的属性
  Father.call(this,name,['aaa','bbb']);    // 第二次调用 Father() , 实际是在 new Son() 时才会调用
  
  this.age = age;
}

// 继承父类原型中的属性和方法
Son.prototype = new Father();               // 第一次调用 Father()
Son.prototype.constructor = Son;

第一次调用使的子类的原型成了父类的一个实例,从而子类的原型得到了父类的实例属性;第二次调用会使得子类的实例也得到了父类的实例属性;而子类的实例属性默认会屏蔽掉子类原型中与其重名的属性。所以,经过这两次调用,子类原型中出现了多余的的属性,从而引进了寄生组合式继承来解决这个问题。

寄生组合式继承的背后思路是:不必为了指定子类的原型而调用父类的构造函数,我们所需要的无非就是父类原型的一个副本而已

本质上,就是使用寄生式继承来继承父类的原型,然后将结果返回给子类的原型。

function obj(o){
  function F(){}
  F.prototype = o;
  return new F();
}
function inheritPrototype(son,father){
  var prototype = obj(father.prototype);  // 创建对象
  prototype.constructor = son;            // 增强对象
  son.prototype = prototype;              // 返回对象
}
function Father(name,friends){
  this.name = name;
  this.friends = friends;
}
Father.prototype.money = "100k $";
Father.prototype.getName = function(){
  console.log(this.name);
};

function Son(name,age){
  // 继承父类的属性
  Father.call(this,name,['aaa','bbb']);
  
  this.age = age;
}

// 使用寄生式继承继承父类原型中的属性和方法
inheritPrototype(Son,Father);

Son.prototype.getAge = function(){
  console.log(this.age);
};

var s1 = new Son('son1',12);
s1.friends.push('ccc');
console.log(s1.friends);  // ["aaa", "bbb", "ccc"]
console.log(s1.money);    // 100k $
s1.getName();             // son1
s1.getAge();              // 12

var s2 = new Son('son2',24);
console.log(s2.friends);  // ["aaa", "bbb"]
console.log(s2.money);    // 100k $
s2.getName();             // son2
s2.getAge();              // 24

优点:使子类原型避免了继承父类中不必要的实例属性。

开发人员普遍认为寄生组合式继承是实现基于类型继承的最理想的继承方式。

最后

最后,强烈推荐两篇很硬的文章

摘第二篇文章的一张硬图过来:

clipboard.png

看完之后,秒懂原型链,有木有?

参考文章


percy507的编程之路
自2016年加入社区后,陆陆续续发布过一些文章,后面也自己折腾过个人博客(hexo+github)。但是自2018年...

主修前端,

962 声望
54 粉丝
0 条评论
推荐阅读
使用vite搭个中后台系统的脚手架
搭个中后台系统的脚手架仓库地址 [链接]搭建脚手架目的学习 vite、recoil 等新技术封装项目中常用的较复杂的组件学习一定的前端架构能力构建命令npm 与 yarn 对新版 husky(v7.0.1+)的配置方式不太相同,我们这...

percy5072阅读 3.2k

封面图
正则表达式实例
收集在业务中经常使用的正则表达式实例,方便以后进行查找,减少工作量。常用正则表达式实例1. 校验基本日期格式 {代码...} {代码...} 2. 校验密码强度密码的强度必须是包含大小写字母和数字的组合,不能使用特殊...

寒青56阅读 8.4k评论 11

JavaScript有用的代码片段和trick
平时工作过程中可以用到的实用代码集棉。判断对象否为空 {代码...} 浮点数取整 {代码...} 注意:前三种方法只适用于32个位整数,对于负数的处理上和Math.floor是不同的。 {代码...} 生成6位数字验证码 {代码...} ...

jenemy48阅读 6.9k评论 12

从零搭建 Node.js 企业级 Web 服务器(十五):总结与展望
总结截止到本章 “从零搭建 Node.js 企业级 Web 服务器” 主题共计 16 章内容就更新完毕了,回顾第零章曾写道:搭建一个 Node.js 企业级 Web 服务器并非难事,只是必须做好几个关键事项这几件必须做好的关键事项就...

乌柏木75阅读 7k评论 16

再也不学AJAX了!(二)使用AJAX ① XMLHttpRequest
「再也不学 AJAX 了」是一个以 AJAX 为主题的系列文章,希望读者通过阅读本系列文章,能够对 AJAX 技术有更加深入的认识和理解,从此能够再也不用专门学习 AJAX。本篇文章为该系列的第二篇,最近更新于 2023 年 1...

libinfs42阅读 6.8k评论 12

封面图
从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
分层规范从本章起,正式进入企业级 Web 服务器核心内容。通常,一块完整的业务逻辑是由视图层、控制层、服务层、模型层共同定义与实现的,如下图:从上至下,抽象层次逐渐加深。从下至上,业务细节逐渐清晰。视图...

乌柏木45阅读 8.5k评论 6

从零搭建 Node.js 企业级 Web 服务器(二):校验
校验就是对输入条件的约束,避免无效的输入引起异常。Web 系统的用户输入主要为编辑与提交各类表单,一方面校验要做在编辑表单字段与提交的时候,另一方面接收表单的接口也要做足校验行为,通过前后端共同控制输...

乌柏木35阅读 6.6k评论 10

主修前端,

962 声望
54 粉丝
宣传栏