JavaScript 原型和继承
修改时间: July 25, 2022 11:42 AM
摘要: 原型链
标签: 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
实例对象被构造(**
new
)了之后能够自动引用(__proto__
)其构造函数的原型对象(**prototype
),同时还可以设置自己的属性或方法,如果与原型对象有同名的属性或方法,则会覆盖原型对象上的属性或方法,优先读取自身的属性或方法。
狗.prototype
大白.__proto__
console.log('大白是狗吗 = ', 大白.__proto__ === 狗.prototype);
// 大白是狗吗 = true
遵循的公式:
被构造的对象.__proto__ === 构造函数.prototype
每个实例对象和原型对象,都有构造器属性 constructor
,它们都指向了自己的构造函数。
狗 === 狗.prototype.constructor; // true
Array === Array.prototype.constructor // true
Object === Object.prototype.contructor // true
所以点开 狗.prototype.contructor.prototype
会进入无限循环
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
遵循 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
继承
原型继承
这个例子使用的是原型继承,关键是 Man
没有使用默认的原型,而是将其替换成了一个新的对象,这个对象是 Person
的实例。这样 Man
的实例不仅能从 Person
的实例中继承属性和方法,而且还与 Person
的原型挂了钩。于是 jack
(通过 [[Prototype]]
即私有有属性 __proto__
),而 Man.prototype
(作为 Person
的实例有通过内部的 [[Prototype]]
)指向 Person.prototype
。
在读取实例上的属性时,首先灰在实例上搜索这个属性,如果没找到则会继续手术实例的原型,在通过原型链实现继承之后,搜索就可以继续向上搜索原型的原型。
原型继承的弊端
- 原型中包含引用值的时候,原型中的引用值会在所有实例共享,一个实例修改引用值时其他实例也会引用修改后的值。
- 子类型在实例化不能给父类型的构造函数传入参数。
盗用构造函数
为了解决原型包含引用值导致的继承问题,而发明的盗用构造函数的继承技巧。
即在子类构造函数中调用父类构造函数。通过 call()
和 apply()
修改父构造函数的上下文 this
指向当前函数,同时也解决了引用值共享问题,即每个实例的引用值各自创建一份不再共享。
盗用构造函数的弊端
- 必须在构造函数中定义属性和方法。
- 子类不能访问父类原型上定义的属性和方法。
- 无法使用
instanceof
进行类型判断
组合继承
组合继承将原型继承和盗用构造函数继承两者的优点集合起来实现的继承方式。
- 实现原型上的属性和方法共享
- 引用值在每个实例间不共享
- 可以使用
intanceof
进行类型判断
组合继承的弊端
- 父类构造函数会调用至少 2 次,即原型继承调用 1 次,子类实例化调用 1 此。
原型式继承
2006 年,Douglas Crockford 写了一篇文章:《JavaScript 中的原型式继承》(“Prototypal Inheritance inJavaScript”)。这篇文章介绍了一种不涉及严格意义上构造函数的继承方法。他的出发点是即使不自定义类型也可以通过原型实现对象之间的信息共享。文章最终给出了一个函数:
function object(o){
function F() {}
F.prototype = o
return new F()
}
E
原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。但要记住,
属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的。
寄生式继承
与原型式类似的一种继承方式,即创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。
寄生式继承同样适合主要关注对象,而不在乎类型和构造函数的场景。object()
函数不是寄生式
继承所必需的,任何返回新对象的函数都可以在这里使用。
寄生式继承弊端
通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似。
寄生组合式继承
组合继承存在父类构造函数始终会被调用 2 次:一次实在创建子类原型时调用,另一次时在子类构造函数中调用。
在这个函数内部,第一步是创建父类原型的一个副本。然后,给返回的 prototype
对象设置 constructor
属性,解决由于重写原型导致默认 constructor
丢失的问题。最后将新创建的对象赋值给子类型的原型。
比组合式继承效率更高,原型链仍然保持不变,因此 instanceof
操作符和 isPrototypeOf()
方法正常有效。寄生式组合继承可以算是引用类型继承的最佳模式。
Babel 转译 class extends
就是使用的寄生组合继承。
Babel · The compiler for next generation JavaScript
参考链接
誓死讲清 prototype 、 proto 、 原型链与继承
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于