JavaScript 原型和继承

JavaScript 原型和继承

修改时间: July 25, 2022 11:42 AM
摘要: 原型链
标签: Javascript
类型: 原创

Untitled

原型

前置阅读

Javascript 继承机制的设计思想

面试题

var F = function () {} Object.prototype.a = function () {} Function.prototype.b = function () {} var f = new F() // 请问f有方法a 方法b吗
  • 答案

    // 1、F是构造函数,函数也是对象的一种,构造函数原型对象F.prototype的原型是Object.prototype console.log(F.prototype.__proto__ === Object.prototype) // 2、实例对象的[[Prototype]]指向的是构造函数的原型对象 console.log(f.__proto__ === F.prototype) // 3、通过原型链向上查找属性的规则,实例对象a属性最终会查找到Object.prototype.a console.log(f.a) // 4、实例对象和构造函数没有直接关联,和构造函数的原型对象有直接关联,实例对象的原型链没有Function.prototype console.log(f.b) // 5、构造函数也是普通函数,js引擎隐式调用 new Function() 创建函数 // var F = function () { } // 等价于 var F = new Function() console.log('F.__proto__ === Function.prototype', F.__proto__ === Function.prototype) // 6、F函数的原型链能查找到b方法 console.log(F.b)

构造函数是什么?

使用***new***关键字来调用的函数,称为构造函数;通俗来讲,构造函数就是用来构造(创建)一个实例对象的函数。

/* 狗的构造函数,用来描述狗这类动物的共有属性和技能,不是具体的一条狗 如果需要领养一条狗,使用new关键字创建 */ function 狗(品种 = "土狗", 名字 = "大黄", ) { this.名字 = 名字 this.品种 = 品种 this.眼睛 = 2 this.嘴 = 1 this.脚 = 4 this.特点 = "对人类忠诚" this.跑 = function () { console.log(`[${this.品种}-${this.名字}]又跑又跳~~`) } this.叫 = function () { console.log(`[${this.品种}-${this.名字}]汪...`) } this.toString = function () { return "🐕" } } // 你要领养一只狗,品种是微笑天使萨摩耶,起名大白 const 大白 = new 狗("萨摩耶", "大白") console.log('萨摩耶具备狗所有属性和方法', 大白) // 大白是狗,当然会叫,会跑 大白.叫(); 大白.跑(); // 大白是萨摩耶,还有新的才艺-微笑 大白.微笑 = function() { console.log('[大白]表演微笑才艺 ~~~///(^v^)\\\~~~') } 大白.微笑(); // 另一只狗,品种哈士奇,叫小灰 const 小灰 = new 狗('哈士奇', '小灰') 小灰.拆家 = function () { console.log('[小灰]把家里沙发撕烂了') } 小灰.拆家()

完整代码 https://jsitor.com/Nos--DI4z

Untitled

Untitled

实例对象被构造(**new )了之后能够自动引用( __proto__ )其构造函数的原型对象(**prototype ),同时还可以设置自己的属性或方法,如果与原型对象有同名的属性或方法,则会覆盖原型对象上的属性或方法,优先读取自身的属性或方法。

  • 狗.prototype

Untitled

  • 大白.__proto__

Untitled

Untitled

console.log('大白是狗吗 = ', 大白.__proto__ === 狗.prototype); // 大白是狗吗 = true

遵循的公式:被构造的对象.__proto__ === 构造函数.prototype

每个实例对象和原型对象,都有构造器属性 constructor ,它们都指向了自己的构造函数。

狗 === 狗.prototype.constructor; // true Array === Array.prototype.constructor // true Object === Object.prototype.contructor // true

所以点开 狗.prototype.contructor.prototype 会进入无限循环

Untitled

prototype 是什么?

为了实现实例对象的属性和方法共享,就给 function 设计了一个 prototype 的概念。简单地说,JavaScript 是基于原型的语言

对于使用过基于类的语言 (如 Java 或 C++) 的开发者们来说,JavaScript 实在是有些令人困惑 —— JavaScript 是动态的,本身不提供一个 class 的实现。即便是在 ES2015/ES6 中引入了 class 关键字,但那也只是语法糖,JavaScript 仍然是基于原型的。

当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象(object)都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象proto),层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。这种属性查找的方式被称为原型链(prototype chain)

简单地说,JavaScript 是基于原型的语言

几乎所有 JavaScript 中的对象都是位于原型链顶端的 [Object](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object) 的实例。

遵循 ECMAScript 标准,someObject.[[Prototype]] 符号是用于指向 someObject 的原型。从 ECMAScript 6 开始,[[Prototype]] 可以通过 [Object.getPrototypeOf()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/GetPrototypeOf) 和 [Object.setPrototypeOf()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf) 访问器来访问。这个等同于 JavaScript 的非标准但许多浏览器实现的属性 __proto__

__proto__是什么?

每个 JS 对象一定对应一个原型对象,并从原型对象继承属性和方法。—《JavaScript 权威指南》

[Object.prototype](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) 的 __proto__ 属性是一个访问器属性(一个 getter 函数和一个 setter 函数), 暴露了通过它访问的对象的内部 [[Prototype]] (一个对象或 [null](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/null))。

浏览器通过该__proto__属性实现原型,这就是我们将其引用的方式。这通常称为 dunder proto,是双下划线原型的缩写。请勿重新分配此属性或直接使用它。在 MDN 页面上,不鼓励使用__proto__,影响性能。

  • [Object.getPrototypeOf](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/GetPrototypeOf)/[Reflect.getPrototypeOf](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect/getPrototypeOf)
  • [Object.setPrototypeOf](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf)/[Reflect.setPrototypeOf](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect/setPrototypeOf)

原型的尽头是 null

Untitled

遵循 ECMAScript 标准,someObject.[[Prototype]] 符号是用于指向 someObject 的原型。从 ECMAScript 6 开始,[[Prototype]] 可以通过 [Object.getPrototypeOf()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/GetPrototypeOf) 和 [Object.setPrototypeOf()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf) 访问器来访问。这个等同于 JavaScript 的非标准但许多浏览器实现的属性 __proto__

但它不应该与构造函数 func 的 prototype 属性相混淆。被构造函数创建的实例对象的 [[Prototype]] 指向 func 的 prototype 属性。Object.prototype 属性表示 [Object](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object) 的原型对象。

prototype chain 原型链

了解了 prototype__proto__,原型链就比较好理解了。

当我们寻找对象的属性时,JavaScript 引擎将首先检查对象本身是否存在该属性。如果找不到,它将转到对象的原型并检查该对象。如果找到,它将使用该属性。

如果未找到,它将继续访问原型的原型,然后不断查找它的__proto__属性。因此,如果我们尝试从上方在 obj 对象上查找属性 someProperty,则引擎将首先检查对象本身。

找不到它,然后跳到它的 __proto__ 对象,该对象等于 Object.prototype。它也不会在那里找到它,并且看到下一个 __proto__ 为空时,它将返回未定义。

这称为原型链。通常将其描述为向下延伸的链,顶部为 null,底部使用我们使用的对象。

执行查询时,引擎将遍历整个链以查找该属性,并返回它找到的第一个属性,如果原型链中不存在该属性,则为 undefined

总结

  • __proto__ 是实例对象的一个属性,这个属性使得实例对象可以使用其共用的属性和方法。
  • 每当一个实例被构造,其 __proto__ 属性就会自动指向构造它的函数的原型对象。记住这个公式:被构造的对象.__proto__ === 构造函数.prototype
  • __proto__ 会一层层向上指,直到遇到 null

继承

原型继承

https://jsitor.com/cUg7Mtq8k

这个例子使用的是原型继承,关键是 Man 没有使用默认的原型,而是将其替换成了一个新的对象,这个对象是 Person 的实例。这样 Man 的实例不仅能从 Person 的实例中继承属性和方法,而且还与 Person 的原型挂了钩。于是 jack(通过 [[Prototype]] 即私有有属性 __proto__),而 Man.prototype(作为 Person 的实例有通过内部的 [[Prototype]])指向 Person.prototype

在读取实例上的属性时,首先灰在实例上搜索这个属性,如果没找到则会继续手术实例的原型,在通过原型链实现继承之后,搜索就可以继续向上搜索原型的原型。

Untitled

原型继承的弊端

  1. 原型中包含引用值的时候,原型中的引用值会在所有实例共享,一个实例修改引用值时其他实例也会引用修改后的值。
  2. 子类型在实例化不能给父类型的构造函数传入参数。

https://jsitor.com/R7k3O6T1y

盗用构造函数

为了解决原型包含引用值导致的继承问题,而发明的盗用构造函数的继承技巧。

即在子类构造函数中调用父类构造函数。通过 call()apply() 修改父构造函数的上下文 this 指向当前函数,同时也解决了引用值共享问题,即每个实例的引用值各自创建一份不再共享。

https://jsitor.com/YFSlh9f_h

盗用构造函数的弊端

  1. 必须在构造函数中定义属性和方法。
  2. 子类不能访问父类原型上定义的属性和方法。
  3. 无法使用 instanceof 进行类型判断

组合继承

组合继承将原型继承盗用构造函数继承两者的优点集合起来实现的继承方式。

  1. 实现原型上的属性和方法共享
  2. 引用值在每个实例间不共享
  3. 可以使用 intanceof 进行类型判断

https://jsitor.com/t0Ti0kkQ5

组合继承的弊端

  • 父类构造函数会调用至少 2 次,即原型继承调用 1 次,子类实例化调用 1 此。

原型式继承

2006 年,Douglas Crockford 写了一篇文章:《JavaScript 中的原型式继承》(“Prototypal Inheritance inJavaScript”)。这篇文章介绍了一种不涉及严格意义上构造函数的继承方法。他的出发点是即使不自定义类型也可以通过原型实现对象之间的信息共享。文章最终给出了一个函数:

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

E

原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。但要记住,
属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的。

寄生式继承

与原型式类似的一种继承方式,即创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。

寄生式继承同样适合主要关注对象,而不在乎类型和构造函数的场景。object() 函数不是寄生式
继承所必需的,任何返回新对象的函数都可以在这里使用。

https://jsitor.com/UUezCUq0k

寄生式继承弊端
通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似。

寄生组合式继承

组合继承存在父类构造函数始终会被调用 2 次:一次实在创建子类原型时调用,另一次时在子类构造函数中调用。

https://jsitor.com/LmsISiMwd

在这个函数内部,第一步是创建父类原型的一个副本。然后,给返回的 prototype 对象设置 constructor 属性,解决由于重写原型导致默认 constructor 丢失的问题。最后将新创建的对象赋值给子类型的原型。

比组合式继承效率更高,原型链仍然保持不变,因此 instanceof 操作符和 isPrototypeOf() 方法正常有效。寄生式组合继承可以算是引用类型继承的最佳模式。

Babel 转译 class extends 就是使用的寄生组合继承。

Babel · The compiler for next generation JavaScript

参考链接

继承与原型链 - JavaScript | MDN

JavaScript Prototype

2022 年在学一遍 JS 原型、原型链 - 掘金

JS 原型(prototype)和原型链完全攻略

Javascript Object Hierarchy

理解 JS 的 prototype

prototype VS proto

誓死讲清 prototype 、 proto 、 原型链与继承

学习乐园 · TypeScript--JavaScript 的超集

Babel · The compiler for next generation JavaScript

  • JavaScript

    JavaScript 一种动态类型、弱类型、基于原型的直译式脚本语言,内置支持类型。它的解释器被称为 JavaScript 引擎,为浏览器的一部分,广泛用于客户端的脚本语言,最早是在 HTML 网页上使用,用来给 HTML 网页增加动态功能。

    730 引用 • 1280 回帖 • 4 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...