死磕JavaScript面向对象 - 定义对象

前言

近来发现JavaScript能做的事情越来越多,用的人也变多了,有太多的框架和模式都搞不清楚原理(早些年的prototype, yui, jQuery, 到后来的MVC,MVVM),所以想深入的学习一下JavaScript。

经过一段时间的摸索,终于知道了ECMAScript的来龙去脉,而我对JavaScript的认知还停留在ES3上面。但对于ES3,也只知道一些简单的用法,看高手写代码竟然完全看不懂。

最近几天卡在了闭包和对象两个章节上,因为和其他语言差的太多,所以看了很多文章仍然还只是一直半解,对于一个函数作为一等公民的面向对象语言,搞不清楚对象的基本知识实在有些说不过去,所以决定对相关内容进行一次深入的整理。

原始方式 1

1
2
3
4
5
6
7
8
9
// 1.js
var robot = new Object;
// var robot = {};
robot.height = '120cm';
robot.weight = '50kg';
robot.color = 'red';
robot.say = function (string) {
console.log(string);
}

用这种方式是不是感觉有点怪?感觉不像一个整体,所以我们要用下面的方法把对象封装起来:

原始方式 2

1
2
3
4
5
6
7
8
9
// 2.js
var robot = {
height : '120cm',
weight : '50kg',
color : 'blue',
say : function (string) {
console.log(string);
}
}

在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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 5.js
function say(string) {
// var say = function(string) {
console.log(string);
}
function Robot(height, weight, color) {
this.height = height + 'cm';
this.weight = weight + 'kg';
this.color = color;
this.say = say;
}
var robot1 = new Robot(100,50,'red');
var robot2 = new Robot(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
console.log(robot1.say === robot2.say); // true
console.log(robot1 instanceof Object); // true
console.log(robot1 instanceof Robot); // true

构造函数看上去更像其他语言中的面向对象,并且解决了问题1,可以识别对象的类型。

原型模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 6.js
function Robot() {
}
Robot.prototype.height = '120cm';
Robot.prototype.weight = '60kg';
Robot.prototype.color = 'blue';
Robot.prototype.parts = ['body'];
Robot.prototype.say = function(string) {
console.log(string);
}

var robot1 = new Robot();
var robot2 = new Robot();
console.log(robot1.height); // 120cm
console.log(robot2.height); // 120cm
console.log(robot1 instanceof Object); // true
console.log(robot1 instanceof Robot); // true
console.log(robot1.say === robot2.say); // true

这里可以看到,使用原型模式使得robot1和robot2公用了同一套属性值和方法。并且可以正确识别对象类型。

接下来我又尝试了对实例的属性进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
console.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
3
console.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. 创建构造函数的时候发生了什么?

  1. 在创建任何函数的时候,JavaScript都会创建一个原型对象,并且将函数的prototype属性指向该原型对象
  2. 原型对象会有一个constructor属性,指向该函数
  3. 而原型对象的__proto__属性,指向Object.prototype
  4. Object.prototype.constructor 指向 Object 函数
  5. Object.prototype是原型链最顶层,没有__proto__属性
1
2
3
4
5
6
7
8
9
10
11
12
// 7.js
function myFunc () {
}

console.log(myFunc.prototype); // {}
console.log(typeof myFunc.prototype); // object , 证明1
console.log(myFunc.prototype.constructor); // [Function: myFunc] ,证明2
console.log(myFunc.prototype.__proto__); // {}
console.log(myFunc.prototype.__proto__ === Object.prototype); // true,证明3
console.log(myFunc.prototype.__proto__.constructor); // [Function: Object],证明4
console.log(myFunc.prototype.__proto__.__proto__); // null,证明5

创建构造函数时的原型链

2. 使用new进行实例化之后发生了什么?

  1. 基于构造函数创建了一个实例对象
  2. 该实例对象的__proto__属性指向构造函数的prototype属性
  3. 该实例对象的constructor指向构造函数 // todo: 这部分要放到图片里吗?
  4. 原型链其他部分未发生变化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var myObj = new myFunc;

console.log(myObj instanceof myFunc); // true
console.log(myObj instanceof Object); // true
console.log(myObj.constructor); // [Function: myFunc]

console.log(myObj.__proto__ == myFunc.prototype); // true

console.log(myFunc.prototype); // {}
console.log(typeof myFunc.prototype); // object
console.log(myFunc.prototype.constructor); // [Function: myFunc]
console.log(myFunc.prototype.__proto__); // {}
console.log(myFunc.prototype.__proto__.constructor); // [Function: Object]
console.log(myFunc.prototype.__proto__ === Object.prototype); // true
console.log(myFunc.prototype.__proto__.__proto__); // null

实例化对象时的原型链

3. 属性/方法调用的内部流程是怎么样的?

  1. 在实例中找
  2. 如果没找到,去__proto__里找
  3. 如果没找到,去__proto__.__proto__里找
  4. 如果__proto__为null,说明已经到达了原型链的最顶级,也就是Object.prototype,属性/方法在原型链上未定义,返回undefined

4. 重新定义原型链的时候遇到的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
// 8.js
function myFunc() {
}

var myObj = new myFunc();

myFunc.prototype = {
say:function(string){
console.log(string);
}
};

myObj.say('hello world'); // TypeError: undefined is not a function

这看上去很难理解,因为重写构造方法的原型并不会删掉之前的原型,也不会改变实例和原型之间的关系,所以我们又发现了一个问题

  • 问题5:重新定义prototype会切断原型链,并且你会发现constructor也不见了。

重新定义原型前的原型链
重新定义原型后的原型链

所以如果想要完美的重写prototype需要这样:

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
// 9.js
function myFunc() {
}

var myObj = new myFunc;

myFunc.prototype = {
constructor : myFunc,
// __proto__ 会自动创建并指向 Object.prototype
say: function(string) {
console.log(string);
}
};

console.log(myObj instanceof myFunc); // false
console.log(myObj instanceof Object); // true

console.log(myFunc.prototype.__proto__ == myObj.__proto__.__proto__); //true

myObj.__proto__ = myFunc.prototype;

console.log(myObj instanceof myFunc); // true
console.log(myObj instanceof Object); // true

console.log(myFunc.prototype.__proto__ == myObj.__proto__.__proto__); //true

myObj.say('hello world');

除了上面这些问题,其实原型模式还有一个问题:

  • 问题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
23
// 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 12.js
function createRobot(height, weight, color) {
return {
height : height + 'cm',
weight : weight + 'kg',
color : color,
say : function (string) {
console.log(string);
}
}
}

var robot1 = new createRobot(100,50,'red');
var robot2 = new 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

W3School和《JavaScript 高级程序设计》中都提到了这样一种模式,而且还都说明了不推荐这样使用。这里让我很想不明白,从代码上看,和工厂模式没有区别,只是实例化的时候使用了new关键字,但是实例化的时候new并没有生效,个人理解这种方式的工作原理和工厂模式是一样的,所以也和工厂模式有着同样的问题。

(未完待续)