Javascript-创建对象

我们知道:Object 构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。
比如,我想创建两个person对象。包含不同的信息。用Object构造函数的方法,代码如下:

var person1 = new Object();
person1.name = "Tom";
person1.age = 23;
person1.job = "Player";
person1.sayName = function(){
    console.log(this.name);
}
var person2 = new Object();
perosn2.name = "Jerry";
person2.age = 22;
person2.job = "Doctor";
person2.sayName = function(){
    console.log(this.name);
}

以上代码是不是有很多相似又重复的地方。
解决办法:工厂模式

一、工厂模式

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

function createPerson(name, age, job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function() {
        console.log(this.name);
    };
    return o;
}
var person1 = createPerson("Tom", 23, "Player");
var person2 = createPerson("Jerry", 22, "Doctor");

优点:解决了创建很多个相似对象的问题。
缺点:没有解决对象识别的问题(即怎样知道一个对象的类型)。

解决办法:构造函数模式

二、构造函数模式

首先我们要知道构造函数分两类:

  • 原生构造函数:例如 Object() Array() Date()
  • 自定义构造函数:顾名思义,自己定义的构造函数,例如:
function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function() {
        console.log(this.name);
    }
}
var person3 = new Person("Tom", 23, "Player");
var person4 = new Person("Tom", 22, "Doctor");

1、以上代码是使用构造函数模式将工厂模式的例子重写了一遍。这两个函数的区别是:

  • 没有显式的创建对象(new)
  • 直接将方法和属性赋给了this对象
  • 没有return语句
  • 函数名(Person) 第一个字母用大写。构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头

要创建Person 的新实例,必须使用new 操作符。以这种方式调用构造函数实际上会经历以下4个步骤:
(1) 创建一个新对象;
(2) 将构造函数的作用域赋给新对象(因此this 就指向了这个新对象);
(3) 执行构造函数中的代码(为这个新对象添加属性);
(4) 返回新对象。

2、上面的代码中,person3 和person4 分别保存着Person 的一个不同的实例。

这两个对象都有一个constructor(构造函数)属性,该属性指向Person。(这句是JS红皮书中的话,其实是有误的,这两个对象实例其实没有costructor属性,是它们的原型有这个属性,通过原型链就可以访问到

console.log(person3.constructor == Person); // true
console.log(person4.constructor == Person); // true
console.log(person3 instanceof Object); //true
console.log(person3 instanceof Person); //true 

可以看出来,创建的person3和person4的两个对象实例既是Object的实例,又是Person的实例。
由此可知:创建自定义构造函数可以吧它的实例标识为一种特定的类型,这也正是构造函数胜过工厂模式的地方。

3、将构造函数当做函数

构造函数与普通函数的唯一区别就是调用它们的方式不同
我们要知道的是:构造函数也是函数。所以,任何函数,只要通过new 操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过new 操作符来调用,那它跟普通函数也不会有什么两样

//当做构造函数使用
var person = new Person("Tom", 23, "Player");
person.sayName(); //"Tom"
//当做普通函数使用
Person("Jerry", 21, "Doctor");  //添加到window
window.sayName();   //"Jerry"
//在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "Bob", 25, "Nurse");
o.sayName();    //"Bob"

4、构造函数的缺点

构造函数的主要问题就是每个方法都要在每个实例上重新创建一遍。前面例子中的person3和person4这两个实例都有一个sayName()方法,但是这两个方法不是同一个Function实例。因为JS中函数也是对象。所以每定义一个函数,也就实例化了一个对象:

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = new Function("console.log(htis.name)");
}

从这个角度看构造函数,更容易明白创建的每一个Person实例都包含一个不同的Function实例。所以,不同实例上的同名函数是不相等的。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function() {
        console.log(this.name);
    }
}
var person3 = new Person("Tom", 23, "Player");
var person4 = new Person("Tom", 22, "Doctor");
console.log(person3.sayName == person4.sayName); //false

然而,创建两个完成同样任务的Function 实例的确没有必要;况且有this 对象在,根本不用在执行代码前就把函数绑定到特定对象上面。因此,大可像下面这样,通过把函数定义转移到构造函数外部来解决这个问题。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = sayName;
}
function sayName(){
    console.log(this.name);
}
var person3 = new Person("Tom", 23, "Player");
var person4 = new Person("Tom", 22, "Doctor");
console.log(person3.sayName == person4.sayName); //true

在这个例子中,我们把sayName()函数的定义转移到了构造函数外部。而在构造函数内部,我们将sayName 属性设置成等于全局的sayName 函数。这样一来,由于sayName 包含的是一个指向函数的指针,因此person1 和person2 对象就共享了在全局作用域中定义的同一个sayName()函数。这样做确实解决了两个函数做同一件事的问题,可是新问题又来了:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。好在,这些问题可以通过使用原型模式来解决。

三、原型模式

我们需要知道的定义:
1、我们创建的每一个函数都有一个prototype属性,这个属性指向一个对象——原型对象。
2、每个原型对象都有一个constructor属性,指向这个属性指向 prototype属性所在函数的指针。
3、原型对象的用途:包含可以由特定类型的所有实例共享的属性和方法。不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中

function Person(){}
Person.prototype.name = "Tom";
Person.prototype.age = 23;
Person.prototype.job = "Player";
Person.prototype.sayName = function() {
    console.log(this.name);
}
var person1 = new Person(); 
person1.sayName();  //"Tom"
var person2 = new Person(); 
person2.sayName();  //"Tom"
console.log(person1.sayName == person2.sayName); //true

1、理解原型对象

需要知道的:
1、我们创建的每一个函数都有一个prototype属性,这个属性指向一个对象——原型对象。
2、每个原型对象都有一个constructor属性,指向这个属性指向 prototype属性所在函数的指针。
3、当调用构造函数创建一个新实例(person),每个实例都有一个 __ proto __ 属性,指向原型对象。

如下图:

理解原型.png

检测方法:
1、isPrototypeOf() 来确定实例与原型之间是否存在对应关系

console.log(Person.prototype.isPrototypeOf(person1)); //true

2、Object.getPrototypeOf(),在所有支持的实现中,这个方法返回[[Prototype]]的值

alert(Object.getPrototypeOf(person1) == Person.prototype); //true
alert(Object.getPrototypeOf(person1).name); //"Tom"

3、hasOwnProperty()方法可以检测一个属性是存在于实例中,还是存在于原型中。这个方法(不要忘了它是从Object 继承来的)只在给定属性存在于对象实例中时,才会返回true

2、原型与in操作符

in 操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。

function Person(){}
Person.prototype.name = "Tom";
Person.prototype.age = 23;
Person.prototype.job = "Player";
Person.prototype.sayName = function() {
    console.log(this.name);
}
var person1 = new Person(); 
var person2 = new Person(); 
console.log(person1.hasOwnProperty("name")); //false
console.log("name" in person1); //true
person1.name = "Bob";
console.log(person1.hasOwnProperty("name")); //true
console.log("name" in person1); //true

3、更简单的原型语法

前面例子中每添加一个属性和方法就要敲一遍Person.prototype。为减少不必要的输入,也为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象,如下面的例子所示。

function Person() {}
Person.prototype = {
    name:"Tom",
    age:23,
    job:"Player",
    sayName:function(){
        console.log(this.name);
    }
}
var friend = new Person();
console.log(friend instanceof Object); //true
console.log(friend instanceof Person); //true
console.log(friend.constructor == Person); //false
console.log(friend.constructor == Object); //true

这里的唯一区别就是constructor不再指向Person。

4、原型的动态性

由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来——即使是先创建了实例后修改原型也照样如此。

function Person(){}
var friend = new Person();
Person.prototype.sayHi = function(){
    console.log("hi");
}
friend.sayHi();  //"hi"

尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果是重写整个原型对象,那么情况就不一样了。我们知道,调用构造函数时会为实例添加一个指向最初原型的[[Prototype]]指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。请记住:实例中的指针仅指向原型,而不指向构造函数。看下面的例子

function Person(){}
var friend = new Person();
Person.prototype = {
    constructor:Person,
    name:"Tom",
    age:23,
    job:"Player",
    sayName:function(){
        console.log(this.name);
    }
};
friend.sayName();  //error

5、原生对象的原型

我们知道所有的引用类型都会有自定义的一些方法,比如Array的sort()方法。其实都是Array()构造函数对应的原型的方法。
所有原生引用类型(Object、Array、String,等等)都在其构造函数的原型上定义了方法。

console.log(typeof Array.prototype.sort); //"function"

6、原型对象的问题

原型模式也不是没有缺点。首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型的最大问题。
原型模式的最大问题是由其共享的本性所导致的。
原型中所有属性是被很多实例共享的,这种共享对于函数非常合适。对于那些包含基本值的属性倒也说得过去,毕竟(如前面的例子所示),通过在实例上添加一个同名属性,可以隐藏原型中的对应属性。然而,对于包含引用类型值的属性来说,问题就比较突出了。来看下面的例子。

function Person(){}
Person.prototype = {
    constructor:Person,
    name:"Tom",
    age:23,
    job:"Player",
    friends:["Jerry","Bob"],
    sayName:function(){
        console.log(this.name);
    }
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("Mary");
console.log(person1.friends); //["Jerry", "Bob", "Mary"]
console.log(person2.friends);   //["Jerry", "Bob", "Mary"]
console.log(person1.friends === person2.friends); //true

当通过person1修改friends属性的时候,其实是修改了原型,所以person2也会共享这个修改后的属性。但是实例一般应该要有属于自己的全部属性的。

四、组合使用构造函数模式和原型模式

创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参数;可谓是集两种模式之长。

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Bob","Mary"];
}
Person.prototype = {
    constructor:Person,
    sayName:function(){
        console.log(this.name);
    }
}
var person1 = new Person("Tom", 23, "Player");
var person2 = new Person("Jerry", 21, "Doctor");
person1.friends.push("Van");
console.log(person1.friends); // ["Bob", "Mary", "Van"]
console.log(person2.friends); //["Bob", "Mary"]
console.log(person1.friends === person2.friends); //false
console.log(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(){
            console.log(this.name);
        }
    }
}
var person = new Person("Tom", 23, "Player");
person.sayName(); //"Tom"

六、寄生构造函数模式

function Person(name, age, job) {
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function(){
        console.log(this.name);
    };
    return o;
}
var person = new Person("Tom", 23, "Player");
person.sayName(); //"Tom"

在这个例子中,Person 函数创建了一个新对象,并以相应的属性和方法初始化该对象,然后又返回了这个对象。除了使用new 操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的。构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个return 语句,可以重写调用构造函数时返回的值。
建议在能用其他方式的情况下不用寄生构造函数模式

七、稳妥构造函数模式 (了解即可)

道格拉斯·克罗克福德(Douglas Crockford)发明了JavaScript 中的稳妥对象(durable objects)这个概念。所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this 的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用this 和new),或者在防止数据被其他应用程序(如Mashup程序)改动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用this;二是不使用new 操作符调用构造函数。按照稳妥构造函数的要求,可以将前面的Person 构造函数重写如下

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容