前言
近来发现JavaScript能做的事情越来越多,用的人也变多了,有太多的框架和模式都搞不清楚原理(早些年的prototype, yui, jQuery, 到后来的MVC,MVVM),所以想深入的学习一下JavaScript。
经过一段时间的摸索,终于知道了ECMAScript的来龙去脉,而我对JavaScript的认知还停留在ES3上面。但对于ES3,也只知道一些简单的用法,看高手写代码竟然完全看不懂。
最近几天卡在了闭包和对象两个章节上,因为和其他语言差的太多,所以看了很多文章仍然还只是一直半解,对于一个函数作为一等公民的面向对象语言,搞不清楚对象的基本知识实在有些说不过去,所以决定对相关内容进行一次深入的整理。
原始方式 1
1 |
// 1.js |
用这种方式是不是感觉有点怪?感觉不像一个整体,所以我们要用下面的方法把对象封装起来:
原始方式 2
1 |
// 2.js |
在JavaScript中,我们通常以以上两种方式创建对象,在实际开发过程中我们会遇到一些问题:
- 问题1: 没有办法识别对象的类型,因为我们创建的所有对象都是Object
- 问题2: 创建多个对象很麻烦
工厂模式
工厂模式用来解决代码重用的问题(问题2),可以用一个函数多次创建同一个结构的对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 3.js
function createRobot(height, weight, color) {
return {
height : height + 'cm',
weight : weight + 'kg',
color : color,
say : function (string) {
console.log(string);
}
}
}
var robot1 = createRobot(100,50,'red');
var robot2 = createRobot(120,60,'blue');
console.log(robot1.height,robot1.weight,robot1.color); // 100cm 50kg red
console.log(robot2.height,robot2.weight,robot2.color); // 120cm 60kg blue
这段代码看上去已经很好的解决了代码重用的问题,但是又产生了新问题:
- 问题3:多个同类型对象的方法被重复创建
这里指的就是robot1.say和robot2.say方法,本是相同的函数却被声明了两次,体现在代码里就是:
1
console.log(robot1.say === robot2.say); // false
为了解决这个问题,我们要把say方法拿到工厂外面单独进行定义。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// 4.js
function say(string) {
// var say = function(string) {
console.log(string);
}
function createRobot(height, weight, color) {
return {
height : height + 'cm',
weight : weight + 'kg',
color : color,
say : say
}
}
var robot1 = createRobot(100,50,'red');
var robot2 = createRobot(120,60,'blue');
console.log(robot1.say === robot2.say); // true
console.log(robot1 instanceof Object); // true
console.log(robot1 instanceof createRobot); // false
这样我们就解决了问题3,say方法只被声明了一次,所有robot对象的say方法都指向同一个函数。
构造函数
1 |
// 5.js |
构造函数看上去更像其他语言中的面向对象,并且解决了问题1,可以识别对象的类型。
原型模式
1 |
// 6.js |
这里可以看到,使用原型模式使得robot1和robot2公用了同一套属性值和方法。并且可以正确识别对象类型。
接下来我又尝试了对实例的属性进行修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15console.log(robot1.hasOwnProperty('height')); // false
console.log(robot2.hasOwnProperty('height')); // false
robot1.height = '100cm';
console.log(robot1.height); // 100cm
console.log(robot2.height); // 120cm
console.log(robot1.hasOwnProperty('height')); // true
console.log(robot2.hasOwnProperty('height')); // false
robot1.say = function(string) {
console.log('Ah...' + string);
};
robot1.say('hello world'); // Ah...hello world
robot2.say('hello world'); // hello world
delete robot1.say;
robot1.say('hello world'); // hello world
robot2.say('hello world'); // hello world
这段代码说明了,实例上定义的属性/方法会屏蔽掉原型上的同名属性/方法,而在delete掉实例上的方法后,原型上的方法在实例上又恢复可用了。但是我遇到了一个例外:
1
2
3console.log(robot1.parts, robot2.parts); // [ 'body' ] [ 'body' ]
robot1.parts.push('arm');
console.log(robot1.parts, robot2.parts); // [ 'body', 'arm' ] [ 'body', 'arm' ]
对robot1属性的修改同时影响了robot2, 这说明对于数组的修改是发生在原型上的,这并不是我想要得到的效果。
- 问题4: 对于原型上数组属性的修改影响到了其他实例
《JavaScript 高级程序设计》对原型链进行了详细的解读,当然上面这个问题就是这本书里讲到的,并且还讲了下面一些内容:
1. 创建构造函数的时候发生了什么?
- 在创建任何函数的时候,JavaScript都会创建一个原型对象,并且将函数的prototype属性指向该原型对象
- 原型对象会有一个constructor属性,指向该函数
- 而原型对象的__proto__属性,指向Object.prototype
- Object.prototype.constructor 指向 Object 函数
- Object.prototype是原型链最顶层,没有__proto__属性
1 |
// 7.js |
2. 使用new进行实例化之后发生了什么?
- 基于构造函数创建了一个实例对象
- 该实例对象的__proto__属性指向构造函数的prototype属性
- 该实例对象的constructor指向构造函数 // todo: 这部分要放到图片里吗?
- 原型链其他部分未发生变化
1 |
var myObj = new myFunc; |
3. 属性/方法调用的内部流程是怎么样的?
- 在实例中找
- 如果没找到,去__proto__里找
- 如果没找到,去__proto__.__proto__里找
- 如果__proto__为null,说明已经到达了原型链的最顶级,也就是Object.prototype,属性/方法在原型链上未定义,返回undefined
4. 重新定义原型链的时候遇到的问题
1 |
// 8.js |
这看上去很难理解,因为重写构造方法的原型并不会删掉之前的原型,也不会改变实例和原型之间的关系,所以我们又发现了一个问题
- 问题5:重新定义prototype会切断原型链,并且你会发现constructor也不见了。
所以如果想要完美的重写prototype需要这样:
1 |
// 9.js |
除了上面这些问题,其实原型模式还有一个问题:
- 问题6: 原型模式的构造函数没有参数
混合构造函数&原型模式
为了解决问题4和问题6,就出现和混合构造函数&原型模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30// 10.js
function Robot(height, weight, color) { // 问题6解决
this.height = height + 'cm';
this.weight = weight + 'kg';
this.color = color;
this.parts = ['body'];
}
Robot.prototype.say = function(string) {
console.log(string);
}
var robot1 = new Robot(100,50,'red');
var robot2 = new Robot(120,60,'blue');
console.log(robot1);
/*
{ height: '100cm',
weight: '50kg',
color: 'red',
parts: [ 'body' ] }
*/
console.log(robot2);
/*
{ height: '120cm',
weight: '60kg',
color: 'blue',
parts: [ 'body' ] }
*/
robot1.parts.push('arm');
console.log(robot1.parts,robot2.parts); // [ 'body', 'arm' ] [ 'body' ], 问题4解决
动态原型模式
为了让代码看上去更美观一点儿,又有了动态原型模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// 11.js
function Robot(height, weight, color) {
this.height = height + 'cm';
this.weight = weight + 'kg';
this.color = color;
this.parts = ['body'];
if (Robot.prototype.say === undefined) {
// if (Robot.say === undefined) { // 这样不行, 会重复执行,why?
// if (this.prototype.say === undefined) { // 这样也不行, TypeError: Cannot read property 'say' of undefined,why?
Robot.prototype.say = function(string) {
console.log(string);
}
}
}
var robot1 = new Robot(100,50,'red');
var robot2 = new Robot(120,60,'blue');
console.log(robot1.say === robot2.say);
robot1.say('hello world');
robot2.say('hello world');
混合工厂模式/寄生构造函数模式
1 |
// 12.js |
W3School和《JavaScript 高级程序设计》中都提到了这样一种模式,而且还都说明了不推荐这样使用。这里让我很想不明白,从代码上看,和工厂模式没有区别,只是实例化的时候使用了new关键字,但是实例化的时候new并没有生效,个人理解这种方式的工作原理和工厂模式是一样的,所以也和工厂模式有着同样的问题。
(未完待续)