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