什么是面向对象的javaScript(二)——创建对象

 Object构造函数和对象字面量都可以创建单个对象,但是缺点明显:同一个接口创建多个对象,会产生大量重复代码。为了解决这个问题,人们开始使用工厂模式的一种变体。

一、工厂模式

工厂模式抽象了创建具体对象的过程。ECMA中无法创建类,开发人员发明一种函数,用以封装特定接口创建对象的细节,例如。

    function createPerson(name,age){
        var o = new Object();
        o.name  = name;
        o.age   = age;
        o.sayName = function(){
            alert(this.name);
        };
        return o;
    }
    var p1 = createPerson("zhangsan",15);
    var p2 = createPerson("lisi",20);

 函数createPerson能够根据接收的参数来构建一个包含所有必要信息的Person对象。可以无数次地调用这个函数,而每次他都会返回一个包含三个属性一个方法的对象。工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(怎样知道一个对象的类型)。

二、构造函数模式

 ES中的构造函数可以创建特定类型的对象。例如

    function Person(name,age){
        this.name = name;
        this.age = age;
        this.sayName = function(){
            alert(this.age);
        }
    }
    var p1 = new Person("zhangsan",15);
    var p2 = new Person("lisi",20);

 此例子中Person()函数取代了createPerson()函数。我们注意到,Person()中的代码除了与createPerson()中相同的部分外,还存在一下不同之处:

  • 没有显式地创建对象;
  • 直接将属性和方法赋给了this对象;
  • 没有return语句;

 要创建Person实例,必须使用new操作符。以这种方式调用构造函数会经历一下4个步骤:

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

 在前面例子的最后,p1和p2分别保存着Person的一个不同的实例。这两个对象都有一个constructor(构造器)属性,该属性指向Person,如下所示:

alert(p1.constructor == Person); //true
alert(p2.constructor == Person); //true

 对象的constructor属性最初是用来标识对象类型的。但是,提到检测对象类型,instanceof更靠谱。我们此例子中创建的对象既是Person的实例,同时也是Object的实例,这点可以通过instanceof 验证。

 构造函数模式之所以胜过工厂模式,是因为自定义的构造函数可以将它的实例标识为一种特定的类型。而所有对象均继承自Object。

1、将构造函数当成函数

 构造函数与其他函数的唯一区别是——调用方式不同。任何函数,只要通过new操作符调用,那它就可以作为构造函数。反之,就和普通函数没有区别。前面定义的Person()函数可以通过下列任何一种方式来调用

// 当作构造函数使用
    var person = new Person("zhangsan",15);
    person.sayName(); // zhangsan

    // 当作普通函数使用
    Person("lisi",20);
    window.sayName(); // lisi

    // 在另一个对象的作用域中调用
    var o = new Object();
    Person.call(o,"wangwu",25);
    o.sayName(); // wangwu

 此例前两行代码展示了构造函数的典型用法,即使用new操作符创建一个新的对象。中间两行展示了不用new操作符调用Person()会出现什么结果:属性和方法都被添加到了window对象了。当在全局作用域中调用一个函数时,this对象总是指向Global对象(在浏览器中就是window对象)。因此调用完之后,可以通过window对象调用sayName()方法。最后,也可以使用call()或者apply在某个特殊对象的作用域中调用Person()函数。这里是在对象o的作用域中调用的,因此调用后o就拥有了所有属性和sayName()方法

2、构造函数存在的问题

 构造函数虽好,但也有缺点。一个主要问题是:每个方法都要在每个实例上重新创建一遍。

alert(p1.sayName == p2.sayName); // false

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

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

    function sayName() {
        alert(this.name);
    }

    var p1 = new Person("zhangsan",15);
    var p2 = new Person("lisi",20);

 这样一来,由于p1、p2的sayName属性包含相同的指向全局函数sayName()的指针,因此就共享了全局作用域中的同一个函数。这样确实解决了两个函数做同一件事的问题,可是新的问题又来了:

 在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。好在这些问题可以使用原型模式来解决。

三、原型模式

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

    function Person(){}

    Person.prototype.name = "zhangsan";
    Person.prototype.sayName = function(){
        alert(this.name);
    }

    var p1 = new Person();
    p1.sayName() // zhangsan;
    var p2 = new Person();
    p2.sayName() // zhangsan;

    alert(p1.sayName == p2.sayName); // true;

1、理解原型对象

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

 上图展示了Person构造函数、Person的原型属性和Person的两个实例之间的关系。Person.prototype指向了原型对象,而Person.prototype.constructor又指回了Person。

 原型对象的方法isPrototypeof()可以检测它是否是某个实例的原型对象。

    alert(Person.prototype.isPrototypeOf(p1); //true
    alert(Person.prototype.isPrototypeOf(p2); //true

 E5中增加了一个新方法,叫Object.getPrototypeOf(),该方法返回[[Prototype]]的值,例如:

    alert(Object.getPrototypeOf(p1) == Person.prototype) //true

 支持该方法的浏览器IE9+和其他

 多个实例共享原型的属性和方法的原理——属性的搜索机制

每当代码读取某个对象的属性时,都会执行一次搜索。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找目标属性。

 理解上述机制,原型的属性可以被屏蔽,但是不能被修改就很好理解了

function Person(){}

    Person.prototype.name = "zhangsan";
    Person.prototype.sayName = function(){
        alert(this.name);
    };

    var p1 = new Person();
    var p2 = new Person();
    p1.name = "lisi";
    alert(p1.name); // lisi
    alert(p2.name); // zhangsan

    delete p1.name;
    alert(p1.name); // zhangsan

 用原型的搜索机制很好解释上例的结果

 方法hasOwnProperty()可以检测一个属性是否存在于实例中

alert(p1.hasOwnProperty("name") // 如果name是实例的属性,则true;如果是原型的属性,则false

2、原型与in操作符

 in操作符有两种使用方式:1单独使用;2 for-in循环中使用

 单独用时,in操作符会在能够访问到目标属性时返回true,不论属性是实例的还是原型的。例如

function Person(){}

    Person.prototype.name = "zhangsan";
    Person.prototype.sayName = function(){
        alert(this.name);
    };

    var p1 = new Person();
    var p2 = new Person();

    alert(p1.hasOwnProperty("name")); //false
    alert("name" in p1); // true

 通过in操作符和hasOwnProperty()方法的组合,可以确定一个属性是否是原型属性

    function isPrototypeProperty(object,name){
        return !object.hasOwnProperty(name) && (name in object);
    }

 for-in循环时,返回所有能够通过对象访问的、可枚举的(enumerated)的属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性的实例属性也会在for-in循环中返回,因为根据定义,所有开发人员定义的属性都是可枚举的,Ie8及以前例外,ie早期版本存在一个bug,即屏蔽不可枚举属性的实例属性不会出现在for-in循环中。

 要取得对象上所有可枚举的实例属性,可以用E5的Object.key()方法。如果要得到所有属性,无论可否枚举,可以使用Object.getOwnPropertyNames()。这两个方法都可以代替for-in循环。支持浏览器IE9+和其他

3、更简单的原型语法

为了避免每次添加一个属性,都要敲一遍Person.prototype,更常见的做法如下:

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

 上例将一个字面量对象设置为Person.prototype,结果基本相同,但是有一个例外:constructor属性不再指向Person了。因为本质上完全重写了默认的prototype对象,所以constructor属性也变成了新对象的constructor属性(指向Object构造函数)。此时instanceof操作符还能返回正确的结果,但constructor已经不行了

    var p1 = new Person();
    alert(p1 instanceof Person) //true
    alert(p1.constructor== Person) // false

 如果constructor真的很重要,需要保留,则可以:

    Person.prototype = {
        // 保留constructor的引用
        constructor: Person,
        name: "zhangsan",
        age:  15,
        sayName: function(){
            alert(this.name);
        }
    };

 注意这种方式会导致属性constructor的[[enumerable]]特性被设置为true,而原生的constructor属性是不可枚举的,可以用Object.defineProperty()测试。

4、原型的动态性

 我们对原型的任何修改都能立即从实例中反应出来——即使先创建实例,后修改原型也如此。因为实例与原型之间的连接只是个指针而已,知道什么是引用类型就很容易理解了。

 但是,如果重写整个原型对象,情况就不一样了。注意下列两种区别

    function Person(){}

    var p1 = new Person();

    // 改变原型的属性
    Person.prototype.name = "zhangsan";
    Person.prototype.sayName = function(){
        alert(this.name);
    };

    p1.sayName() // zhangsan

    function Person(){}

    var p1 = new Person();

    // 改变了整个原型对象
    Person.prototype = {
        // 保留constructor的引用
        constructor: Person,
        name: "zhangsan",
        age:  15,
        sayName: function(){
            alert(this.name);
        }
    };

    p1.sayName()  // 发生错误

 示例二的过程如图

 如图所示,重写原型,切断了与之前既存的的实例之间的联系;它们的引用仍然是最初的原型对象。

5、原生对象的原型

 js中的原生对象也在其原型上定义了一些公共方法。

6、原型对象存在的问题

 原型上的属性和方法被所有实例共享,这种共享对函数非常合适。但是实例需要自己的空间,所以单独使用原型并不常见。

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

构造模式用户定义实例属性,原型用于定义方法和共享属性。这种混合模式是ES中使用最广泛、认同度最高的一种创建自定义类型的方法

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

    Person.prototype = {
        constructor:Person,
        sayName:function(){
            alert(this.name);
        }
    }

五、动态原型模式

有过其他OO语言经验的开发人员看到独立的构造函数和原型,会非常的困惑。动态原型模式就是要解决这个问题。
动态原型模式把所有信息都封装在了构造函数中,通过在构造函数中初始化原型(仅在必要的情况下),保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。例如

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

    // 方法绑定
        if(typeof this.sayName != 'function'){
            Person.prototype.sayName = function(){
                alert(this.name);
            }
        }
    }

    // 测试
    var p1 = new Person("zhangsan",15);
    var p2 = new Person("zhangsan",15);
    p1.sayName();
    p2.sayName();

注意上例的方法绑定部分,该方法只会在初次调用构造函数时才会执行。此后,原型就已经给完成初始化,不会再执行。其中,if 语句检查的可以是初始化后应该存在的任何属性和方法,而且检查其中一个即可,不必全部检查。

使用动态原型模式时,不能使用对象字面量重写原型。因为在已经创建了实例的前提下重写原型,会切断现有实例和新原型之间的联系

扩展阅读

寄生构造函数模式
稳妥构造函数模式

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值