ES5对象创建与继承(JS高程阅读随笔)

164 阅读12分钟

回看JS高级程序设计的一些随笔。 关于ES5的对象创建以及继承,大部分是书中内容的记录,也有一些自己的理解。

ES5的对象创建方式以及继承方式看似非常多,难以记忆。

但其实也没必要记忆,只需要知道一些关键结论,以及这些写乱七八糟的方法之间有什么联系就行了。

对于对象创建以及继承,书中的行文方式是循序渐进的给出一系列做法,由弱到强,由浅到深。从暴露一个方法存在的问题,到给出下一个方法对上一个解法的问题解决,一环扣一环的给出最终解法。

先抛两个结论:

  • 组合式对象创建是最好的对象创建方案
  • 寄生组合式继承创建是最好的继承方案

全文其实就是在得出这2个结论。

创建对象

最简单的方式就是通过对象字面量的方式来创建,但问题是 无法复用字面量代码,如果创建多个属性相同的对象,会有大量重复代码。

下面看下ES5中,各种创建对象的模式。

工厂模式

简单了解即可,实际也不再使用了。

function createPerson (name, age, job) {
	var o = new Object()
	o.name = name
	o.age = age
  o.job = job
	o.sayName = function () {
		alert(this.name)
	}
	return o
}

var person1 = createPerson('Nick', 29, 'software engineer')
var person2 = createPerson('greg', 27, 'doctor')

工厂模式的问题是,无法识别当前对象类型。

构造函数模式

function Person (name, age, job) {
	this.name = name
	this.age = age
  this.job = job
	this.sayName = function () {
		alert(this.name)
	}
}

var person1 = new Person('Nick', 29, 'software engineer')
var person2 = new Person('greg', 27, 'doctor')

构造函数 与 工厂模式的不同之处:

  • 没有在内部显示的创建对象
  • 直接将属性和方法赋给了 this 对象
  • 没有return语句

构造函数必须使用new操作符来调用(不用new调用也不会报错,严格模式下会报错)

new操作符调用构造函数实际上经历的步骤:

  • 创建了一个新对象
  • 将构造函数的作用域赋给新对象(this就指向了新对象)
  • 执行构造函数中的代码
  • 返回新对象
var person1 = new Person('Nick', 29, 'software engineer')

// 相当于
var person1 = new Object()
Person.call(obj,'Nick', 29, 'software engineer')

构造函数创建的实例,会有一个constructor属性,指向的是这个实例对应的构造函数。

var person1 = new Person('Nick', 29, 'software engineer')

person1.constructor === Person // true

另外,构造函数也可以当做普通函数来执行,也就是不用new来调用,这样的话,属性的绑定取决于函数调用时this的指向。

function Person (name, age, job) {
	this.name = name
	this.age = age
  this.job = job
	this.sayName = function () {
		alert(this.name)
	}
}

// 在全局作用域下执行
Person('Nick', 29, 'software engineer')
window.name // Nick

// 在一个对象作用域下执行
var o = new Object()
Person.call(o, Person('Nick', 29, 'software engineer'))
o.name // Nick

构造函数模式的问题:

构造函数中的方法,如果是显式定义在构造函数中的,那么,在每次实例化对象的时候,都需要将方法重新实例化一遍,这是没有必要的。另外,复用性也会比较差。

function Person (name, age, job) {
	this.name = name
	this.age = age
  this.job = job
	this.sayName = new Function("alert(this.name)") // 每次实例化Person时,都会创建一个新的sayName方法,在资源上是没必要的
}

也许你会想到,把sayName方法放到外面定义不就行了,像下面这样。

function Person (name, age, job) {
	this.name = name
	this.age = age
  this.job = job
	this.sayName = sayName
}

function sayName () {
	alert(this.name)
}

这样确实可以解决重复实例化函数对象以及重复代码的问题。但他本身的问题是:污染全局作用域。

原型模式

这一节极其重要

我们创建的每一个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含特定类型的所有实例共享的属性和方法。

上面这段话摘自红宝书,其中关键点提取一下:

  • 只要是个函数就有 prototype属性,不一定非得是构造函数
  • prototype是函数的属性
  • prototype是一个指针,指向一个对象(或者简单理解,prototype就是一个对象好像也没啥问题,不影响使用?)

原型模式可以解决构造函数带来的问题,构造函数有什么问题?主要是公共方法的定义问题,那么可以得出一个小结论,原型模式重点是用来解决方法问题的,换句话说,我们更多的情况下,是往原型上定义方法,而不是属性。

function Person () {}

Person.prototype.name = "Mike";
Person.prototype.age = 20;
Person.prototype.name = "Doctor";

Person.prototype.sayName = function () {
	alert(this.name)
};

var person1 = new Person()
person1.sayName() // Mike

var person2 = new Person()
person2.sayName() // Mike

console.log(person1.sayName() === person2.sayName()) // true

上面代码最后一行,可以看出,所有实例访问的都是同一组属性和同一个sayName函数。

理解原型对象

constructor是什么?

每创建一个函数,这个函数就会自动创建一个prototype属性,指向函数的原型对象。原型对象会自动获得一个constructor属性,这个属性指向原来的构造函数。

就前面例子来说,可以认为 Person.prototype.constructor === Person。

_ _ proto _ _ 是什么?

通过构造函数实例化一个对象之后,这个实例与原构造函数之间,会通过一个指针[[prototype]]相关联,一般在浏览器端的实现是 实例下的一个名为 _ _ proto _ _ 的属性,指向原构造函数的prototype属性,举例来说:

function Person () {}

var person1 = new Person()

person1.__proto__ === Person.prototype // true

还有一点需要重点记忆的是,对每个实例访问某一属性的时候,会经历以下步骤

  • 搜索实例中是否有该属性
  • 如果没有,搜索实例指针的原型对象上是否有该属性

实例上的操作不会影响原型属性

如果,实例上没有的属性,而原型中有,那么在实例上对这个属性进行修改操作,相当于先在实例上创建了一个同名属性,并不会影响到原型中的属性。

function Person () {
	this.age = 20
}

Person.prototype.name = 'Mike'

var person1 = new Person()

console.log(person1.name) // Mike 来自原型
console.log(person1.age) // 20 来自实例

person1.name = 'Rose'

console.log(person1.name) // Rose 来自实例

如何区别一个实例能访问的属性,是来自实例本身还是原型?

用hasOwnProperty方法即可。(in 操作符不行,只要能访问到 in 操作符就会返回true)

constructor 与 instanceOf

下面的写法,会一次性的将各种属性绑定到构造函数的prototype上,但有个问题,原型上的constructor属性没有了。但是,这个不会影响实例instanceOf的判断,具体见代码:

function Person () {}

Person.prototype = {
	name: 'Mike',
	age: 20,
	sayName: function () {console.log(this.name)}
}

const person1 = new Person()

console.log(person1 instanceOf Person) // true

也就是说,constructor 不会影响 instanceOf 关键字的判断。

给构造函数prototype赋值时,一定要注意顺序

正确用法

function Person () {}

Person.prototype = {
	name: 'Mike',
	age: 20,
	sayName: function () {console.log(this.name)}
}

const person1 = new Person()
person1.sayName() // Mike

错误用法

function Person () {}

const person1 = new Person()

Person.prototype = {
	name: 'Mike',
	age: 20,
	sayName: function () {console.log(this.name)}
}

person1.sayName() // 会报错

因为在实例化之后再修改构造函数的原型对象,等于切断了实例__proto__与原构造函数原型属性之间的关系。

原型模式的问题

第一个问题是,原型模式没法传参

第二个问题是,原型模式中,引用类型的属性会被实例之间共享(这是最大的问题,后面有一些列的影响)

function Person () {}

Person.prototype = {
	name: 'Mike',
	age: 20,
	friends: ['Bob', 'Will']
	sayName: function () {console.log(this.name)}
}

const person1 = new Person()
const person2 = new Person()

person1.friends.push('jean')

console.log(person1.friends) // ['Bob', 'Will', 'jean']
console.log(person2.friends) // ['Bob', 'Will', 'jean']

组合式

组合构造函数模式和原型模式,可以当做是最好的对象创建方法。直接上代码:

function Person (name, age, job) {
	this.name = name,
	this.age = age,
	this.job = job,
	this.friends = ['Will']
}

Person.prototype = {
	sayName: function () {console.log(this.name)}
}

var person1 = new Person('Mike', 20, 'FE')
var person2 = new Person('Bob', 21, 'RD')

person1.friends.push('jean')

console.log(person1.friends) // ['Will', 'jean']
console.log(person2.friends) // ['Will']

console.log(person1.friends === person2.friends) // false
console.log(person1.sayName === person2.sayName) // true

简单来说,就是结合了构造函数模式和原型模式的各自的优点,即:

  • 构造函数的 可以传参 (也就解决了原型会共享引用类型属性的问题)
  • 原型的共享方法

再直观点,用构造函数的方式定义公共属性,用原型的方式定义公共方法

寄生构造函数模式

见下方代码。

function Person (name, age, job) {
	var o = new Object()
	o.name = name,
	o.age = age,
	o.job = job,
	o.friends = ['Will']
	o.sayName = function () {alert(this.name)}
	return o
}

var person1 = new Person('Mike', 20, 'FE')

适用场景:希望能够修改全局对象某些属性的时候,可以采用这个方式。

// 创建一个新的数组造构造函数,修改了push方法,但不影响Array本身

function SpecialArray (name, age, job) {
	var arr = new Array()
	arr.push = function () {alert('push')}
	return arr
}

继承

说完了对象的创建,下面看看继承。

下面介绍ES5中各种继承方法,每一种都会存在一些问题,后面的方法基本上是在解决前面方法的一些问题而衍生出来的,逐步达到“完美”,最后的寄生组合式继承,是最好的JS继承方法

原型链及原型链继承

原型链

每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向起构造函数原型对象的内部指针。

如果,让一个构造函数A的原型对象等于另一个构造函数B的实例的话,这样,A就继承了来自B的所有属性,包括B实例上的属性,以及B原型对象上的属性。B如果也是通过这样的方式继承了构造函数C的所有属性,那A就相当于继承了B和C的属性。

那么在A上面访问一个属性的时候会经历以下顺序。

  • A实例
  • A原型对象
  • B实例
  • B原型对象
  • C实例
  • C原型对象
  • Object实例(Object是所有对象实例原型链的最顶层对象)
  • Object原型对象(Object是所有对象实例原型链的最顶层对象)
function C () {
	this.name = 'C instance'
}
C.prototype.name = 'C prototype'

function B () {
	this.name = 'B instance'
}
B.prototype = new C() // B 继承自 C
B.prototype.name = 'B prototype'

function A () {
	this.name = 'A instance'
}
A.prototype = new B() // A 继承自 B
A.prototype.name = 'A prototype'

var a = new A()

console.log(a.name) // 查找顺序 'A instance' -> 'A prototype' -> 'B instance' -> 'B prototype' ->  'C instance' -> 'C prototype'  

说清楚原型链,也就说清楚了原型链继承。

注意几个点:

  • 原型链最顶层 是 Object
  • 通过原型链继承,子类型的 prototype被重写了,所以其实例的 constructor 也就不是原来的构造函数了,而是父类型的构造函数,按上面例子来说,a 的 constructor 已经是 B了。
  • 使用原型链继承时,如果要给子类型的原型再定义属性,一定要在将子类型原型对象指向父类型实例之后,原因不解释了(赘述了也是)

原型链继承的问题

和原型创建对象的问题一样,一是没办法传参,二是引用类型的属性会被共享,且在继承场景下,共享问题更严重。

因为,父类的所有属性,都成了子类型的原型属性,所以不论父类型是如何定义的属性,只要是引用类型,就会被共享。

借用构造函数继承

直接看代码吧

function Parent (lastName) {
	this.lastName = lastName
}

function Child (lastName) {
	Parent.call(this, lastName) // 在Child构造函数的作用域下,执行Parent,也就是把Parent上面的属性,都绑定到Child的作用域上
}

var son = new Child('Bryant')

console.log(son.lastName) // Bryant

这样做的好处是,可以传参。

借用构造函数继承的问题

问题和用构造函数创建对象一样,方法是无法复用的。

所以到这应该能想到,把借用构造函数和原型链,结合起来。也就出现了下面的“组合式继承”

组合式继承

组合式继承是ES5中最常用的继承方式,但不完美。

集合 借用构造函数继承 和 原型链继承 的优点,实现的继承方式。(这个也比较像前面创建对象中的,组合式创建对象)

思路上,用构造函数的方式继承属性,用原型链方式继承方法,看代码:

function Parent (lastName) {
	this.lastName = lastName
	this.house = ['haidian', 'chaoyang']
}
Parent.prototype.sayLastName = function () {console.log(this.lastName)}

function Child (lastName) {
	Parent.call(this, lastName) // 继承属性 lastName,house
}

Child.prototype = new Parent() // 继承 sayLastName方法
Child.prototype.constructor = Child // 手动指定constructor

var son = new Child('Bryant')
var daughter = new Child('Bryant')

son.house.push('changping')

console.log(son.house) // ['haidian', 'chaoyang', 'changping']
console.log(daughter.house) // ['haidian', 'chaoyang']

组合式继承的问题

为什么说组合式继承是最常用的,但又不完美呢?

因为,**组合式继承,调用了2次父类的构造函数。**更深层的问题是,调用2次构造函数,会创建2套来自父类的属性到子类当中,一套在子类的构造函数里,里面是父类的构造函数属性,一套在子类的原型对象里,里面是父类所有的属性,包括实例的和原型的,所以,在子类的原型对象上,来自父类的实例属性,是重复的。

这里有点绕,多读几遍,画画图就理解了,不理解也没关系,记得,“有缺点”就行了。

下面,是解决办法。终极解决方案,是“寄生组合式继承” ,但为了理解什么是寄生组合式继承,需要先了解“原生式继承”和“寄生式继承”,他们是逐级增强的。

原型式继承

先看代码

function createObject (O) {
	function F () {}
	F.prototype = new O()
	return new F()
}

function Parent () {
	this.lastName = 'Bryant'
	this.house = ['haidian', 'chaoyang']
}

var son = createObject(Parent)
var daughter = createObject(Parent)

son.push('changping')

console.log(son.lastName) // Bryant
console.log(daughter.lastName) // Bryant
console.log(son.house) // ['haidian', 'chaoyang', 'changping']
console.log(daughter.house) // ['haidian', 'chaoyang', 'changping']

创建了一个方法 createObject,本质上就是把原型继承的逻辑封装起来了。我理解作用就是让这段原型继承逻辑能够复用。

问题是,引用类型的属性被共享,这个不多说了。

另外提一点,Object.create 方法,内部就是这个逻辑。

寄生式继承

寄生式继承和原型式思路类似,都是通过一个类似工厂模式的方法,浅拷贝一个对象之后,再给其添加其他属性,最终返回一个新的对象。

function createObject (O) {
	function F () {}
	F.prototype = new O()
	return new F()
}

function createAnthor (original) {
	var clone = createObject(original)
	clone.sayHi = function () {console.log('Hi')}
	return clone 
}

寄生组合式继承

最后来看看寄生组合式继承,简单来说就是沿袭 组合式继承的思路,用构造函数继承属性,原型继承方法,但在原型继承方法的部分,不再直接调用构造函数,而是采纳寄生式继承的方式,拷贝一份父类的原型对象,这样来杜绝无用属性的产生。

function createObject (O) {
	function F () {}
	F.prototype = new O()
	return new F()
}

function Parent (lastName) {
	this.lastName = lastName
	this.house = ['haidian', 'chaoyang']
}

Parent.sayLastName = function () {consle.log(this.lastName)}

function Child () {
	Parent.call(this)
}

Child.prototype = createObject(Parent.prototype)
Child.prototype.constructor = Child

// 或者把以上步骤分封装成一个函数
function inheritPrototype (C, P) {
	var prototype = createObject(P.prototype)
	prototype.constructor = C
	C.prototype = prototype
}