JavaScript 是一种灵活的语言,兼容并包含面向对象风格、函数式风格等编程风格。我们知道面向对象风格有三大特性和六大原则,三大特性是封装、继承、多态,六大原则是单一职责原则(SRP)、开放封闭原则(OCP)、里氏替换原则(LSP)、依赖倒置原则(DIP)、接口分离原则(ISP)、最少知识原则(LKP)。

JavaScript 并不是强面向对象语言,因此它的灵活性决定了并不是所有面向对象的特征都适合 JavaScript 开发,本教程将会着重介绍三大特性中的 继承 和六大原则里的单一职责原则、开放封闭原则、最少知识原则。

本文将着重介绍一下继承相关内容,设计原则将会在后文予以介绍。

1. 原型对象链

JavaScript 内建的继承方法被称为原型对象链,又称为原型对象继承。对于一个对象,因为它继承了它的原型对象的属性,所以它可以访问到这些属性。同理,原型对象也是一个对象,它也有自己的原型对象,因此也可以继承它的原型对象的属性。

这就是原型继承链:对象继承其原型对象,而原型对象继承它的原型对象,以此类推。

2. 对象继承

使用对象字面量形式创建对象时,会隐式指定 Object.prototype为新对象的 [[Prototype]]。使用 Object.create()方式创建对象时,可以显式指定新对象的 [[Prototype]]。该方法接受两个参数:第一个参数为新对象的 [[Prototype]],第二个参数描述了新对象的属性,格式如在 Object.defineProperties()中使用的一样。

// 对象字面量形式,原型被隐式地设置为 Object.prototype
var rectangle = { sizeType: '四边形' }// Object.create() 创建,显示指定为 Object.prototype,其效果同上
var rectangle = Object.create(Object.prototype, {sizeType: {configurable: true,enumerable: true,value: '四边形',writable: true}
})

我们可以用这个方法来实现对象继承:

var rectangle = {sizeType: '四边形',getSize: function () {console.log(this.sizeType)}
};var square = Object.create(rectangle, {sizeType: { value: '正方形' }
});rectangle.getSize();
// "四边形"
square.getSize();
// "正方形"// 判断对象是否包含特定的自身(非继承)属性
console.log(rectangle.hasOwnProperty('getSize'));
// true// 测试一个对象是否存在于另一个对象的原型链上
console.log(rectangle.isPrototypeOf(square));
// trueconsole.log(square.hasOwnProperty('getSize'));
// false// 判断对象是否为数组、对象的元素或属性
console.log('getSize' in square);
// trueconsole.log(square.__proto__ === rectangle);
// true
console.log(square.__proto__.__proto__ === Object.prototype);
// true

对象 square继承自对象 rectangle,也就继承了 rectangle 的 sizeType属性和 getSize() 方法,又通过重写 sizeType 属性定义了一个自有属性,隐藏并替代了原型对象中的同名属性。所以 rectangle.getSize()输出四边形,而 square.getSize() 输出正方形。

在访问一个对象的时候,JavaScript 引擎会执行一个搜索过程,如果在对象实例上发现该属性,该属性值就会被使用,如果没有发现则搜索其原型对象 [[Prototype]],如果仍然没有发现,则继续搜索该原型对象的原型对象 [[Prototype]],直到继承链顶端,顶端通常是一个 Object.prototype,其 [[prototype]] 为 null。这就是原型链的查找过程。

可以通过 Object.create() 创建 [[Prototype]]为 null 的对象:var obj = Object.create(null)。对象 obj是一个没有原型链的对象,这意味着 toString() 和 valueOf等存在于 Object 原型上的方法同样不存在于该对象上,通常我们将这样创建出来的对象为纯净对象。

3. 原型链继承

JavaScript 中的对象继承是构造函数继承的基础,几乎所有的函数都有 prototype属性(通过Function.prototype.bind 方法构造出来的函数是个例外),它可以被替换和修改。

函数声明创建函数时,函数的 prototype 属性被自动设置为一个继承自 Object.prototype的对象,该对象有个constructor的自有属性 ,其值就是函数本身。

// 构造函数
function YourConstructor() { }// JavaScript 引擎在背后做的:
YourConstructor.prototype = Object.create(Object.prototype, {constructor: {configurable: true,enumerable: true,value: YourConstructor,writable: true}
});console.log(YourConstructor.prototype.__proto__ === Object.prototype)
// true

JavaScript 引擎帮你把构造函数的 prototype属性设置为一个继承自 Object.prototype 的对象,这意味着我们创建出来的构造函数都继承自 Object.prototype。由于 prototype可以被赋值和改写,所以通过改写它来改变原型链:

// 四边形
function Rectangle(length, width) {this.length = length;this.width = width;
};// 获取面积
Rectangle.prototype.getArea = function () {return this.length * this.width;
};// 获取尺寸信息
Rectangle.prototype.getSize = function () {console.log(`Rectangle: ${this.length}x${this.width},面积: ${this.getArea()}`)
};// 正方形
function Square(size) {this.length = size;this.width = size;
};Square.prototype = new Rectangle();
// 原本为 Rectangle,重置回 Square 构造函数
Square.prototype.constructor = Square;Square.prototype.getSize = function () {console.log(`Square: ${this.length}x${this.width},面积: ${this.getArea()}`)
};var rect = new Rectangle(5, 10);
var squa = new Square(6);rect.getSize();
// Rectangle: 5x10,面积: 50
squa.getSize();
// Square: 6x6,面积: 36

为什么使用 Square.prototype = new Rectangle() 而不用 Square.prototype = Rectangle.prototype呢。这是因为后者使得两个构造函数的 prototype指向了同一个对象,当修改其中一个函数的 prototype 时,另一个函数也会受影响。

所以 Square 构造函数的 prototype 属性被改写为了 Rectagle 的一个实例。

但是仍然有问题。当一个属性只存在于构造函数的 prototype上,而构造函数本身没有时,该属性会在构造函数的所有实例间共享,其中一个实例修改了该属性,其他所有实例都会受影响:

// 四边形
function Rectangle(sizes) {this.sizes = sizes
};// 正方形
function Square() { };Square.prototype = new Rectangle([1, 2]);var squa1 = new Square();
// sizes: [1, 2]squa1.sizes.push(3);
// 在 squa1 中修改了 sizesconsole.log(squa1.sizes);
// sizes: [1, 2, 3]var squa2 = new Square();
console.log(squa2.sizes);
// sizes: [1, 2, 3]

4. 构造函数窃取

构造函数窃取又称构造函数借用、经典继承。这种技术的基本思想相当简单,即在子类型构造函数的内部调用父类构造函数。

function getArea() {return this.length * this.width
};// 四边形
function Rectangle(length, width) {this.length = length;this.width = width;
};// 获取面积
Rectangle.prototype.getArea = getArea;// 获取尺寸信息
Rectangle.prototype.getSize = function () {console.log(`Rectangle: ${this.length}x${this.width},面积: ${this.getArea()}`)
};// 正方形
function Square(size) {Rectangle.call(this, size, size);this.getArea = getArea;this.getSize = function () {console.log(`Square: ${this.length}x${this.width},面积: ${this.getArea()}`)}
};var rect = new Rectangle(5, 10);
var squa = new Square(6);rect.getSize();
// Rectangle: 5x10,面积: 50
squa.getSize();
// Square: 6x6,面积: 36

这样的实现避免了引用类型的属性被所有实例共享的问题,在父类实例创建时还可以自定义地传参,缺点是方法都是在构造函数中定义,每次创建实例都会重新赋值一遍方法,即使方法的引用是一致的。

这种方式通过构造函数窃取来设置属性,模仿了那些基于类的语言的类继承,所以这通常被称为伪类继承或经典继承。

5. 组合继承

组合继承又称伪经典继承,指的是将原型链和借用构造函数的技术组合发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。

// 四边形
function Rectangle(length, width) {this.length = length;this.width = width;this.color = 'red';
};// 获取面积
Rectangle.prototype.getArea = function () {return this.length * this.width
};// 获取尺寸信息
Rectangle.prototype.getSize = function () {console.log(`Rectangle: ${this.length}x${this.width},面积: ${this.getArea()}`)
};//  正方形
function Square(size) {// 第一次调用 Rectangle 函数Rectangle.call(this, size, size)this.color = 'blue'
};// 第二次调用 Rectangle 函数
Square.prototype = new Rectangle();
Square.prototype.constructor = Square;Square.prototype.getSize = function () {console.log(`Square: ${this.length}x${this.width},面积: ${this.getArea()}`)
};var rect = new Rectangle(5, 10);
var squa = new Square(6);rect.getSize();
// Rectangle: 5x10,面积: 50
squa.getSize();
// Square: 6x6,面积: 36

组合继承是 JavaScript 中最常用的继承模式,但是父类构造函数被调用了两次。

6. 寄生组合式继承

// 四边形
function Rectangle(length, width) {this.length = length;this.width = width;this.color = 'red';
};
// 获取面积
Rectangle.prototype.getArea = function () {return this.length * this.width;
};
// 获取尺寸信息
Rectangle.prototype.getSize = function () {console.log(`Rectangle: ${this.length}x${this.width},面积: ${this.getArea()}`);
};
// 正方形
function Square(size) {// 第一次调用 Rectangle 函数Rectangle.call(this, size, size)  this.color = 'blue'
};// 继承方法
function inheritPrototype(sub, sup) {var prototype = Object.create(sup.prototype);prototype.constructor = sub;sub.prototype = prototype;
};
// 实现继承
inheritPrototype(Square, Rectangle);Square.prototype.getSize = function () {console.log(`Square: ${this.length}x${this.width},面积: ${this.getArea()}`);
};var rect = new Rectangle(5, 10);
var squa = new Square(6);rect.getSize();
// Rectangle: 5x10,面积: 50
squa.getSize();
// Square: 6x6,面积: 36

这种方式的高效率体现它只调用了一次父类构造函数,并且因此避免了在 Rectangle.prototype上面创建不必要的、多余的属性。与此同时,原型链还能保持不变。因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

不过这种实现有些麻烦,推介使用 组合继承 和下面的 ES6 方式实现继承。

7. ES6 的 extends 方式实现继承

ES6 中引入了 class关键字,class 之间可以通过 extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰、方便和语义化的多。

// 四边形
class Rectangle {constructor(length, width) {this.length = length;this.width = width;this.color = 'red';};// 获取面积getArea() {return this.length * this.width};// 获取尺寸信息getSize() {console.log(`Rectangle: ${this.length}x${this.width},面积: ${this.getArea()}`)}
};// 正方形
class Square extends Rectangle {constructor(size) {super(size, size);this.color = 'blue';};getSize() {console.log(`Square: ${this.length}x${this.width},面积: ${this.getArea()}`)}
};var rect = new Rectangle(5, 10);
var squa = new Square(6);rect.getSize();
// Rectangle: 5x10,面积: 50squa.getSize();
// Square: 6x6,面积: 36

然而并不是所有浏览器都支持 class/extends 关键词,不过我们可以引入 Babel 来进行转译。class 语法实际上也是之前语法的语法糖,用户可以把上面的代码放到 Babel 的在线编译中看看,编译出来是什么样子。

JavaScript 设计模式学习第五篇-继承与原型链相关推荐

  1. 一篇JavaScript技术栈带你了解继承和原型链

    作者 | Jeskson 来源 | 达达前端小酒馆 1 在学习JavaScript中,我们知道它是一种灵活的语言,具有面向对象,函数式风格的编程模式,面向对象具有两点要记住,三大特性,六大原则. 那么 ...

  2. JavaScript面向对象——深入理解默认的继承方式原型链

    描述: 正如我们所了解,JavaScript中的每个函数中都有一个指向某一对象的prototype属性.该函数被new操作符调用时会创建并返回一个对象,并且该对象中会有一个指向其原型对象的秘密链接,通 ...

  3. JavaScript学习(五十九)—原型、原型链、闭包以及闭包的不足

    JavaScript学习(五十九)-原型.原型链.闭包以及闭包的不足 一.什么是闭包? 所谓闭包就是指被定义在其他函数内部的函数. 闭包函数可以访问它所在的函数的所有变量. 文字太抽象了,画图解释一下 ...

  4. JavaScript学习(五十八)—作用域链

    JavaScript学习(五十八)-作用域链 一.作用域链 在每个作用域中都有一个对象,这个对象被称为变量对象. 变量对象的作用就是用来管理该作用域下面定义的变量和函数的,也就是在该作用域下面定义的变 ...

  5. JavaScript之继承(原型链)

    JavaScript之继承(原型链) 我们知道继承是oo语言中不可缺少的一部分,对于JavaScript也是如此.一般的继承有两种方式:其一,接口继承,只继承方法的签名:其二,实现继承,继承实际的方法 ...

  6. JS中对象的四种继承方式:class继承、原型链继承、构造函数继承、组合继承(构造函数和原型链继承的结合)

    前言 才发现之前没有对JavaScript中的继承做过总结,不过看得到是不少,接下来就对这几种继承方式做一下总结. class继承 class继承是ES6引入的标准的继承方式. ES6引入了class ...

  7. ES5常用的组合继承及原型链理解

    ES5常用的组合继承及原型链理解 <!DOCTYPE html> <html lang="en"><head><meta charset= ...

  8. 【JS继承】JS继承之原型链继承

    自我介绍:大家好,我是吉帅振的网络日志:微信公众号:吉帅振的网络日志:前端开发工程师,工作4年,去过上海.北京,经历创业公司,进过大厂,现在郑州敲代码. JS继承专栏 1[JS继承]什么是JS继承? ...

  9. 【论文笔记】AAAI2022多智能体强化学习论文五篇

    文章目录 引子 Anytime Multi-Agent Path Finding via Machine Learning-Guided Large Neighborhood Search MAPF- ...

最新文章

  1. golang源码分析:编译过程词法解析的流程
  2. 车联网系统会不会只是智能手机系统的翻版?
  3. C++~回溯+贪心法解决01背包问题
  4. 计算机设计复合材料,两种复合材料几何建模算法-计算机辅助设计与图形学学报.PDF...
  5. python多线程坑_python多线程的坑
  6. BZOJ3239 Discrete Logging
  7. 卖shell看站什么意思_粤语俚语卖咸鸭蛋是什么意思?
  8. aix ip别名配置
  9. Javascript第四章参数和返回值基本用法第二课
  10. Idea 进行断点调试的 快捷键
  11. 程序员凌晨闲暇无聊时干什么
  12. vantfieldlabel样式修改_Vant Field 输入框
  13. Go异常处理——defer、panic、recover
  14. 今天过节,摔杯,逼宫,吃瓜吧?
  15. [深入研究4G/5G/6G专题-57]: L3信令控制-6-什么是无线承载DRB Profile
  16. sap增加税码注意事项,进项税调整SAP相应调整
  17. ACM暑假集训总结(2014年夏)
  18. 路由器为何会有特殊的默认路由(静态路由的一种特殊形式------默认路由)
  19. EasyExcel增加下拉选择框
  20. C++实现字符串的部分复制

热门文章

  1. php开源文档共享,几款常见的PHP开源文档管理系统介绍_PHP教程
  2. cisco路由器升级rom版本
  3. 人无远虑必有近忧,90后的我如何姑娘熬成婆
  4. 产品名称:iWX JAVA微信管理平台源码-微友1314
  5. java程序内存占用过高问题排查
  6. 《Effective Morden C++》Item 8: Prefer nullptr to 0 and NULL.
  7. 陀螺研究院 | 产业区块链发展周报(11.14—11.20)
  8. 【钢铁侠3】【高清1280版HD-RMVB.英语中字】【2013最新美国票房科幻动作大片】...
  9. 毛利率、净利率和成本利润率的区别是什么 ?
  10. 解析:外部网页内如何一键复制微信号添加微信好友