β

理解Javascript原型链

Tonni's Blog 101 阅读

在过去很长一段时间我都不是真正的了解Javascript原型继承机制。最近在读《Javascript高级程序设计》这本书的时候看到148页才算真正开始了解Javascript的继承机制以及Javascript[[Prototype]]属性的奥秘。下面做下总结,很多知识点都是来自于该书上的,很不错的书,推荐WEB开发者购买。

ECMAScript规定了6种基本的数据类型,其中一个特殊的数据类型就是Object,Object是一个无序的数据集合,Javascript内置的所有全局对象都是在继承自数据类型Object,注意这里的两个Object的关系:一个是ECMAScript基本数据类型,一个是Javascript内置全局对象。

原型

Javascript中每个函数都有一个属性prototype,无论是我们自定义的属性还是Javascript内置对象的构造函数(Object、String、Function等)都有这一属性:

var fn = function (){

}
console.dir(Function);
console.dir(fn);

在浏览器的Console界面都能找得到函数内部的prototype属性。

prototype属性是一个指针指向一个对象,所谓原型就是我们通过new关键字构造出来的对象实例的原型。

使用原型对象的好处是让对象实例共享它包含的属性和方法。换句话说,不必在构造着函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。

var fn = function (){
}

var fn2 = function (){
}

var proto = {
  name : "Tonni",
  say : function (){
      console.info("Hello World");
  }
}

fn.prototype = fn2.prototype = proto;

console.info(new fn().name); // output Tonni
console.info(new fn().name === new fn2().name); // output true

这里构造函数fnfn2的原型都指向了对象proto,当使用new关键字构造出来的对象都包含一个name属性,并且值都为Tonni

如果你在之前之细观察,你会发现每个函数的prototype里面都有一个constroctor的方法,通过名字我们猜测这是一个构造器,实际上它就是一个构造器,一个指向当前函数的构造器。

var fn = function (){};
console.info(fn.prototype.constructor === fn); // output true

我们在new一个构造函数时Javascript内部执行了4个步骤:

当我们调用构造函数创建出来一个新的对象实例时,该函数内部会有一个指针指向构造函数对象的原型对象。这个对象在Chrome、FireFox中叫做__proto__,在某些浏览器中这个属性是不可见的。我们可以使用浏览器的Console界面查看得到。

var fn = function (){
}
console.true(fn.__proto__ === Function.prototype); //output true

输出true,因为函数fn是通过内置全局对象Function构造器构造出来的一个实例。

链式查找

每当代码读取某个对象的某个属性时都会执行一次搜索,搜索先从对象本身开始,如果在对象的实例中找到了指定名字的属性则返回该属性,如果没有在实例中找到属性则会继续在对象的原型中查找该属性,然后重现上面的查找过程,如下:

var fn = function (){
    this.age = 24;
};
fn.prototype = {
    constructor : fn,
    name : "Tonni"
}

var fn2 = function (){
    this.company = "UCloud";
}
var proto = new fn();
proto["constructor"] = fn2;
fn2.prototype = proto;

var fn3 = function (){
    this.profession = "Web Developer";
}
var proto2 = new fn2();
proto2["constructor"] = fn3;
fn3.prototype = proto2;

var _obj = new fn3();
console.info(_obj.name); //output Tonni
console.info(_obj.age); //output 24
console.info(_obj.company); //output UCloud
console.info(_obj.profession) //output Web Developer

当然我们也可以对Javascript内置对象原型进行扩展:

Function.prototype._say = function (){
    console.info("My name is Tonni");
}
var fn = function (){}
fn._say(); //output My name is Tonni

原型模式常用方法

hasOwnProperty

hasOwnproPerty用来检查给定属性是存在对象实例中还是存在对象原型中。当给定属性存在于对象实例中而不是在对象实例的原型中时返回true

fn = function (){
    this.name = "Tonni";
    this.company = "UCloud";
}
fn.prototype.age = 24;
fn.prototype.company = "UCloud";
var _obj = new fn();
console.info(_obj.hasOwnProperty("name")); //output true
console.info(_obj.hasOwnProperty("age")); //output false
console.info(_obj.hasOwnProperty("company")); //output true
// rewrite age property of _obj
_obj.age = 25;
console.info(_obj.hasOwnProperty("age")); //output true

in

有两种方式使用in操作符:单独使用和for-in中循环使用。

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

var fn = function (){
    this.name = "Tonni";
}
fn.prototype.age = 24;
var _obj = new fn();

console.info("name" in _obj); //output true
console.info("age" in _obj); //output true

有了hasOwnPropertyin操作符我们就可以组合使用这两个方法确定给定属性是否存在于对象的原型中。给我们可以封装一个hasOwnPrototypeProperty的方法来确定给定属性是否存在于对象的原型中。

var hasPrototypeProperty = function (object, name){
    return !object.hasOwnProperty(name) && (name in object);
}
var fn = function (){
    this.name = "Tonni";
}
fn.prototype.age = 24;
var _obj = new fn();

console.info(hasPrototypeProperty(_obj, "age")); //output true
_obj.age = 24;
console.info(hasPrototypeProperty(_obj, "age")); //output false
console.info(hasPrototypeProperty(_obj, "name")); //output false

当使用for-in循环时,返回的是能够通过对象访问的、可枚举的属性,其中包括实例中的属性,也包括原型中的属性。

var fn = function (){
    this.name = "Tonni";
}
fn.prototype.age = 24;
var _obj = new fn();

// Creating a non enumerable property, ie8 not supported.
Object.defineProperty(_obj, "company", {
    enumerable : false,
    value : "UCloud"
});

// Can not get company
for(i in _obj){
    console.info(_obj[i]);
}

要取得所有对象上所有可枚举的属性可以使用ECMAScript 5的Object.keys()方法。这个方法接收一个对象作为参数,以数组的形式返回对象实例内所有可枚举的属性:

var fn = function (){
    this.name = "Tonni";
    this.company = "UCloud";
}
fn.prototype.age = 24;
var _obj = new fn();
    this.profession = "Web Developer";
Object.defineProperty(_obj, "profession", {
    enumerable : false,
    value : "Web Developer"
    })
console.info(Object.keys(_obj)); //output ["name", "company"]

如果想得到所有实例中的属性,无论是否可以枚举,可以使用Object.getOwnPropertyNames()方法。

var fn = function (){
    this.name = "Tonni";
    this.company = "UCloud";
}
fn.prototype.age = 24;
var _obj = new fn();
    this.profession = "Web Developer";
Object.defineProperty(_obj, "profession", {
    enumerable : false,
    value : "Web Developer"
    })
console.info(Object.getOwnPropertyNames(_obj)); //output ["name", "company", "profession"]

原型的动态性

由于原型中查找值是一次搜索过程,因此我们对原型做的任何改动都能立即在实例上反映出来,即使先实例化实例再修改原型。记住,原型只是一个指向对象的指针,而非原型的副本。

var fn = function (){
}
fn.prototype.name = "Tonni";
var _obj = new fn();
console.info(_obj.name); //output Tonni
console.info(_obj.age); //output undefined
fn.prototype.age = 24;
console.info(_obj.age); //output 24

如果将原型重写将会发生一个错误,因为实例指向的原型已经不再是构造实例时所指向的原型了,重写原型切断了现有原型与之前已经存在的任何实例之间的联系,实例引用的依然是最初的原型。

var fn = function (){
}
fn.prototype.name = "Tonni";
var _obj = new fn();
// rewrite properties
fn.prototype = {
    constructor : fn,
    age : 24,
    say_name : function (){
        console.info(this.name);
    }
}
console.info(_obj.age); //Error

原生对象

Javascript内置对象也是通过构造函数来创建的,我们也可以对Javascript原生对象的原型进行扩展:

Array.prototype.forEach = function (callback){
  for (var i = 0, length = this.length; i < length; i++) {
      callback(this[i]);
  }
};
var arr = new Array(
        "Js",
        "Java",
        "PHP",
        "ruby",
        "python"
        );

arr.forEach(function (item){
    console.info(item);
});

但是并不推荐这种做法,这样容易与Javascript标准冲突,也可能会重写方法,我还是比较推荐underscore的做法:绝不扩展原生对象。

原型模式的问题及解决办法

原型模式的最大问题在于共享的本性导致的。原型中所有属性都是被实例共享的,这种共享对于函数非常合适。对于那些包含基本类型值的属性倒也说得过去,毕竟,通过实例上添加一个同名属性,可以隐藏原型中对应的属性。然而对于引用类型值的属性,问题就比较突出了:

var fn = function (){}

fn.prototype = {
    constructor : fn,
    name : "Tonni",
    friends : ["Lu", "Shang"]
}

var person1 = new fn();
var person2 = new fn();
person1.friends.push("Zhang");

// The friends property of person2 has been changed
console.info(person2.friends);

解决此类问题最好的办法就是采用构造函数模式与原型模式项结合:将共享的属性和方法存放在原型中,在构造对象实例时定义实例属性,下面我们重写上面的例子:

var fn = function (){
    this.friends = ["Lu", "Shang"];
}

fn.prototype = {
    constructor : fn,
    name : "Tonni"
}

var person1 = new fn();
var person2 = new fn();
person1.friends.push("Zhang");
console.info(person1.friends);

// The friends property of person2 will not changed
console.info(person2.friends);
作者:Tonni's Blog
原文地址:理解Javascript原型链, 感谢原作者分享。