导言:首先,我想从ES5出发,探讨在创建对象时我们会面临哪些问题。接着,我们将深入了解构造函数是如何解决这些问题的。最后,我们将探究ES6对构造函数进行了哪些重要的升级与改进。
一、创建对象遇到的问题
1.1 获取自身属性繁琐
在JavaScript中,我们要描述一个事物,最好的方式是创建一个对象,然后给这个对象添加属性和方法。
比如,我们要描述一台手机:
var phone = {
brand: '小米',
model: 'MIX 3',
fullName: '小米MIX 3'
};
上面描述了一个手机的基本信息,有品牌、型号和全称。
我们会发现全称这个属性是重复的,我们可不可以把它提取出来呢?
像这样:
var phone = {
brand: '小米',
model: 'MIX 3',
fullName: brand + ' ' + model
};

这样当然是不可以的,因为这样写就相当于把brand和model当作一个变量了,我们把他们定义在phone里了,而我们在外部可没有定义。
那这样呢?
var phone = {
brand: '小米',
model: 'MIX 3',
fullName: phone.brand + ' ' + phone.model
};

当然也是不可以。你疯了吗?这个时候phone对象还没有创建呢。
那这样总行了吧?
var phone = {
brand: '小米',
model: 'MIX 3',
getFullName(){
return phone.brand + ' ' + phone.model
}
};
getFullName可是在phone对象创建之后运行的。运行一下,果然可以。

1.2 批量创建对象繁琐
如果我们要创建多个手机对象,可能需要多次复制粘贴,而且我们会发现,有很多代码逻辑是相同的。

那么有什么东西可以帮我们做这些重复工作呢?聪明的你马上就想到了方法,函数就是一段可重复执行的代码块,正好可以帮助我们做这些重复操作。于是你写下了如下方法:
function CreatePhone (brand, model) {
var phone = {}
phone.brand = brand
phone.model = model
phone.fullName = brand + model
return phone
}
var myPhone = CreatePhone('iPhone', '12')
console.log(myPhone) // {brand: "iPhone", model: "12", fullName: "iPhone 12"}
var myPhone2 = CreatePhone('小米', '15')
console.log(myPhone2) // {brand: "小米", model: "15", fullName: "小米 15"}
完美。
后来,你看到了ES5 中的构造函数,你发现原来构造函数就是用来做这个的。
二、ES5中的构造函数
你查阅了ES5文档,发现它可以轻松实现我们上面的需求,于是你写下了如下代码:
你对之前的函数做了一些简单修改:
- 构造函数首字母大写
- 无需手动创建对象和返回对象
- 可通过this访问自身属性
- 需要使用new来调用
function CreatePhone (brand, model) {
this.brand = brand
this.model = model
this.fullName = this.brand + this.model
}
var myPhone = new CreatePhone('iPhone', '12')
console.log(myPhone) // {brand: "iPhone", model: "12", fullName: "iPhone 12"}
var myPhone2 = new CreatePhone('小米', '15')
console.log(myPhone2) // {brand: "小米", model: "15", fullName: "小米 15"}
你发现创建对象如此简单。
2.1 对象的方法呢?
你不满足于你的手机只能描述名称,于是你为你的手机创建了一个打电话的方法:
function CreatePhone (brand, model) {
this.brand = brand
this.model = model
this.fullName = this.brand + ' ' + this.model
this.call = function () {
console.log('我的型号是'+ this.fullName +',我在打电话')
}
}
var myPhone = new CreatePhone('iPhone', '12')
myPhone.call() // 我的型号是iPhone 12,我在打电话
var myPhone2 = new CreatePhone('小米', '15')
myPhone2.call() // 我的型号是小米 15,我在打电话
你轻松地为手机创建了打电话方法。
但是,你走在路上,眉头一皱,发现事情没有这么简单。
你打印对比了这两个对象的打电话方法,发现竟然返回false,也就是说,他们分别开辟了一个内存空间。
function CreatePhone (brand, model) {
this.brand = brand
this.model = model
this.fullName = this.brand + ' ' + this.model
this.call = function () {
console.log('我的型号是'+ this.fullName +',我在打电话')
}
}
var myPhone = new CreatePhone('iPhone', '12')
var myPhone2 = new CreatePhone('小米', '15')
console.log(myPhone.call === myPhone2.call); // false
你百思不解,为什么一样的方法,开辟了两个内存空间,如果有一百个方法,创建一百个对象,那得多浪费空间啊。
求知欲驱使你继续研究,你发现了原型链。
2.2 原型链
你读完原型链的文档,你发现了构造函数和实例对象之间的关系,并画出了一个最简单的三角关系。

function CreatePhone (brand, model) {
this.brand = brand
this.model = model
this.fullName = this.brand + ' ' + this.model
this.call = function () {
console.log('我的型号是'+ this.fullName +',我在打电话')
}
}
var myPhone = new CreatePhone('小米', '15')
console.log(CreatePhone.prototype); // {}
console.log(myPhone.__proto__); // {}
console.log(myPhone.__proto__ === CreatePhone.prototype); // true
你发现CreatePhone上有一个隐藏的prototype属性,它指向一个空对象。
而用CreatePhone生成的myPhone实例对象,你发现它身上也有一个隐藏的__proto__属性。
并且你发现他们共享同一片内存空间。事情变的有意思起来了。
你在文档上抓住了几个重点:
1、当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾;
2、JavaScript 只有一种结构:对象。每个对象(object)都有一个私有属性指向另一个名为原型(prototype)的对象。原型对象也有一个自己的原型,层层向上直到一个对象的原型为
null。
也就是说,我们在实例对象上访问一个属性或者方法,它会先看自己有没有,如果没有再看构造函数原型上面有没有。
function CreatePhone (brand, model) {
this.brand = brand
this.model = model
this.fullName = this.brand + ' ' + this.model
}
CreatePhone.prototype.call = function () {
console.log('我的型号是' + this.fullName + ',我在打电话')
}
var myPhone = new CreatePhone('小米', '15')
myPhone.call() // 我的型号是小米 15,我在打电话
var yourPhone = new CreatePhone('华为', 'P50')
yourPhone.call() // 我的型号是华为 P50,我在打电话
console.log(myPhone.call === yourPhone.call); // true
上面,你发现终于每个实例的打电话方法都可以共享一块内存空间了。这不能再完美了。
2.3 ES5构造函数中的继承
你创建了一个手机构造函数后,你又想创建一个卫星手机的构造构造函数。
这个卫星手机也有商标、型号、全称的属性以及打电话的方法。并且还有一些属于自己的属性方法。
function Phone (brand, model) {
this.brand = brand
this.model = model
this.fullName = this.brand + ' ' + this.model
}
Phone.prototype.call = function () {
console.log('我的型号是' + this.fullName + ',我在打电话')
}
function SatellitePhone (brand, model) {
this.brand = brand
this.model = model
this.fullName = this.brand + ' ' + this.model
this.antenna = '天线'
}
SatellitePhone.prototype.call = function () {
console.log('我的型号是' + this.fullName + ',我在打电话')
}
你复制了一份改了改,发现一样能用。
但是机智的你发现了一个问题。如果你想给所有手机增加一个color属性,你还得给Phone和SatellitePhone都增加一份,同时维护两份数据,这太累了。
那你不能直接在SatellitePhone中调用一次Phone呢:
function SatellitePhone (brand, model) {
Phone(brand, model)
this.antenna = '天线'
}
显然this指向会出问题。
this可以用call解决,聪明的你写出了以下代码:
function Phone (brand, model) {
this.brand = brand
this.model = model
this.fullName = this.brand + ' ' + this.model
}
Phone.prototype.call = function () {
console.log('我的型号是' + this.fullName + ',我在打电话')
}
function SatellitePhone (brand, model) {
Phone.call(this, brand, model)
this.antenna = '天线'
}
var myPhone = new SatellitePhone('小米', '10')
console.log(myPhone); // {brand: '小米', model: '10', fullName: '小米 10', antenna: '天线'}
利用call方法修改this指向,这样很好地解决了属性继承的问题。
那么,方法怎么继承呢?
你冥思苦想,你鼠标向上一划,看到了原型链的知识。
你灵机一动,只要可以让SatellitePhone的prototype得到Phone上的属性和方法就可以了:
SatellitePhone.prototype = Phone.prototype
但是这样会导致 SatellitePhone 的实例和 Phone 的实例共享同一个原型对象。这可能会导致意外的副作用,如果在 Phone.prototype 上添加或修改属性时,会影响到所有继承自 Phone 的实例。
SatellitePhone.prototype = new Phone()
SatellitePhone.prototype.constructor = SatellitePhone
这里通过 new 关键字创建一个 Phone 实例,这个实例会拥有 Phone 构造函数中定义的所有属性和方法。将这个实例赋值给 SatellitePhone.prototype,使得 SatellitePhone 的实例可以直接访问这些属性和方法。
对象原型(__proto__)和构造函数(prototype)原型对象里面都有一个属性constructor属性,constructor我们称为构造函数,因为它指向构造函数本身。
constructor主要用于记录该对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。
因为直接赋值会覆盖掉SatellitePhone.prototype内的constructor,上面还重新指定了constructor。
ES5实现继承完整代码:
function Phone (brand, model) {
this.brand = brand
this.model = model
this.fullName = this.brand + ' ' + this.model
}
Phone.prototype.call = function () {
console.log('我的型号是' + this.fullName + ',我在打电话')
}
function SatellitePhone (brand, model) {
// 继承属性
Phone.call(this, brand, model)
this.antenna = '天线'
}
// 继承prototype方法
SatellitePhone.prototype = new Phone()
SatellitePhone.prototype.constructor = SatellitePhone
这种继承方式一般也被称为组合继承。组合继承就是将原型链和借用构造函数的技术结合到一起实现继承。
2.4 ES5 构造函数的问题
最后,你发现ES5构造函数的写法好像不太得劲,比如:
- 为什么用构造函数定义一个类,它的属性和方法好像分离了一样(一个定义在大括号里,一个定义在外面的prototype上)
- 继承写起来好麻烦
- 用for...in去遍历实例对象,可以把prototype上的方法遍历出来
等等。于是,你决定去看看ES6有没有新的解决方案。
96

被折叠的 条评论
为什么被折叠?



