近来发现JavaScript能做的事情越来越多,用的人也变多了,有太多的框架和模式都搞不清楚原理(早些年的prototype, yui, jQuery, 到后来的MVC,MVVM),所以想深入的学习一下JavaScript。
经过一段时间的摸索,终于知道了ECMAScript的来龙去脉,而我对JavaScript的认知还停留在ES3上面。但对于ES3,也只知道一些简单的用法,看高手写代码竟然完全看不懂。
最近几天卡在了闭包和对象两个章节上,因为和其他语言差的太多,所以看了很多文章仍然还只是一直半解,对于一个函数作为一等公民的面向对象语言,搞不清楚对象的基本知识实在有些说不过去,所以决定对相关内容进行一次深入的整理。
原始方式 1
// 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
// 2.js var robot = { height : '120cm', weight : '50kg', color : 'blue', say : function (string) { console.log(string); } } |
在JavaScript中,我们通常以以上两种方式创建对象,在实际开发过程中我们会遇到一些问题:
- 问题1: 没有办法识别对象的类型,因为我们创建的所有对象都是Object
- 问题2: 创建多个对象很麻烦
工厂模式
工厂模式用来解决代码重用的问题(问题2),可以用一个函数多次创建同一个结构的对象。
// 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方法,本是相同的函数却被声明了两次,体现在代码里就是:
console.log(robot1.say === robot2.say); // false |
为了解决这个问题,我们要把say方法拿到工厂外面单独进行定义。
// 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方法都指向同一个函数。
构造函数
// 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,可以识别对象的类型。
原型模式
// 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公用了同一套属性值和方法。并且可以正确识别对象类型。
接下来我又尝试了对实例的属性进行修改
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掉实例上的方法后,原型上的方法在实例上又恢复可用了。但是我遇到了一个例外:
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__属性
// 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进行实例化之后发生了什么?
- 基于构造函数创建了一个实例对象
- 该实例对象的__proto__属性指向构造函数的prototype属性
- 该实例对象的constructor指向构造函数 // todo: 这部分要放到图片里吗?
- 原型链其他部分未发生变化
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. 属性/方法调用的内部流程是怎么样的?
- 在实例中找
- 如果没找到,去__proto__里找
- 如果没找到,去__proto__.__proto__里找
- 如果__proto__为null,说明已经到达了原型链的最顶级,也就是Object.prototype,属性/方法在原型链上未定义,返回undefined
4. 重新定义原型链的时候遇到的问题
// 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需要这样:
// 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,就出现和混合构造函数&原型模式
// 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解决 |
动态原型模式
为了让代码看上去更美观一点儿,又有了动态原型模式:
// 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'); |
混合工厂模式/寄生构造函数模式
// 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并没有生效,个人理解这种方式的工作原理和工厂模式是一样的,所以也和工厂模式有着同样的问题。
转载请注明:XAMPP中文组官网 » 死磕JavaScript面向对象 – 定义对象