Javascript高级编程学习笔记(21)—— 对象原型

JS中对象相关的最重要的恐怕就是原型链了

原型链也是JS中对象继承的实现的基础

 

接昨天的文章,我们使用构造函数创建对象的时候仍然存在一些问题

那就是所有的实例没法共用一个函数

这样无疑会造成极大的内存浪费

 

那么解决的办法是什么呢?

那就是通过对象的原型来实现

 

原型链

在JS中我们创建的每一个函数都有一个 prototype(原型)属性

该属性是一个指针指向一个对象,这个对象的用途是包含由特定类型所有实例的共享属性和方法

这个对象也就是我们常说的原型对象

原型链就是对象实例和对象的原型对象组成的继承链

而JS中所有的对象都继承自Object类,所以原型链的顶端也就是Object构造函数的prototype 指向的对象

可以参考下图,可能画的有点丑请多见谅

对于这样一个继承链,在其它的面向对象的语言中,最下面的有关对象的部分应该是公有的,和类的概念十分类似

而不太好理解的,就是有关构造函数的那一部分

造成JS继承灵活多变的地方就在于这里,JS中的函数也是一个对象

所以函数也有自己的原型链,为了让构造函数和被构造函数创建的实例之间联系起来,所以函数除了有自己的 [[Prototype]] 属性之外,还有prototype属性指向被其构造的对象实例的原型对象

与此同时被函数prototype属性指向的对象,其constructor属性也会指向这个函数,这样就使构造函数和对象紧密地结合在一起

 

PS. 上文中的 [[Prototype]] 属性,就是对象的__proto__属性,但是这一属性只在浏览器中实现,JS的其他实现,对象不一定会有 __proto__属性

在这些没有__proto__ 的实现中虽然无法访问,但是可以通过 isPrototypeOf() 来检测对象之间的这种联系

而在ES5以上的脚本实现中,还支持一个Object.getPrototypeOf() 用于获取一个对象的原型对象

 

原型模式

在此基础上,我们可以发现,一个构造函数构造的所有函数的实例都有办法能够访问到原型对象

那么一类对象的共享属性的存放位置就显而易见了,那就是将其存放到原型对象上

以昨天的例子来说,使用原型模式创建对象如下:

function Person(){}

var pPrototype = Person.prototype;
pPrototype.name = 'lhy';
pPrototype.job = 'font end';
pPrototype.age = 21;
pPrototype.sayName = function(){
    alert(this.name);
}

var person1 = new Person();

能够访问的原因在于,当代码读取对象的属性的时候 ,都会执行一次搜索,该搜索过程会从该对象实例开始

如果没有找到指定属性,则顺着原型链进行查找,直到Object.prototype为止

找到相应属性则返回,没找到则报未定义的错误

 

PS. 虽然我们可以通过实例对象访问原型对象的属性,但是我们无法通过实例对象来重写原型对象的属性

什么意思呢?就是说虽然我们可以通过实例对象的属性访问原型对象的某个属性,但是我们对实例对象对应属性的修改不会映射到原型对象上

如果我们在实例对象上设置了,一个本来只有原型对象上才有的属性,那么JS会为实例对象创建一个同名属性,并设置它的值

然后原型对象上的属性就被同名的属性屏蔽了,再也无法访问到原型对象上的这个属性,除非使用delete删除实例上的该属性

例如:

 

为了能够判断一个属性是来自于该实例,还是其原型对象

可以使用 hasOwnProperty() 方法来进行判断,对于来自实例的属性返回 true

 

Tips. in操作符不在for-in循环中单独使用的时候,用于判断对象上是否存在该属性,上面的方法与in操作符配合就可以判断属性到底来自实例,还是原型

function test(propertyName,obj){
    if(obj.hasOwnProperty(propertyName)){
         alert('来自实例');
    )else if(propertyName in obj){
        alert('来自原型');
    }
}

Ps.  for-in 循环会遍历对象上的所有可枚举属性,包括来自原型的属性,需要注意的是,如果原型上有个不可枚举的属性,而实例上有个与之同名的可枚举属性,那么实例上的这个属性会被遍历出来,不会受原型的影响

除此而外JS提供了Object.keys() 方法,可以获取到对象所有可枚举属性的集合

 

 

原型模式还有另一种简化写法

function Person (){}
Person.prototype = {name:"lhy",age:"21"};

但是这种写法存在一个问题,由于这里直接将一个对象指定为函数的原型对象

所以这里的函数原型的 constructor 属性也被改变了,这样我们就没法通过 constructor 来判断函数类型

那怎么办呢?

办法还是有的,那就是手动将要指定的原型对象的 constructor 设置为构造函数

不过要注意的是如果手动设置,那么该属性会被for-in 遍历出来,因为这种方式的就是为实例 创建一个 constructor 属性,以此来屏蔽不正确的constructor属性

所以最后还要将 constructor 属性特性描述符的 Enumerable 设为false,才能完美地解决

 

原型的动态性

从之前举的例子,和刚才简化的写法,我们不难发现原型是一个动态地对象

因为我们对原型对象的修改都会立即体现在实例对象上

同样的我们在操作原型时就要更加地小心,尤其是在重写原型对象时

拿上方的例子来说,虽然我们通过一些操作尽量减少了重写原型对象带来的差异,但还有一些是没法消除的

那就是我们重写原型对象时,会切断已有实例和重写的原型对象的实例,因为它们仍旧指向旧的原型对象

 

 讲了这么多,那么原型模式是我们创建一类对象的最好的选择嘛?

虽然对于函数来说十分地合适,但对于一些引用类型的值可能就会有一些问题

比如我希望对象拥有自己独立的属性,那么原型模式可能就无法胜任了

那么怎么办呢?

 

构造函数与原型的组合模式

构造函数的缺点是没法共用数据,原型模式的缺点是没法有独立数据

那将他们组合起来不就行了吗

所以这种组合模式就产生了

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

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

这种模式基本上就能满足我们平时的所有需求,所以这也是我们广泛使用的方式

 

动态原型模式

上面的方法虽然从功能上说,无可挑剔,但是暴露在外的独立的原型属性的声明,让追求极致的程序员们十分难受

所以动态原型模式就出现了

function Person(name, job, age){
    this.name = name;
    this.age = age;
    this.job = job;
    if(this.sayName !== 'function'){
        Person.prototype.sayName = function(){
            alert(this.name);
        }
    }
}

 

寄生构造函数模式

虽然动态原型模式,已经能够完美地处理绝大部分情况,但是有些特殊情况,仍会存在问题

就像我们不建议给原生的对象的原型添加属性,但有时候我们确实需要根据某个原生的对象来创建一类对象,那么怎么办呢?

所以就有了寄生构造函数模式,其原理就是我不在原生的原型上添加,但我可以使用工厂方法,对其构造的原型对象进行加工,从而得到我想要的对象

function specialArray(){
    var values = new Array();
    values.push.apply(values,arguments);
    values.myName = function(){
        return 'lhy';
    }
    return values;
}

这种只是改良了的工厂方法,存在的弊端和工厂方法几乎一致,无法确定对象类型等

所以使用场景很少

 

稳妥构造函数

我们知道在一些安全环境下,我们没法使用this、new等来创建,或者需要防止数据被其他应用程序篡改时使用

pS. 不使用new 是指不用new调用构造函数

function Person(name, job, age ){
    var o = new Object();
    o.sayName = function(){
        alert(name);
    }
    return o;
}

这样创建对象时传入的原始数据,只能通过返回的对象o 上面的方法进行访问,所以保证了原始数据的安全性

 

 

 

总的来说,创建一类函数使用组合模式的场景更为广泛,使用频率也更高

 

posted @ 2018-12-24 15:03  巽秋  阅读(212)  评论(0编辑  收藏  举报