【读好书 - 第 5 期】你知道几种创建对象的方法?

326 阅读7分钟

前言

这是我读《javascript高级程序设计》的第五篇分享。主要讲述了创建对象的几种方法。电子书和实体书的阅读体验很差,因为代码没有高亮。而且文章的内容比较多,读起来比较辛苦。这篇文章对文中的代码进行了编辑,并且对主要内容进行了提取。如果大家喜欢,就不要吝啬手中的赞哦。这是我继续分享的动力。

大纲视图

工厂模式

工厂模式抽象了创建具体对象的过程函数, 用函数来封装以特定接口创建对象的细节。

function createPerson (name, age, job) { 
    var o = new Object(); 
    o.name = name; 
    o.age = age; 
    o.job = job; 
    o.sayName = function () { 
        alert(this.name); 
    }; 
    return o; 
} 
var person1 = createPerson(" Nicholas", 29, "Software Engineer"); 
var person2 = createPerson(" Greg", 27, "Doctor");

工厂模式虽然解决了创建多个相似对象的问题, 但却没有解决对象识别的问题(即怎样知道一个对象的类型)。

简单,这种来说,你不知道对象是什么类型的对象,比如:你不知道对象是Array还是Date

为了解决这种问题,就出现了构造函数的方式创建对象。

构造函数方式

ObjectArray是原生构造函数。我们也可以创建构造函数,比如:

function Person (name, age, job) { 
    this.name = name; 
    this.age = age; 
    this.job = job; 
    this.sayName = function () {
        alert(this.name); 
    }; 
} 
var person1 = new Person(" Nicholas", 29, "Software Engineer"); 
var person2 = new Person(" Greg", 27, "Doctor");

我们在工作中,会经常用这种方法。这种方法有几个特点:

  • 不显示的声明一个函数
  • 属性和方法赋值给this对象
  • 没有return语句

如果要生成一个实例的话,会做以下几步:

1、创建一个新的对象 2、将构造函数的作用域给新的对象。因此,this指向新对象. 3、执行构造函数中的代码 4、返回一个新对象

优点:

创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是 构造函数模式胜过工厂模式的地方。

缺点:

就是每个方法都要在每个实例上重新创建一遍。

怎么理解这句话?我们知道,对象的引用类型,也就是指向一个堆空间。但是上面的方法,每定义一个函数,也就是实例化了一个对象

alert( person1. sayName == person2. sayName); //false

解决这个问题的方法就是原型模式

原型模式

大名鼎鼎的原型模式终于上场了。

我们创建的每个函数都有一个prototype(原型)属性, 这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

学术性的话总是那么难以理解。简单来说就是:

每个函数的prototype执向构造函数,每个实例都可以享用构造函数的方法和属性。

function Person () { }
Person.prototype.name = "Nicholas"; 
Person.prototype.age = 29; 
Person.prototype.job = "Software Engineer"; 
Person.prototype.sayName = function () { 
    alert(this.name); 
}; 
var person1 = new Person(); 
person1.sayName(); //"Nicholas" 
var person2 = new Person(); 
person2. sayName(); //"Nicholas" 
alert( person1. sayName == person2. sayName); //true 

person1和 person2访问的都是同一组属性和同一个sayName()函数。

要理解原型函数,需要理解以下几点知识:

  • 只要创建新的函数,就会生成prototype,这个属性指向原型对象。
  • 原型对象自动获得constructor
  • constructor指向原型对象

Person.prototype.constructor指向Person。

当用构造函数创建一个实例之后,这个实例内部会有一个指针[[prototype]]。它不可以直接访问,但是浏览器实现了一个__proto__的属性。可以访问构造函数的prototype

上面说了这么多,其实下面的一张图就可以完全的表述明白了。

原型链

说到这里,就一定会说到原型链的知识了。先看一个例子:

function Person () { } 
Person.prototype.name = "Nicholas"; 
Person.prototype.age = 29; 
Person.prototype.job = "Software Engineer"; 
Person.prototype.sayName = function () { 
    alert(this.name); 
}; 
var person1 = new Person(); 
var person2 = new Person(); 
person1.name = "Greg"; 
alert(person1.name); //"Greg"—— 来自 实例 
alert( person2. name); //"Nicholas"—— 来自 原型

这里不对原型链做过多的解释了,各种博客对原型链的解释太多了。

一句话基本概括大意:

当一个实例查找一个属性的时候,会先查找自身,如果没有回通过__prototype__向上查找,找到了就停止查找,没找到一直往上找,直到找不到该属性为止。

我怎么知道属性是自己的,还是原型的?

这个问题的解决方案就是hasOwnProperty()

用法如下:

alert( person1.hasOwnProperty("name")); //true

最后,每次写Person.prototype是不是很烦?这里有一个简单的方式:

function Person(){ } 
Person. prototype = { 
    name : "Nicholas", 
    age : 29, 
    job: "Software Engineer", 
    sayName : function () { 
        alert( this. name); 
    } 
};

原型对象有没有问题?有的。

1、 不能传递初始化的参数 2、如果原型对象中有引用类型的属性,每个实例的操作就会互相影响了。


function Person(){ } 
Person.prototype = { 
    constructor: Person, 
    name : "Nicholas", 
    age : 29, 
    job : "Software Engineer", 
    friends : ["Shelby", "Court"], 
    sayName : function () { 
        alert( this. name); 
    } 
}; 
var person1 = new Person(); 
var person2 = new Person(); 
person1.friends.push(" Van"); 
alert( person1. friends); //"Shelby, Court, Van" 
alert( person2. friends); //"Shelby, Court, Van"
alert( person1. friends === person2. friends); //true

这个问题已经显而易见了,通过person1去操作friends。person2同时发生了变化。

集两家之长: 组合使用构造函数模式和原型模式

function Person(name, age, job){ this.name = name; 
    this.age = age; 
    this.job = job; 
    this.friends = ["Shelby", "Court"]; 
} 
Person.prototype = { 
    constructor : Person, 
    sayName : function(){ 
        alert( this.name); 
    }
} 
var person1 = new Person(" Nicholas", 29, "Software Engineer"); 
var person2 = new Person(" Greg", 27, "Doctor"); 
person1.friends.push(" Van"); 
alert(person1.friends); //"Shelby, Count, Van" 
alert(person2.friends); //"Shelby, Count" 
alert(person1.friends === person2.friends); //false 
alert(person1.sayName === person2.sayName); //true

上面这种方式解决了之前说的所有问题了。但是这种方式,把构造函数和原型独立了,非常的怪异。动态原型就是致力于解决这个问题的。

动态原型

动态原型把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下), 又保持了同时使用构造函数和原型的优点。

function Person( name, age, job){ //属性 
    this.name = name; 
    this.age = age; 
    this.job = job; 
    //方法 
    if (typeof this.sayName != "function"){ 
        Person.prototype.sayName = function(){ 
            alert( this.name); 
        }; 
    } 
} 
var friend = new Person("Nicholas", 29, "Software Engineer"); 
friend.sayName(); // Nicholas

这种方式,已经非常完美了!

如果之前的方式都不合适。就可以使用寄生构造函数模式了。

寄生构造函数模式

这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但从表面上看,这个函数又很像是典型的构造函数。

function Person(name,age,job){
    var o=new Object();
    o.name=name;
    o.age=age;
    o.job=job;
    o.sayName=function(){
        alert(this.name);
    };
    return o;
} 
var friend = new Person("Nicholas", 29, "Software Engineer"); 
friend.sayName(); //"Nicholas"
  • Person 创建了一个对象
  • 用属性和方法初始化对象
  • 返回o这个对象

通过末尾return这个对象,可以重写调用构造函数的返回值。

打个比方:创建一个具有额外方法的特殊数组。

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");
alert(colors.toPipedString()); //"red|blue|green"

这种模式不能依赖instanceof操作符来确定对象类型,所以建议在可以使用其他模式的情况下,不要使用这种模式。

稳妥构造函数模式

稳妥构造函数模式没有公共属性,而且其方法也不引用this的对象。 稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用this和new),或者在防止数据被其他应用程序(如Mashup程序)改动时使用。 稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:

一、是新创建对象的实例方法不引用this; 二、是不使用new操作符调用构造函数。

按照稳妥构造函数的要求,可以将前面的Person构造函数重写如下。

function Person( name, age, job){ 
    //创建要返回的对象
    var o = new Object(); 
    //可以在这里定义私有变量和函数
    //添加方法 
    o.sayName = function(){ 
        alert( name); 
    }; 
    //返回对象
    return o; 
}

在以这种模式创建的对象中,除了使用sayName()方法之外,没有其他办法访问name的值。可以像下面使用稳妥的Person构造函数。

var friend = Person(" Nicholas", 29, "Software Engineer"); 
friend.sayName(); //"Nicholas"

这样,Person中就保存了一个稳妥对象。除了调用sayName方法,没有别的方式可以方位数据成员。

这种方式非常适合在某种安全执行环境。

今天的分享就到这里了,下一期将会主要介绍js中的继承。期待的话,点个关注哦!!