《javascript高级程序设计(第二版)》学习(4)原型

本文介绍了JavaScript中对象创建的几种模式,包括工厂模式、构造函数模式和原型模式,并详细阐述了它们的工作原理及优缺点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文章为个人学习笔记,内容纯属个人观点,如有纰漏,欢迎指出

对象回顾

我们知道创建一个对象最直观的方法,就是创建一个Object的一个实例,然后在为它添加上属性和方法。

var person = new Object();
person.name="mike";
person.age=11;
person.sayInfo=function(){
    alert(this.name+"  "+this.age);
}

使用Object构造函数或对象字面量都可以创建单个对象(如json),蛋这些方式有明显的缺点,使用一个接口创建很多对象,会产生大量的重复代码。为了解决这个问题,人们使用了工厂模式的一种变体

工厂模式

工厂模式是众所周知的一种设计模式,该模式抽象了具体对象的过程,考虑到在ECMAScript中无法创建类,聪明的前辈们就发明了一种函数,用函数来封装以特定接口创建对象的细节。该模式例子如下

function createPerson(name, age, job){
    var o = new Object();
    o.name=name;
    o.age=age;
    o.job=job;
    o.sayInto = function(){
        alert(this.name+this.age+this.job);
    }
    return o;
}

var person1 = createPerson("Nicholas", 29, "teacher");
var person2 = createPerson("tom", 21, "student");

函数createPerson()能够根据接受函数来构建一个包含所有必要信息的Person对象。可以数次调用这个函数,每次将返回一个包含三个属性一个方法的对象。构造函数解决了创建多个相似对象的问题,但是却没有结局对象的识别问题(注意到函数内部是new Object,得到的并不是Person的实例)

构造函数模式

在讲述原型之前,我觉得很必要说下构造函数模式。构造函数模式有个很明显的特点,就是其中的属性是”this.”开始的,这表示这个属性或者方法是指向它的调用者的。我们知道,在JS中,函数既是对象,也是类,因此可以使用new,得到的是这个“类”的一个实例。所以,上段代码可以这样写。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job=job;
    this.sayInfo(){
        alert(//do something)
    }
}

var person1 = new Person("mike",11,"student");
var person2 = new Person("mi",21,"engineer");

构造函数模式与工厂模式相比,有以下几点不同:

  1. 没有显示地创建对象;
  2. 直接将属性和方法赋给了this对象;
  3. 没有return;

我们注意到,这里创建对象和java语言中使用new很类似,后面跟随的是一个“构造函数”。使用这种方式会经历一下4个步骤:

  1. 创建一个新对象
  2. 将构造函数的作用域赋给新对象(this)
  3. 执行构造函数中的代码(为对象添加属性)
  4. 返回新对象

以这种方式定义的构造函数是定义在Global对象(在浏览器中是window对象)中的。

构造函数模式的不足:构造函数的主要问题,就是每个方法都要在实例上重新创建一遍。很简单的道理,我创建了小明和小王,他们都有姓名和年龄以及说出自己信息的方法,因为使用的是构造函数,所以他们方法做的事是相同的,从这点上看,两个对象有相同的方法,但这两个方法都是归属于各自对象的。如果说共同的方法能大家共享多好,否则是否是一种浪费呢?

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

解决这个问题的一种做法是这样的,不直接在构造函数中定义一个函数,而是使用一个函数的引用。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job=job;
    this.sayInfo()=oneFunc;
}

function oneFunc(){
        alert(//do something)
}

在这个例子中,我们把方法放在了构造函数外,而在构造函数内部,让实例方法等于一个一个全局函数。这样一来,两个实例化对象就共享了在全局作用域中廷议的同一个函数了。

可是这有一点小小的问题:在全局作用域中定义的函数实际上只是被某个对象调用,这与全局函数的作用有点名不副实(全局嘛,就是让大家都爽,结果只能一部分人爽)。更糟糕的情况是,如果对象拥有多种方法呢?是不是意味着更多的全局函数,这样使用,显然污染了全局空间,也没有封装性可言。

对于构造函数模式的问题,原型模式给出了很好的解决。

原型模式

在JS中,我们创建的每个函数,都有一个prototype(原型)属性(这个可以在浏览器中,F12,进入js代码段,查看你创建出的对象,它的属性中有proto 里面包含了原型对象的信息),这个属性是一个指针,指向一个对象,而这个对象的用户时包含可以由特定类型的所有所有实例共享的属相和方法。

如果从字面上来理解的话,prototype就是通过调用构造函数而创建的那个对象实例的原型对象。

关于原型我觉得有必要说明一下原型链,它指的是,如果这些对象的原型存在着一种类似继承的关系,那么当对象要使用属性或者方法时,如果自身没有,会从它的原型上找,如果这个原型没有,会从上一个原型找,最顶层的分别是Object和null。

使用原型对象的好处是可以让所有对象实例都共享它所包含的属性和方法。例如:

function Person(){
}

Person.prototype.name="Amy";
Person.prototype.age=11;
Person.prototype.job="student"
Person.prototype.sayInfo=function(){//do something};

var person1 = new Person();
person1.name; //Amy
var person2 = new Person();
person2.name; //Amy
alert(person1.sayInfo == person2.sayInfo) // true

从上我们可以很清楚的看出这和构造函数模式有一个很大的不同,构造函数是每个人都有属性和方法,而原型模式是每个对象去共享原型对象中的属性和方法。

原型对象是自动生成的,因此要理解原型模式,不得不说说原型对象

原型对象

无论什么时候,只要创建了一个新函数,就会根据一组特定的规则,为这个函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属相所在函数的指针。

当调用构造函数创建一个新实例后,该实例内部将包含一个指针,指向构造函数的原型对象。在ECMA5中管这个指针叫做[[prototype]],js中没有标准的方式访问这个指针,但是各浏览器上都支持一个“_proto_”属性。
构造函数、原型对象和实例的关系

尽管我们在脚本中无法访问这个属性,但js中可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系。

alert(Person.prototype.isPrototypeOf(person1)) //true

很明显,person1是Person的一个实例,Person的原型当然是实例的原型(这个结论有失偏颇,后面将说明为何)。此外,在ECMA5中,所有对象的超类Object中有一个Object.getPrototypeOf(),返回的是某个对象的原型。因此,上面的代码也可以这样验证。

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

每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。首先会从对象实例本身开始。如果在实例中找到了就不会去打扰原型,返回;否则会试试去找实例的原型对象,在原型对象中查找,如果找到了就返回,没找到的话会向该原型对象的上一个原型寻找。

虽然可以通过对象实例访问原型中的值,但实例没有办法去修改原型中的值。

function Person(){};
Person.prototype.name="Amy";

person1 = new Person();
alert(person1.name); //Amy

person1.name="mike";
person2 = new Person();

alert(person1.name); //mike
alert(person2.name); //Amy

原本的第一个实例是和第二个实例共享原型中的name,之后person1定义了自己的name属性,不会影响到原型中的name属性。

可以利用下面这个Object中的hasOwnProperty方法检查某个属性是否是它本身拥有的,true表示实例有这个属性,false表示没有或者这个属性是存在原型中。

person1.hasOwnProperty("name") //true;
person2.hasOwnProperty("name") //false;
原型与in操作符

在js中,in会访问对象的属性,不论这个属性是存在实例,还是存在原型中。

//还是前面person1 和person2
person1.hasOwnProperty("name") //true;
alert( "name" in person1) //true  综合判断得存在于实例

person2.hasOwnProperty("name") //false;
alert( "name" in person2) //true  综合判断得存在于原型
简洁的原型写法

之前的那种原型模式写起来有点累?不用担心,我们依然可以像使用JSON一样,使用字面量来对原型对象增加属性和方法。

Person.prototype={
    "name":"Amy",
    "age":12,
    sayName:function(){
        alert(this.name);
    }
};

在这段代码中,我们将Person.prototype设置等于一个以字面量形式创建的对象,最终结果相同。但是,constructor属性不再指向Person()了。每创建一个函数,都会自动创建一个原型出来,原型的constructor会指向该函数。当我们采用这种字面量的方式时,本质上是完全重写了默认的原型对象,因此constructor属性变成了新对象的constructor属性(改为指向Object了),虽使用instanceOf能返回期望的结果,但通过constructor无法确定对象的类型。

//接上面
var friend = new Person();
alert(friend instanceof Object); //true
alert(friend instanceof Person); //true
alert(friend.constructor == Object); //true
alert(friend.constructor == Person); //false

前面提到了,new出的对象会是构造函数对应类的实例结论有失偏颇就是指这里,因为原型对象被重写了,虽然使用instanceof确实得到了期望的结果,对象的类型并不是Person,这看起来十分的矛盾。但从应用的角度上来讲,这并没什么大碍,如果有强迫症的话,可以显式的添加constructor属性

    function Person(){}

    Person.prototype={
        "constructor":Person,
        "name":"tom"
    };

    var friend = new Person();

    alert(friend instanceof Object); //true
    alert(friend instanceof Person); //true
    alert(friend.constructor == Object); //false
    alert(friend.constructor == Person); //true

这样,就可以修改原本中矛盾的地方了。但这么使用,会让constructor变为可枚举(默认是不可枚举的),例如我们使用for in 时也会得到这个属性,如果要做的更加完美。

function Person(){}

Person.prototype={
    "name":"tom"
};

Object.defineProperty(Person.prototype, "constructor" {
    enumerable : false,
    value : Person
}); 
原型的动态性

在原型中查找值过程是一次搜索,对原型对象的任何修改都可以立刻从实例上得到反映,不管这个对象实在这次修改前面还是后面实例出来的。该原因归结于实例和原型之间松散的连接关系。前面提到,实例中如果没有这个属性或者方法会去访问它的原型,因此即便是在之前实例的对象,也可以访问到新增的属性和方法。

var friend = new Person();

Person.prototype.sayHi = function(){
    alert("hi");
};

friend.sayHi(); //完全没问题

记住,如果是完全重写原型,那么就不一样了

function Person(){}

var friend = new Person();

Person.prototype={
    constructor:Person,
    sayHi:function(){
        alert("hi");
    }
};

friend.sayHi(); //error

重写原型对象切断现有原型与任何之前已经存在的对象实例之间的联系;之前对象引用的依然是最初的原型。
重写原型对之前对象的影响

原型对象的问题

原型模式也不是没有缺点,首先,省略了位构造函数传递初始化参数这一关键环节,得到的对象有点类似前面提到的工厂模式,得到的都是一样的对象,虽然所有实例共享共同的属性。共享属性对于函数而言,也许不是个问题,反而是一件好事,但如果属性是引用类型,就有麻烦了。


function Person(){}

Person.prototype = {
    constructor:Person,
    name:"amy",
    friends:["tom","mike"]
};

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

person1.friends.push("Van");

alert(person1.friends);  // tom mike van
alert(person2.friends);  // tom mike van
alert(person1.friends == person2.friends); //true

从代码中看到了重写的原型对象中有一个字符串数组,person1向数组中添加了一个新朋友,而person2的字符串数组也反映出了这个结果。

实例一般都是要有自己独特的全部属性,而这个问题正是我们很少看到有人单独使用原型模式的原因了。

——END THANKS FOR WATCH——

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值