重拾JS——创建对象

创建对象,刚开始我觉得是一件非常简单的事情,就一行代码 var person = {...}。然而,在我重头学习创建对象后,我发现事情并没有想象中的那么简单。

创建对象,不只是那一行代码那么简单,他还有好几种模式,而且,各种模式之间那种层层递进,不断迭代的关系,让我觉得妙不可言。

创建 Object 实例

我们知道,在 JS 中,所有对象都是 Object 的实例。因此,创建对象其实就是创建 Object 实例

创建 Object 实例,有两种方法:

一种是 使用 new 操作符后接 Object 构造函数:

var person = new Object();
person.name = 'zhang3';
person.age = 29;

一种是对象字面量表示法:开发人员更加青睐这种,简洁:

var person = {
  name : "zhang3",
  age : 29 
};

上面两种方式在创建单个对象的时候,问题不大。但是如果创建很多个相似的对象的时候,会产生大量重复代码:

var person1 = {...};
var person2 = {...};
var person3 = {...};
...

工厂模式

为了解决对象字面量在创建多个相似对象,会产生大量重复代码的问题,人们开始使用下面这样的函数,我们称之为工厂模式:

function createPerson(name, age){
  var o = new Object();
  o.name = name;
  o.age = age;
  o.sayName = function() {
    console.log(this.name); // 注意,这里使用的是 this.name
  }; 
  return o;
}
var person1 = createPerson("zhang3", 29); 
var person2 = createPerson("li4", 27);

优点:解决了创建多个相似对象的问题。

不足:无法解决对象识别的问题。(就是说没有类型,只知道他是个对象,不知道它是谁谁谁的实例)

构造函数模式

原生的构造函数,如 Object、Array,我们可以使用这些构造函数创建对象。其实,我们也可以创建自定义构造函数,从而自定义对象类型的属性和方法:

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.sayName = function(){
    console.log(this.name);
  };
}

var person1 = new Person("zhang3", 29); 
var person2 = new Person("li4", 27);

与工厂模式的不同:

  • 首字母大写
  • 没有显式的创建一个对象
  • 直接将属性和方法赋值给this
  • 没有 return 语句
  • 使用 new ,而不是调用

之所以有这些不同,依然能达到相同的功能,主要是 new 操作符的作用:

  • 创建一个对象 var person1 = new Object();
  • 将构造函数的作用域赋值给新对象 person1.__proto__ = Person.prototype;
  • 执行构造函数的代码 Person.call(person1);
  • 返回新对象 return person1

优点:可以将实例标识为特定类型。

不足:每个方法要在每个实例上都要创建一遍,方法应该是共享的。

改进:将函数挪到构造函数外面,如下:

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.sayName = sayName;
}

function sayName(){
  console.log(this.name);
}

var person1 = new Person("Nicholas", 29);
var person2 = new Person("Greg", 27);

依然不足:其一,全局作用域定义的函数只能被某个对象调用;其二,如果这样的函数有好些个,都写在外面,没有封装性。

原型模式

每个函数都有一个 prototype 的属性,它是一个指针,指向函数的原型对象。

函数的原型对象的用途是包含所有实例共享的属性和方法

因此,我们可以不在构造函数中定义对象实例的信息,而是将这些信息添加到函数的原型对象中:

function Person(){};

Person.prototype.name = "zhang3";
Person.prototype.age = 29;
Person.prototype.sayName = function(){
  console.log(this.name)
};

var person1 = new Person();
person1.sayName(); //"zhang3"

var person2 = new Person();
person2.sayName(); //"zhang3"

person1.sayName == person2.sayName; // true

可以看到 person1 与 person2 的 sayName 方法,访问的是同一个函数。

原型对象有个 constractor 属性,指向构造函数, 构造函数,原型对象,实例之间的关系如下图:

简写:函数的 prototype 属性,指向的是一个原型对象,原型对象也是对象,因此,可以直接将一个对象赋值给 prototype,不用每次都写 Person.prototype.x = xxx 了:

function Person(){}

Person.prototype = {
  name : "zhang3",
  age : 29,
  sayName : function () {
    console.log(this.name)
  }
};

问题:每创建一个函数,就会同时创建它的 prototype 对象,这个对象会自动获得 constractor 属性。而简写的方式,相当于是重写了原型对象,就不会默认有 constractor属性了,因此需要手动将它指定回去。

手动指定

function Person(){}
Person.prototype = {
  constructor : Person,
  name : "Nicholas",
  age : 29,
  sayName : function () {...}
};

注意 constructor 的 [[Enumerable]] 会被设置为 true ,默认情况下 这个属性是不可枚举的。

function Person(){}
Person.prototype = {
  name : "Nicholas",
  age : 29,
  sayName : function () {...}
};

Object.defineProperty(Person.prototype, "constructor", {
  enumerable: false,
  value: Person
});

不足:其一,每个实例都取得相同的属性值,实际情况中,我们是需要自定义每个实例的属性的,这一点可以通过传参解决。其二,最大的问题是,引用类型的共享。

function Person(){}
Person.prototype = {
  constructor : Person,
  name : "zhang3",
  age : 29,
  friends : ["aa", "bb"],
  sayName : function () {...}
};

var person1 = new Person();
var person2 = new Person();

person1.friends.push("cc");

console.log(person1.friends); // "aa,bb,cc" 
console.log(person2.friends); // "aa,bb,cc" 
console.log(person1.friends === person2.friends); // true

组合 构造函数模式 和 原型模式

原型模式的不足之处在于无法传参,可以通过构造函数解决;

构造函数的不足之处在于方法的重复定义,可以通过原型模式解决;

于是,可以将这两种模式组合起来使用,既满足传参,自定义实例属性,又满足方法共享:

function Person(name, age){
  this.name = name;
  this.age = age;
  this.friends = ["aa", "bb"];
}
Person.prototype = {
  constructor : Person,
  sayName : function () {...}
};

var person1 = new Person('Nicholas', 29,);
var person2 = new Person('Greg', 27);

person1.friends.push("cc");

console.log(person1.friends); // "aa,bb,cc" 
console.log(person2.friends); // "aa,bb" 
console.log(person1.friends === person2.friends); // false

组合构造函数模式和原型模式,是目前 JS 中使用最广泛,认同度最高的一种创建自定义对象的方法。

到这,如何创建对象的几种模式应该差不多了。其实,还有三种模式,个人感觉可能只是使用场景的不同,才需要使用到他们,下面一起接着来看看。

动态原型模式

个人感觉这种模式,只是将属性定义和方法定义写在了一起而已。(如果你知道它的妙用,欢迎留言告诉我)

function Person(name, age) {
  this.name = name;
  this.age = age;

  if(typeof this.sayName != 'function') { // 只需要检测其中一个方法
    Person.prototype.sayName = function() {
      console.log(this.name)
    }
    // 后面还可以定义其他的方法
  }
}

寄生构造函数模式

这种模式的基本思想是:创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后返回新创建的对象。

function Person(name, age) {
  var o = new Object();
  o.name = name;
  o.age = age;
  o.sayName = function(){...};
  return o;
}

var friend = new Person("zhang3", 29);
friend.sayName(); //"zhang3"

Person 函数来看,发现,跟工厂模式是一模一样的。只不过是使用了 new 操作符调用 Person。前面说过,new 操作符会返回一个新对象,而 Person 函数末尾的 retrun 会覆盖 new 的返回值。

用途一:可以扩展已有的构造函数

function SpecialArray(){
  var values = new Array();
  values.push.apply(values, arguments);
  values.toPipedString = function() { 
    return this.join("|"); 
  };
  return values;
}

var colors = new SpecialArray("red", "blue", "green"); 
console.log(colors.toPipedString()); // "red|blue|green"

疑问:上面如果不使用 new 操作符,直接调用,好像也能达到类似的效果:

var colors2 = SpecialArray("red", "blue", "green");
console.log(colors2.toPipedString()); //"red|blue|green"

那它跟工厂模式有啥区别?这点我比较迷惑,期待大家留言,让我学习下。

而且这种模式创建出来的实例的原型并不是 SpecialArray,可以通过 instanceof 来检验,所以书上也建议说如果有其他模式可以使用,最好不要使用这种模式。

稳妥的构造函数模式

所谓稳妥对象,就是没用公共属性,其方法也不引用实用 this。适合在一些对安全要求比较高的环境中。

function Person(name, age) {
  var o = new Object();
  ... // 定义私有变量和函数,没有公用属性
  
  o.sayName = function() {
    // 这里与工厂模式区别,不使用 this
    console.log(name) 
  }; 
  return o;
}
var friend = Person("Nicholas", 29);

作用域安全的构造函数

我们知道,构造函数也是函数,也能直接调用。只不过,在没了 new 操作符后,直接调用,函数中的 this 很有可能是全局环境(例如 window),因此,有可能将函数中的属性和方法定义到全局环境中,可能造成污染。

因此,可以像下面这样,先判断 this 是不是构造函数的实例,这样就锁定了可以调用构造函数的作用域了。

function Person(name, age) {
  if (this instanceof Person) {
    this.name = name;
    this.age = age;
  } else { 
    return new Person(name, age,);
  }
}
阅读 438

推荐阅读