对象是JS中的引用数据类型,对象是一种复合数据类型,在对象中可以保存多个不同数据类型的属性。使用typeof检查一个对象时,会返回object。
1 对象创建模式 - Object构造函数模式
套路: 先创建空Object对象, 再动态添加属性/方法
适用场景: 起始时不确定对象内部数据
问题: 语句太多
<script>
var p = new Object();
p.name = 'zhaoshuai-lc'
p.age = 27
p.setName = function (name) {
this.name = name
}
console.log(p);
p.setName('zhaoshuai-la')
console.log(p);
</script>
但是,引出一下问题:
为什么?箭头函数就出现了问题,这里涉及到的是箭头函数的问题:
对于箭头函数而言:this —> window
对于普通函数而言:this—>p
2 对象创建模式-对象字面量模式
套路: 使用{}创建对象, 同时指定属性/方法
适用场景: 起始时对象内部数据是确定的
问题: 如果创建多个对象, 有重复代码
<script>
var p = {
name: 'zhaoshuai-lc',
age: 27,
sex: 'male'
}
console.log(p);
</script>
3 对象创建模式-自定义构造函数模式
套路: 自定义构造函数, 通过new创建对象
适用场景: 需要创建多个类型确定的对象
问题: 每个对象都有相同的数据, 浪费内存
4 对象创建模式-构造函数+原型的组合模式
构造函数+原型的组合模式–>最好用这个写法
套路: 自定义构造函数, 属性在函数中初始化, 方法添加到原型上
适用场景: 需要创建多个类型确定的对象
放在原型上可以节省空间(只需要加载一遍方法)
4 对象的继承
4.1 prototype 的引入
由于直接 new 出来的实例对象,都有自己的属性和方法的副本,无法做到数据共享,也浪费了内存。考虑到这点,Brendan Eich 决定为构造函数设置一个 prototype 属性。将所有需要共享的属性和方法,都放在这个 prototype 对象里面;那些独有的属性和方法,就放在构造函数里面。
这样实例对象创建好之后,将会有两类属性和方法,一个是自己的,一个是通过原型链(沿着 prototype )查找到的。
继承就是子类可以使用父类的所有功能,并且对这些功能进行扩展。 比如构造函数 B 想要使用构造函数 A 里的属性和函数,一种是直接复写 A 里的代码,复制粘贴一下。还有一种就是使用继承,让 B 继承 A,这样 B 就可以使用 A 里的功能。
4.2 原型链继承
将子类的原型对象指向父类的实例。
Child.prototype = new Parent()
function Parent(name, gender) {
this.name = name;
this.gender = gender;
}
Parent.prototype.getInfo = function () {
console.log('my name is ' + this.name + '; I am a ' + this.gender)
}
function Child(age) {
this.age = age
}
Child.prototype.getChildInfo = function () {
console.log('i am child')
}
// 将子类的原型对象指向父类的实例。
Child.prototype = new Parent("parent", "boy")
var child1 = new Child(30)
console.log(child1) // Child {age:30}
child1.getInfo() // my name is parent; I am a boy
console.log(child1.name) // parent
child1.getChildInfo() // TypeError: child1.getChildInfo is not a function
- 默认 Child 实例的原型对象是指向 Child.prototype 的,经过
Child.prototype = new Parent("parent","boy")
Child 的原型对象就指向了 Parent 的实例,具体原型链图如下:
依图可得,child 实例 _proto_
属性指向 parent 实例, parent 实例下的 _proto_
属性指向 Parent.prototype。所以 child1 既可以访问到 parent 实例的属性和方法(即 Parent 构造函数中定义的),也可以访问到 Parent.prototype 上定义的属性和方法
function Parent(name, gender) {
this.name = name;
this.gender = gender;
this.colors = ["blue", "pink"] // 新增引用类型属性 colors
}
Parent.prototype.getInfo = function () {
console.info('my name is ' + this.name + '; I am a ' + this.gender)
}
function Child(age) {
this.age = age
this.sports = ["basketball"] // 新增引用类型属性 sports
}
var parent = new Parent("parent", "boy")
Child.prototype = parent
var child1 = new Child(30, "child1 name") // 想传入child1 name 作为该实例独有的名称
child1.gender = "girl"
child1.colors.push("green")
child1.sports.push("football")
console.info(child1) // Child {age: 30, sports: ["basketball", "football"], gender: "girl"}
console.log(child1.name) // parent
var child2 = new Child(29, "child2 name") // 想传入child2 name 作为该实例独有的名称
console.info(child2) // Child {age: 29, sports: ["basketball"]}
console.info(child2.colors) // ["blue", "pink", "green"]
优点:
- 继承了父类构造函数里的属性和方法,又继承了父类原型对象上的属性和方法
缺点:
- 无法实现多继承(因为指定了原型对象)
- 来自原型对象的所有属性都被共享了,当更改个引用类型的属性时,所有的实例对象都会受到影响(这点从child2.colors可以看出来)
- 无法传递参数给父对象,比如我要设置个独有的 name 值,就只能在自己的构造函数里重新定义 name 属性,用来屏蔽父对象的name(这点从child1.name可以看出来)
4.3 构造函数继承
在子类构造函数内部使用 call 或 apply 来调用父类构造函数
function Child() {
Parent.call(this,...arguments)
}
来达到增强子类的目的,等于复制父类的实例属性给子类
function Parent(name,gender) {
this.name = name;
this.gender = gender;
}
function Child(age,name,gender) {
this.age = age
Parent.call(this,name,gender) // 等价于Parent.apply(this,[name,gender])
}
var child1 = new Child(30,"child1 name","girl")
console.info(child1) // Child {age: 30, name: "child1 name", gender: "girl"}
console.info(child1.name) // child1 name
console.info(child1 instanceof Child) // true
console.info(child1 instanceof Parent) // false
- 相当于在子类构造函数里,直接执行了父类构造函数。执行完后就会在子类的构造函数新增了父类所有的属性。
- 这个继承是借用了 call 或 apply 函数的能力。
- Child 的实例跟 Parent之间并无联系(这点从 intanceof Parent 可以看出来)
优点:
- 可以有多继承(在子类构造函数里分别调用父类的构造函数即可)
- 解决了子类实例共享父类构造函数里引用类型属性的问题(这点从 child.colors 可以看出来)
- 可以向父类传递参数(这点从 child1.name 可以看出来)
缺点:
- 不能继承父类原型对象上的属性和方法
- 子类实例跟父类之间并无联系(这点从 child1 instanceof Parent 为 false 可以看出来)
- 无法实现函数复用,对于父类里的函数属性,每个子类都会复制一份,影响性能
4.4 组合继承
既然原型链继承跟构造函数继承分别有优缺点,那能否将这两者结合起来?
// 原型链继承
Child.prototype = new Parent()
// 构造函数继承
function Child() {
Parent.call(this,...arguments)
}
- 使用原型链继承来保证子类能够继承父类原型对象上的属性和方法
- 使用构造函数继承来保证子类能够继承到父类构造函数上的属性和方法
基操:
- 通过call/apply在子类构造函数内部调用父类构造函数
- 将子类构造函数的原型对象指向父类构造函数创建的一个匿名实例
- 修正子类构造函数原型对象的constructor属性,将它指向子类构造函数
function Parent(name) {
this.name = name;
}
Parent.prototype.getInfo = function () {
console.info('my name is ' + this.name)
}
function Child(name) {
Parent.call(this, name) // 构造函数继承
}
Child.prototype.getChildInfo = function () {
console.log('i am child')
}
Child.prototype = new Parent() // 原型链继承,传入空参数即可,因为传入任何参数都会被屏蔽
Child.prototype.constructor = Child // 修复 child1 constructor 指向 Parent 问题
var child1 = new Child("child1 name")
child1.getChildInfo() // TypeError: child1.getChildInfo is not a function
console.info(child1) // Child {name: "child1 name"}
child1.getInfo() // my name is child1 name
console.info(child1.constructor) //ƒ Child(name) { Parent.call(this,name) // 构造函数继承 }
优点:
- 可以继承父类实例属性和方法,也可以继承父类原型对象上的属性和方法
- 跟构造函数继承一样,解决了父类构造函数里引用类型属性共享问题
- 跟构造函数继承一样,可以向父级传递参数
那这个组合继承是不是完美的呢?答案是否定的,接下来一探究竟
function Parent(name) {
console.info(name)
this.name = name
}
function Child(name) {
Parent.call(this, name)
}
Child.prototype = new Parent() // undefined
Child.prototype.constructor = Child
var child1 = new Child('child1') // child1
console.info(child1) // Child { name: 'child1' }
console.info(Child.prototype) // Parent { name: undefined, constructor: [Function: Child] }
- 由于 new Parent 时会调用一次 Parent 函数,此时 name 为 undefined; 而在 Child构造函数里也会调用次 Parent 函数,并传入 child1 作为 name 参数
- Child.prototype 是指向 Parent 实例对象,这个实例的 name 为 undefined, constructor
重新设置为 Child 了 - 调用了两次父类的构造函数
- 子类实例中的属性和方法会屏蔽父类实例上的属性和方法,增加了不必要的内存