js基础:对象创建和继承的几种方式

最近看网上各种文章, 头异常的大。 刚好看完红宝书,于是整理了一下。

1. 工厂模式

单纯地创建对象, 使用 Object 构造函数, 或者字面量表示都 ok, 但是, 会产生大量重复代码, 每次都要写 new Object() 等等,
工厂模式可以解决这个问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createPerson(name, job) {
// 创建一个Object
var o = new Object()
o.name = name
o.job = job
// 口吐芬芳
o.sayFword = function () {
alert('Funny')
}
return o
}
// 创建实例
var person1 = createPerson('Tony Stark', 'Iron Man')
var person2 = createPerson('进喜 王', '铁人')

工厂模式解决了创建多个相似对象的问题, 但是没有解决对象识别问题, 所有实例类型均为 object.
为了解决这个问题, 群众想出了构造函数模式

2. 构造函数模式

1
2
3
4
5
6
7
8
9
10
11
function Person(name, job) {
this.name = name
this.job = job
// 口吐芬芳
this.sayFword = function () {
alert('Funny')
}
}
// 创建实例
var person1 = new Person('Tony Stark', 'Iron Man')
var person2 = new Person('王进喜', '铁人')

这里, Person 取代了 createPerson 函数, 而且, 有几个不同:

  • 没有显式创建对象
  • 直接将属性, 方法赋给了 this
  • 没有显式 return
  • 函数名以大写字母开头

这个时候, 就可以识别对象类型了

1
2
3
4
5
person1.constructor === Person //true
person2.constructor === Person //true

person1 instanceof Person //true
person2 instanceof Person //true

如果不用 new 会有什么后果 ?
后果是, this 指向全局对象, 浏览器中, 传入的属性和方法都挂到 window 对象了

1
2
3
var person3 = Person('王进喜', '铁人')
window.name //'王进喜'
window.job //'铁人'

我们可以用 call 或 apply, 将 this 指向实例, 但这样标识不了类型, 构造函数模式便失去了意义

1
2
3
4
var person4 = new Object()
Person.call(person4, '王进喜', '铁人')
person4.name //'王进喜'
person4 instanceof Person //false

当然, 除非手动将proto指向 Person.prototype, 这是后话

1
2
person4.__proto__ = Person.prototype
person4 instanceof Person // true

构造函数模式有一个问题 —— 每个方法都要在每个实例上创建一遍, 也就是说, 不同函数的同名方法实际上是不相等的

1
2
// person1下的sayFword 和 person2下的sayFword 是两个不同的方法
person1.sayFword === person2.sayFword //false

而我们想要让某一些方法成为该类型的所有实例共用的, 而不是每创建一个实例就去创建一个同样的方法/属性
于是, 群众又想出了下一个模式, 原型模式

3. 原型模式

原型模式的思想是, 给构造函数 (实际上只要是函数) 赋予一个 prototype 属性 , 这个属性的值是一个指针, 指向一个对象, 而这个对象包含其所有实例共享的属性和方法

1
2
3
4
5
6
7
8
9
10
11
function Person() {}
Person.prototype.name = 'Tony Stark'
Person.prototype.job = 'Iron Man'
// 口吐芬芳
Person.prototype.sayFword = function () {
alert('Funny')
}
// 创建实例
var person1 = new Person()
var person2 = new Person()
person1.sayName === person2.sayName // true

默认情况下 prototype 会自动获得 constructor 属性, 该属性指向构造函数本身

1
Person.prototype.constructor === Person // true

如果实例中定义了跟 prototype 重名的属性, 会怎么样呢

1
2
3
4
var person3 = new Person()
person3.name = '灭霸'
console.log(person3.name) //'灭霸'
console.log(Person.prototype.name) // 'Tony Stark'

很明显, 原型上的同名属性并不会受到影响
我们可以用 delete 操作符删除对实例的属性, 从而重新访问原型中的属性

1
2
delete person3.name
console.log(person3.name) // 'Tony Stark'

需要注意的是, 使用 in 操作符, 即使属性是在原型上, 也会返回 true (for in 亦是如此)

1
'job' in person3 //true

单纯的原型模式, 也是有缺点的, 例如

1
2
3
4
5
6
7
8
9
function Person() {}
Person.prototype = {
friends: [1, 2],
}
// 创建实例
var person1 = new Person()
var person2 = new Person()
person1.friends.push(3)
console.log(person2.friends) // 1,2,3

如果共享的属性为引用类型, 修改实例该属性时, 会影响到原型上的该属性, 从而影响到其他实例
这个问题可以用 组合构造函数和原型模式 来解决

4. 组合构造函数和原型模式

构造函数模式用于定义实例的属性, 而原型模式用于定义方法和共享属性 , 集两家之所长

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person(name, job, friends) {
this.name = name
this.job = job
this.friends = friends
}
// 口吐芬芳
Person.prototype.sayFword = function () {
alert('Funny')
}
// 创建实例
var person1 = new Person('Tony Stark', 'Iron Man', [1, 2])
var person2 = new Person('王进喜', '铁人', [3, 4])
person1.sayFword === person2.sayFword // true
person1.friends.push(3)
console.log(person2.friends) // 3, 4

5. 动态原型模式

这种模式多了一层判断

1
2
3
4
5
6
7
8
9
function Person(name, age) {
this.name = name
this.age = age
if (typeof this.sayFword != 'function') {
Person.prototype.sayFword = function () {
alert(this.name)
}
}
}

那么, 这样做的优点在哪呢 ?

想想看, 如果没有 if 的话,每 new 一次,都会重新定义一个新的函数,然后挂到 Person.prototype.sayFword 属性上, 这就造成没必要的时间和空间浪费 。加上 if 后,只在 new 第一个实例时才会定义 sayFword 方法,之后就不会了 。
并且 ————
if语句检查的可以是初始化之后应该存在的任何属性或方法 —— 不必用一大堆if语句检查每个属性和每个方法,只要检查其中一个即可。
———— 什么意思呢 ?
是这样的:假设除了 sayFword 方法外,还定义了其他方法,比如 sayHi、sayBye 等等。此时只需要把它们都放到对 sayFword 判断的 if 块里面就可以了

1
2
3
4
5
6
if (typeof this.sayFword != "function") {
Person.prototype.sayFword = function() {...};
Person.prototype.sayHi = function() {...};
Person.prototype.sayBye = function() {...};
...
}

这样一来,要么它们全都还没有定义(new 第一个实例时),要么已经全都定义了(new 其他实例后),即它们的存在性是一致的,用同一个判断就可以了,而不需要分别对它们进行判断。

6. 寄生构造函数模式

1
2
3
4
5
6
7
8
9
10
function SpecialArray() {
var values = new Array()
// 结构 arguments 拼接数组 ,在es6中不用这样写, 直接 value.push(...arguments)
values.push.apply(values, arguments)
// 此方法 将数组用|分隔拼接成字符串返回
values.toPipedString = function () {
return this.join('|')
}
return values
}

虽然它写的和工厂模式一样,但是创建时用了 new,因此使得实现的过程不一样(但是实现过程不重要)
作用嘛, 比如创建具有额外方法的已有类型(如数组,Date 类型等,但是又想不污染原有的类型 ;
例如上面如果直接在 Array 中定义新的方法,会污染其它的数组对象, 于是创建了一个 SpecialArray 类型, 它包含了 Array 的所有方法, 又有自己的方法 toPipedString ;

7. 稳妥构造函数模式

稳妥对象指没有公共属性,而且其他方法也不引用 this 的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用 this 和 new),或者防止数据被其他应用程序改动时使用 .

1
2
3
4
5
6
7
8
9
10
11
function Persion(name, age, job) {
// 创建要返回的对象
var o = new Object()
// 添加方法
o.sayName = function () {
alert(name)
}
return o
}
var p1 = Persion('bill', 23, 'FE')
p1.sayName() // bill;

除了了调用 sayName()外,没有别的方式可以访问其他数据成员。