细说 JavaScript 创建对象总结(中)- 关系错乱的原型模式

阅读 161
收藏 8
2017-04-22
原文链接:www.imooc.com

我们知道构造函数模式虽然解决了工厂模式无法识别对象的问题,但构造模式却也画蛇添足--创建了两个函数来解决同一个问题。

这时候就轮到原型模式登场了。

1.了解一下原型对象

我们知道我们创建一个函数(函数也是对象),而该函数就会拥有一个prototype属性,而该属性是一个指针,指向该对象的原型对象;并且该原型对象会获得一个constructor属性和分享的属性跟方法,并且该属性也是一个指针,指向prototype属性所在对象

我们来举个栗子:

// 记录姓名
function Person() {}
Person.prototype.name = "xiaoming";
Person.prototype.getName = function() {
    console.log(this.name);
}

var person1 = new Person();
person1.getName();    //    xiaoming

var person2 = new Person();
person2.getName();    // xiaoming

我们来看下他们之间的关系图
图片描述

而这就是文字解释了

在我们定义完构造函数后,此时该构造函数的原型对象默认只有prototype属性,而拥有的方法也是从Object继承而来(如toString()方法、valueOf()方法)。而后我们使用new操作符调用该构造函数,则会创建一个实例,而该实例也有一个属性proto,该属性也是一个指针,指向该构造函数的原型对象

我们在这说下默认的原型

刚刚我们提及在没有使用new操作符创建实例之前,构造函数原型对象上拥有的方法是从Object继承而来。从这可以得出所有函数的原型对象都是Object的实例,并且函数的原型对象中都包含一个指针指向Object.prototype

若此时我们记录人的姓名一个叫小明、一个叫小红那要怎么办?不怕我们有办法。

// 记录姓名
function Person() {}
Person.prototype.name = "xiaoming";
Person.prototype.getName = function() {
    console.log(this.name);    // this 指向实例,即是调用对象
}

var person1 = new Person();
person1.name = "xiaohong";    // 修改person1实例上name属性值
person1.getName();    // xiaohong

var person2 = new Person();
person2.getName();    // xiaoming

上面例子中我们添加了一条语句person1.name这实际上是修改person1实例上的name属性,而纳闷person2.getName()却依旧打印xiaoming。

这就是其中搜索机制的原因了。(优先搜索实例

在执行person1.getName()时,
解析器问:person1实例有name属性吗?答:有。
那就返回了person1实例上的name属性。

而执行person2.getName()时,
解析器问:person2实例有name属性吗?答:没有。
解析器再问:person2的原型对象有name属性吗?答:有。
那就返回了person2原型对象的name属性。

上图:
图片描述

此时我又回心转意了,两个人都要叫做小明(作死),在上面方法的基础上我们要怎么做?不怕,我们有方法。

// 记录姓名
// 方法1
function Person() {}
Person.prototype.name = "xiaoming";
Person.prototype.getName = function() {
    console.log(this.name);    // this 指向实例,即是调用对象
}

var person1 = new Person();
person1.name = "xiaohong";    // 修改person1实例上name属性值
person1.getName();    // xiaohong

var person2 = new Person();
person2.getName();    // xiaoming

delete person1.name;    // 使用delete操作符删除实例的属性
person1.getName();    // xiaoming

// 方法2(推荐此方法)
// 此方法为组合使用构造函数模式和原型模式
function Person() {
    // 在构造函数中定义实例属性
    this.name = "xiaoming";
}

Person.prototype.getName = function() {
    console.log(this.name);    // this 指向实例,即是调用对象
}

var person1 = new Person();
person1.getName();    //    xiaoming

var person2 = new Person();
person2.getName();    // xiaoming

方法二关系图
图片描述

经过上面的记录名字的例子,这时候我们就要知道这个属性跟方法是属于实例还是原型?

2.确认属性和方法的归属

①:hasOwnProperty()方法:检测一个属性存在于实例还是原型中,并返回布尔值。
true为实例中,false为原型中。
②:in操作符:若属性能通过对象访问到则返回true,不管属性在实例中还是原型中。

使用上面方法跟操作符我们就可以定义一个方法来检测属性跟方法的归属

function hasprototypeProperty(object, name) {
    return ! object.hasOwnProperty(name) && (name in object);
    // true: 原型中, false: 实例中
}
3.原型书写小福利

在上面的例子中我们总是写Person.prototype。这样非常麻烦,现在有了简洁的写法。

function Person() {}

Person.prototype = {
    // 使用简洁写法必须加上这句,保证constructor属性指向正确
    constructor: Person,    
    name: "xiaoming",
    getName: function() {
        console.log(this.name);
    }
}

但这种写法带来的是问题是使得constructor属性可枚举,若我们在遍历对象属性时不想遍历该属性,我们可以采取以下方法。

function Person() {}

Person.prototype = {
    // 使用简洁写法必须加上这句,保证constructor属性指向正确
    constructor: Person,    
    name: "xiaoming",
    getName: function() {
        console.log(this.name);
    }
}

// 使用Object.defineProperty(),语法请自行百度
 Object.defineProperty(Person.prototype, "constructor", {
  enumerable: false,
  value: Person
 })

// 遍历属性
for (var key in Person.prototype) {
  console.log(key);
 }

以上我们都在说我们自己定义的对象的原型对象,那我们的原生对象(如Array,Function)有原型对象吗?

4.原生对象的原型对象

假如现在我们有个需求要给数组对象添加个计算数组中各项出现的次数,那我们现在就可以利用给原生对象的原型对象添加方法来解决这个问题。

Array.prototype.countRepeat = function(arr) {
    var list = {};
    for(var i = 0; i < arr.length; i++) {
      if (! list [arr[i]]) {
        list[arr[i]] = 0;
      }
      list[arr[i]]++;
    }

    console.log(list);
}

var arr = [11, 12, 12, "name", 11, 14, 13,12];
arr.countRepeat(arr);    // Object {11: 2, 12: 3, 13: 1, 14: 1, name: 1}

但是给原生对象的原型对象直接添加方法会被继承,之后定义的所有数组对象的原型对象上都一个countRepeat()这个方法,也就是说所有的数组对象都可以使用该方法,但有时我们又只是想让某个数组拥有该方法而已(又作死),那又怎么办,不怕,我们又办法。

// 寄生构造函数模式
function addArrMethod(arr) {  

  arr.countRepeat = function() {

    var list = {};
    // this指向arr
    for(var i = 0; i < this.length; i++) {
      if (! list [this[i]]) {
        list[this[i]] = 0;
      }
      list[this[i]]++;
    }

    return list ;
  }
  return arr;
}
var arr = [11, 12, 12, "name", 11, 14, 13,12];

var result = new addArrMethod(arr);
result.countRepeat();    // Object {11: 2, 12: 3, 13: 1, 14: 1, name: 1}

最后就是最重要的环节了(又一个坑)

5.原型对象的问题

记得上面我们记录小明小红名字的例子呢?忘记了?没事。

// 记录姓名
function Person() {}
Person.prototype.name = "xiaoming";
Person.prototype.getName = function() {
    console.log(this.name);    // this 指向实例,即是调用对象
}

var person1 = new Person();
person1.name = "xiaohong";    // 修改person1实例上name属性值
person1.getName();    // xiaohong

var person2 = new Person();
person2.getName();    // xiaoming

这个例子中person2实例顺利的取得了原本保存在原型对象中的name的属性值,这也是因为name属性值为基本数据类型,若我们把修改实例属性的属性值变为引用类型的话,坑就出现了。

// 记录姓名以及朋友
function Person() {}
Person.prototype = {
    constructor: Person,
    name: "xiaoming",
    friends: ["xiaohong", "xiaoli"]
}

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

person1.friends.push("xiaohuang");

console.log(person1.friends);    // ["xiaohong", "xiaoli", "xiaohuang"]
console.log(person2.friends);    // ["xiaohong", "xiaoli", "xiaohuang"]
console.log(person1.friends === person2.friends);    // true

而造成这种原因就是friends数组存在于原型对象中,而非在实例中,所以person1.friend和person2.friend引用的是同一个数组,所以造成了上面的结果,那解决办法不就显而易见了吗?那就是把属性定义在实例中(也就是在构造函数中写属性),把方法定义在原型对象中,而这种模式我们也提到过了,那就是组合使用构造函数和原型模式(最推荐被用于创建自定义方法的模式)。

function Person() {
    this.name = "xiaoming";
    this.friends = ["xiaohong", "xiaoli"];
}
Person.prototype = {
    constructor: Person
   getName: function() {
        console.log(this.name);
    }
}

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

person1.friends.push("xiaohuang");

console.log(person1.friends);    // ["xiaohong", "xiaoli", "xiaohuang"]
console.log(person2.friends);    // ["xiaohong", "xiaoli"]
console.log(person1.friends === person2.friends);    // false

本文对你有帮助?欢迎扫码加入前端学习小组微信群:

评论