原型与继承
原型与继承
原型
原型内容
每个函数都会创建一个
prototype
属性(指向原型对象),该属性是一个对象,默认只有一个constructor
属性(指向该函数本身)。其他所有方法都继承自Object
。通过
new
运算符生成对象,每次生成的对象都不一样,可使用函数的prototype
属性在对象之间提供共享的属性和方法。并不是所有函数都具有
prototype
属性,Function.prototype.bind()
没有该属性,因为bind()
并不是一个构造函数。
每个 JavaScript 函数实际上都是一个
Function
对象,由Function()
构造函数创建一个新的Function
对象。Function
是一个函数,也是一种特殊类型的对象,也具有__proto__
属性。Function
是它自己的构造器,因Function.__proto__
指向Function.prototype
,导致Function.constructor === Function
。每个对象都有一个
__proto__
属性,指向创建该对象的函数(即:function Object()
)的prototype
。__proto__
是存在于实例与构造函数的原型对象(prototype
)之间的连接,而不是存在于实例与构造函数之间。__proto__
属性已在 ECMAScript 6 语言规范中标准化,用于确保 Web 浏览器的兼容性。它已被不推荐使用,现在更推荐使用以下 API:Object.getPrototypeOf(object)
/Reflect.getPrototypeOf(target)
:返回指定对象的原型(内部[[Prototype]]
属性的值)。Object.setPrototypeOf(obj, prototype)
/Reflect.setPrototypeOf(target, prototype)
:设置一个指定的对象的原型(即,内部[[Prototype]]
属性)到另一个对象或null
。Object.create(proto)
:用于创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype
)。
Object.prototype
是原型链的终点,所有对象都是从它继承了方法和属性。Object.prototype
的__proto__
指向null
。原型的动态性:从原型上搜索值的过程是动态的,所以即使实例在修改原型之前已经存在,任何时候对原型对象所做的修改也会在实例上反映出来。
原型的问题:源自它的共享特性,来自于包含引用值的属性。
new 运算符
new
运算符 :创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。
new
关键字会进行如下操作:- 创建一个空的简单 JavaScript 对象(即,
{}
)。 - 为新创建的对象添加属性
__proto__
,将该属性链接至构造函数的原型对象(prototype
)。 - 将新创建的对象作为
this
的上下文。 - 如果该函数没有返回对象,则返回
this
。
- 创建一个空的简单 JavaScript 对象(即,
执行
new Foo(...)
时,会进行如下操作- 一个继承自
Foo.prototype
的新对象被创建。 - 使用指定的参数调用构造函数
Foo
,并将this
绑定到新创建的对象。new Foo
等同于new Foo()
(即,没有指定参数列表,Foo
不带任何参数调用的情况)。 - 由构造函数返回的对象就是
new
表达式的结果。- 如果构造函数没有显式返回一个对象,则使用创建的新对象。
- 一般情况下,构造函数不返回值,但是用户可以选择主动返回对象,来覆盖正常的对象创建步骤
- 一个继承自
const person = new Person('123', 12)
// 等同于
// 1、创建一个全新的对象。
// 2、为新创建的对象添加属性 __proto__,将该属性链接至构造函数的原型对象(prototype),相当于 obj.__proto__ = Person.prototype 。
var obj = Object.create(Person.prototype)
// 3、将新创建的对象作为 `this` 的上下文。
Person.apply(obj, ['12344', 23])
原型链
ECMA-262 把原型链定义为 ECMAScript 的主要继承方式。其基本思想就是通过原型继承多个引用类型的属性和方法。
当读取对象的一个属性或者调用一个方法时,如果不存在,JavaScript 会尝试在原型中查找。写/删除操作直接在对象上进行,不使用原型。
默认情况下,所有引用类型都继承自
Object
,这是通过原型链实现的。任何函数的默认原型都是一个Object
的实例,这意味着这个实例有一个内部指针指向Object.prototype
。判断一个对象是否在另一个对象的原型链上
object instanceof constructor
:用于检测构造函数(constructor
)的prototype
属性是否出现在某个实例对象(object
)的原型链上。prototypeObj.isPrototypeOf(object)
:用于测试一个对象是否存在于另一个对象(object
)的原型链上。
子类覆盖父类方法,必须在原型赋值之后再添加到原型上。
原型链的问题:
在原型中包含引用值。原型中包含的引用值会在所有实例间共享。
子类型在实例化时,不能给父类型的构造函数传参。无法在不影响所有对象实例的情况下把参数传进父类的构造函数。
继承
盗用构造函数(对象伪装、经典继承)
- 实现思路:在子类构造函数中调用父类构造函数。使用
apply()
和call()
方法以新创建的对象为上下文执行构造函数。 - 优点:在子类构造函数中向父类构造函数传参。
- 缺点:必须在构造函数中定义方法,因此函数不能重用。此外,子类也不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式。由于存在这些问题,盗用构造函数基本上也不能单独使用。
function SuperType(name) {
this.name = name
}
function SubType() {
// 继承 SuperType 属性并传参
SuperType.call(this, 'Nicholas')
this.age = 29 // 实例属性
}
let instance = new SubType()
console.log(instance.name) // "Nicholas"
console.log(instance.age); // 29
组合继承
- 实现思路:使用原型链继承原型上的属性和方法,并通过盗用构造函数继承实例属性。
- 优点:构造函数可以传参,不会与父类的引用属性共享,可以复用父类的函数。
- 缺点
- 在继承父类函数时,调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费。
- 父类构造函数始终会被调用两次。一次在是创建子类原型时调用,另一次是在子类构造函数中调用。
function SuperType(name) {
this.name = name
this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
console.log(this.name)
}
function SubType(name, age) {
// 继承 SuperType 属性并传参
SuperType.call(this, name) // 第二次调用 SuperType()
this.age = age // 实例属性
}
// 继承方法
SubType.prototype = new SuperType() // 第一次调用 SuperType()
SubType.prototype.sayAge = function () {
console.log(this.age)
}
let instance1 = new SubType('Nicholas', 29)
instance1.colors.push('black')
console.log(instance1.colors) // [ 'red', 'blue', 'green', 'black' ]
instance1.sayName() // "Nicholas";
instance1.sayAge() // 29
let instance2 = new SubType('Greg', 27)
console.log(instance2.colors) // [ 'red', 'blue', 'green' ]
instance2.sayName() // "Greg";
instance2.sayAge() // 27
原型式继承
实现思路:
object()
函数会创建一个临时构造函数,将传入的对象赋值给这个构造函数的原型,然后返回这个临时类型的一个实例。本质上,object()
是对传入的对象执行了一次浅复制。function object(o) { function F() {} F.prototype = o return new F() }
使用场景:需要在一个对象的基础上,再创建一个新对象。需要把这个对象先传给
object()
,然后再对返回的对象进行适当修改。
function object(o) {
function F() {}
F.prototype = o
return new F()
}
let person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van'],
}
let anotherPerson = object(person)
anotherPerson.name = 'Greg'
anotherPerson.friends.push('Rob')
let yetAnotherPerson = object(person)
yetAnotherPerson.name = 'Linda'
yetAnotherPerson.friends.push('Barbie')
console.log(person.friends) // [ 'Shelby', 'Court', 'Van', 'Rob', 'Barbie' ]
ECMAScript 5 通过增加 Object.create(proto, propertiesObject)
方法将原型式继承的概念规范化了。
proto
:新创建对象的原型对象propertiesObject
:如果该参数被指定且不为undefined
,则该传入对象的自有可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)将为新创建的对象添加指定的属性值和对应的属性描述符。这些属性对应于Object.defineProperties()
的第二个参数。
寄生式继承
实现思路:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。
注:通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似。
function object(o) {
function F() {}
F.prototype = o
return new F()
}
function createAnother(original) {
// 通过调用函数创建一个新对象
let clone = object(original)
clone.sayHi = function () {
// 以某种方式增强这个对象
console.log('hi')
}
return clone // 返回这个对象
}
let person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van'],
}
let anotherPerson = createAnother(person)
anotherPerson.sayHi() // "hi"
寄生式组合继承
实现思路:通过盗用构造函数继承属性,使用混合式原型链继承方法。
基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。
即:使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。
优点:解决了无用的父类属性问题,能正确的找到子类的构造函数。
function object(o) {
function F() {}
F.prototype = o
return new F()
}
// inheritPrototype 函数:
// > 第一步,创建父类原型的一个副本。
// > 然后,给返回的 prototype 对象设置 constructor 属性,解决由于重写原型导致默认 constructor 丢失的问题。
// > 最后,将新创建的对象赋值给子类型的原型。
// inheritPrototype 接受两个参数:
// > subType :子类构造函数
// > superType :父类构造函数
function inheritPrototype(subType, superType) {
let prototype = object(superType.prototype) // 创建对象
prototype.constructor = subType // 增强对象
subType.prototype = prototype // 赋值对象
}
function SuperType(name) {
this.name = name
this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
console.log(this.name)
}
function SubType(name, age) {
SuperType.call(this, name)
this.age = age
}
inheritPrototype(SubType, SuperType)
SubType.prototype.sayAge = function () {
console.log(this.age)
}