最新消息:XAMPP默认安装之后是很不安全的,我们只需要点击左方菜单的 "安全"选项,按照向导操作即可完成安全设置。

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

XAMPP案例 中文小张 828浏览 0评论

近来发现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. 创建构造函数的时候发生了什么?

  1. 在创建任何函数的时候,JavaScript都会创建一个原型对象,并且将函数的prototype属性指向该原型对象
  2. 原型对象会有一个constructor属性,指向该函数
  3. 而原型对象的__proto__属性,指向Object.prototype
  4. Object.prototype.constructor 指向 Object 函数
  5. 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进行实例化之后发生了什么?

  1. 基于构造函数创建了一个实例对象
  2. 该实例对象的__proto__属性指向构造函数的prototype属性
  3. 该实例对象的constructor指向构造函数 // todo: 这部分要放到图片里吗?
  4. 原型链其他部分未发生变化

 

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. 重新定义原型链的时候遇到的问题

 

// 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面向对象 – 定义对象

您必须 登录 才能发表评论!