- 如何理解原型,如何理解原型链?
- new 一个对象发生了什么?
- new 的实现
- 每一个函数 ( 类 ) 都有一个 prototype ( 原型 ) 属性,属性值是一个对象:这个对象中存储了当前类供实例调取使用的公有属性和方法
- 在 “浏览器默认” 给原型开辟的堆内存中有一个属性 constructor:存储的是当前类本身
- 每一个对象 ( 实例 ) 都有一个 __proto__ ( 原型链 ) 属性,这个属性指向当前实例所属类的原型 ( 不确定的类都指向 Object.prototype )
- 原型链是一种基于 __proto__ 向上查找的机制。当我们操作实例的某个属性或者方法的时候,首先找自己空间中私有的属性和方法
- 找到了则结束查找
- 没有找到,则基于 __proto__ 找自己所属类的 prototype,如果找到就用这个公有的。如果没有找到则基于原型上的 proto 继续向上查找,一直找到 Object.prototype 原型为止,如果没有说明操作的属性或者方法不存在
- JS 中每个对象都有 __ proto __ 属性,这个属性指向了原型。
- 对于一个 object 来说可以通过 __ proto __ 属性找到一个原型对象。
- 原型的 constructor 属性指向构造函数,构造函数又通过 prototype 指回原型,但是并不是所有函数都具有这个属性,Function.prototype.bind() 就没有这个属性。
- 为什么 obj 可以访问到 valueOf 函数,就是因为 obj 通过原型链找到了 valueOf 函数。
构造函数、原型与实例之间的关系
- 每创建一个函数,该函数就会自动带有一个 prototype 属性。这个属性是一个指针,指向了一个对象,我们称之为原型对象。
- 原型对象上默认有一个 constructor 属性,该属性也是一个指针指向与其相关联的构造函数。
- 通过调用构造函数产生的实例都有一个内部属性 __ proto __,指向了原型对象,所以实例能够访问原型对象上的所有属性和方法。
所以三者的关系是:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针 __ proto __。也就是说实例可以通过内部指针访问到原型对象,原型对象通过 constructor 指针找到构造函数。
如下示例:
function Dog(name) {
this.name = name
this.type = 'Dog'
}
Dog.prototype.speak = function() {
console.log('wangwangwang')
}
var doggie = new Dog('taidi')
doggie.speak() // wangwangwang
console.log(Dog.prototype.constructor === Dog) //true
console.log(doggie.__proto__.constructor === Dog) // true
doggie.prototype // undefined 只有函数对象才有 prototype 属性
如下图所示:

原型链
前面我们说到所有的实例都有一个内部指针 __ proto __ 指向它的原型对象,并且可以访问原型对象上的所有属性和方法。doggie 实例指向了 Dog 的原型对象,可以访问 Dog 原型对象上的所有属性和方法;如果 Dog 原型对象变成了某一个类的实例 some1,这个实例又会指向一个新的原型对象 Some1,那么此时 doggie 就能访问 some1 的实例属性和 Some1 原型对象上的所有属性和方法。同理新的原型对象 Some1 恰巧又是另外一个对象的实例 some2,这个实例 some2 又会指向新的原型对象 Some2,那么此时 doggie 就能访问 some2 的实例属性和 Some2 原型对象上的所有属性和方法。
- 通过原型链实现继承的方法代码如下:
// 定义一个 Animal 函数作为 Dog 的父类
function Animal() {
this.superType = 'Animal'
}
Animal.prototype.superSpeak = function() {
console.log(this.superType)
}
function Dog(name) {
this.name = name
this.type = 'Dog'
}
// 改变 Dog 的 prototype 指针指向 Animal 的实例
Dog.prototype = new Animal()
// 等同于下面注释部分
/*
var animal = new Animal()
Dog.prototype = animal
*/
Dog.prototype.speak = function () {
console.log(this.type)
}
var doggie = new Dog('taidi')
doggie.superSpeak() // Animal
console.log(doggie.__proto__.constructor === Animal) // true
以上代码,首先定义了一个 Animal 构造函数,通过new Animal()得到实例,会包含一个实例属性 superType 和一个原型属性 superSpeak。另外又定义了一个Dog构造函数。然后情况发生变化,代码中加粗那一行,将Dog的原型对象覆盖成了 animal 实例。当 doggie 去访问superSpeak属性时,js会先在doggie的实例属性中查找,发现找不到,然后,js就会去doggie 的原型对象上去找,doggie的原型对象已经被我们改成了一个animal实例,那就是去animal实例上去找。先找animal的实例属性,发现还是没有 superSpeack, 最后去 animal 的原型对象上去找,诶,这才找到。
流程如下图所示:

这就说明我们可以通过原型链的方式,实现 Dog 继承 Animal 的所有属性和方法。
总结:当重写了 Dog.prototype 指向的原型对象后,实例的内部指针也发生了改变,指向了新的原型对象,然后就能实现类与类之间的继承了。
MDN详解-继承与原型链请戳这里
查找性能
- 在原型链上查找属性比较耗时,对性能有副作用,这在性能要求苛刻的情况下很重要。另外,试图访问不存在的属性时会遍历整个原型链。
- 遍历对象的属性时,原型链上的每个可枚举属性都会被枚举出来。要检查对象是否具有自己定义的属性,而不是其原型链上的某个属性,则必须使用所有对象从
Object.prototype
继承的 hasOwnProperty 方法。hasOwnProperty
是 JavaScript 中唯一一个处理属性并且不会遍历原型链的方法。
new 一个对象发生了什么?
- 创建一个新对象
- 将构造函数的 this 指向新对象,执行构造函数中的代码给对象添加属性
- 设置新对象的 proto 属性指向构造函数的 prototype 原型对象
- 返回该对象 ( 如果构造函数没有 return 也会默认返回 this )
function _new(fun, ...args) {
const newObj = Object.create(fun.prototype)
const result = fun.apply(newObj, args)
return typeof result == 'object' ? result : newObj
}
继承
父类代码如下:
// 定义一个动物类
function Animal(name = 'Animal') {
// 属性
this.name = name
// 实例方法
this.sleep = function () {
console.log(`${this.name} is sleeping`)
}
}
// 原型方法
Animal.prototype.eat = function(food) {
console.log(`${this.name} is eating ${food}`)
}
- 原型继承 --- 将父类的实例做为子类的原型
child.prototype = new parent()
child.prototype.constructor = child
- 原型继承 --- 将父类的实例做为子类的原型
function Cat() {
}
Cat.prototype = new Animal()
Cat.prototype.name = 'cat'
var cat = new Cat()
console.log(cat.name) // cat
console.log(cat.eat('fish')) // cat is eating fish
console.log(cat.sleep()) // cat is sleeping
console.log(cat instanceof Animal) // true
console.log(cat instanceof Cat) // true
特点:
- 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
- 父类新增原型方法/原型属性,子类都能访问到
- 简单,易于实现
缺点:
- 要想为子类新增属性和方法,必须要在
new Animal()
这样的语句之后执行,不能放到构造器中 - 无法实现多继承
- 来自原型对象的所有属性被所有实例共享(来自原型对象的引用属性是所有实例共享的)(详细请看附录代码: 示例1)
- 创建子类实例时,无法向父类构造函数传参
- 构造继承 --- 使用父类的构造函数来增强子类的实例,即复制父类的属性给子类 ( 没用到原型 )
function Cat(name = 'Tom') {
Animal.call(this)
}
var cat = new Cat()
console.log(cat.name) // Tom
// console.log(cat.eat('fish')) // 报错找不到 eat 方法
console.log(cat.sleep()) // Tom is sleeping
console.log(cat instanceof Animal) // false
console.log(cat instanceof Cat) // true
特点:
- 解决了1中,子类实例共享父类引用属性的问题
- 创建子类实例时,可以向父类传递参数
- 可以实现多继承(call多个父类对象)
缺点:
- 实例并不是父类的实例,只是子类的实例
- 只能继承父类的实例属性和方法,不能继承原型属性/方法
- 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
- 实例继承 --- 为父类实例添加新特性,做为子类的实例返回
function Cat(name = 'Tom') {
var animal = new Animal()
animal.name = name
return animal
}
var cat = new Cat()
console.log(cat.name) // Tom
console.log(cat.eat('fish')) // Tom is eating fish
console.log(cat.sleep()) // Tom is sleeping
console.log(cat instanceof Animal) // true
console.log(cat instanceof Cat) // false
特点:
- 不限制调用方式,不管是new 子类()还是子类(),返回的对象具有相同的效果
缺点:
- 实例是父类的实例,不是子类的实例
- 不支持多继承
- 拷贝继承
function Cat(name = 'Tom') {
var animal = new Animal()
for(var o in animal) {
Cat.prototype[o] = animal[o]
}
Cat.prototype.name = name
}
var cat = new Cat()
console.log(cat.name) // Tom
console.log(cat.eat('fish')) // Tom is eating fish
console.log(cat.sleep()) // Tom is sleeping
console.log(cat instanceof Animal) // false
console.log(cat instanceof Cat) // true
特点:
- 支持多继承
缺点:
- 效率较低,内存占用高(因为要拷贝父类的属性)
- 无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到)
- 组合继承 --- 通过调用父类的构造继承父类的属性并保留传参的优点,然后通过将父类的实例做为子类原型,实现函数的复用
function Cat(name = 'Tom') {
Animal.call(this)
this.name = name
}
Cat.prototype = new Animal()
var cat = new Cat()
console.log(cat.name) // Tom
console.log(cat.eat('fish')) // Tom is eating fish
console.log(cat.sleep()) // Tom is sleeping
console.log(cat instanceof Animal) // true
console.log(cat instanceof Cat) // true
特点:
- 弥补了方式2的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
- 既是子类的实例,也是父类的实例
- 不存在引用属性共享问题
- 可传参
- 函数可复用
缺点:
- 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)
- 寄生组合继承 --- 通过寄生方式砍掉父类的实例属性,这样在调用两次父类构造的时候就不会初始化两次实例方法和属性,避免组合继承的缺点
function Cat(name = 'Tom') {
Animal.call(this)
this.name = name
}
(function() {
// 创建一个没有实例方法的类
var Trans = function () {}
Trans.prototype = new Animal()
//将实例作为子类的原型
Cat.prototype = new Trans()
})()
var cat = new Cat()
console.log(cat.name) // Tom
console.log(cat.eat('fish')) // Tom is eating fish
console.log(cat.sleep()) // Tom is sleeping
console.log(cat instanceof Animal) // true
console.log(cat instanceof Cat) // true
特点:
- 堪称完美
缺点:
- 实现较为复杂
原型题
function fun() {
this.a = 0
this.b = function() {
console.log(this.a)
}
}
fun.prototype = {
b: function() {
this.a = 20
console.log(this.a)
},
c: function() {
this.a = 30
console.log(this.a)
}
}
var my_fun = new fun()
my_fun.b() // 0
my_fun.c() // 30
function Fn() {
var n = 10
this.m = 20
this.aa = function() {
console.log(this.m)
}
}
Fn.prototype.bb = function() {
console.log(this.n)
}
var f1 = new Fn
Fn.prototype = {
aa: function() {
console.log(this.m + 10)
}
}
var f2 = new Fn
console.log(f1.constructor) // Fn
console.log(f2.constructor) // Object
f1.bb() // undefined
f1.aa() // 20
f2.bb // TypeError
f2.aa() // 20
f2.__proto__.aa() // NaN
网友评论