Javascript中的对象和继承

最近要写一些Javascript代码,发现Javascript相比其它的面向对象语言有着很大的不同。C++、Java等以类为基础 的(class-based)面向对象语言是通过子类-父类来实现继承的概念。Javascript只有对象,并没有“类(class)” 的概念(Javascript保留了“class”关键字,但是并没有使用)。

##概述 Javascript中的对象(假设为o)除了自有的属性(own property)之外,还维护着一个“链接”,这个“链接”指向另一个对象; 这个对象称为对象o的原型(prototype)。o的原型对象中也有一个“链接”指向它自己的原型。这样就形成了一个原型 链(prototype chain)。原型链以null为结尾。

Javascript中的对象在访问一个属性时,首先在自有属性中查找。如果没有找到,则会沿着原型链依次查看原型链上的对象是否 有该属性,直至找到或者到达原型链的终点。所以,一个对象的属性集合也包括了它原型链上的所有对象的所有属性;也就可以 说一个Javascript对象“继承”了它的原型。

Javascript在创建一个对象时会指派这个新建对象的原型。如果我们在创建不同的对象时指派同一个原型对象,那么就可以 认为这些对象“继承”自同一个“父亲(原型对象)”。

##名词解释

###[[Prototype]] 有时候会在文档中看到[[Prototype]]。[[Prototype]]源自ECMAScript标准,仅仅是一个符号标识(pure notation), 表示对象的“原型”。它是一个名词,或者说是一个概念。某些对象,比如Object或Function,它们有一个属性叫prototype。引 入[[Prototype]]应该是为了在英语环境中区分“叫prototype的属性”和“名词prototype(原型)”。

###__proto__ 在具体的Javascript实现中,__proto__是对象的一个属性,通过它我们可以访问这个对象的原型([[Prototype]])。

var obj = ...               //obj通过某种方式创建
obj.__proto__               //这就是obj的原型对象
obj.__proto__.some_property //假设原型对象有个属性叫some_property,我们可以这么访问
                            //当然,使用obj.some_property也是访问的

实际上,对象的__proto__属性继承自Object.prototype。__proto__是Object.prototype对象上的一个 “存取属性”(accessor property,由一对getter和setter函数组成的属性)。因为几乎所有对象的原型链上都有 Object.prototype对象,所以自然每个对象也都继承__proto__属性。obj.__proto__的值视对象的创建方式而定,见 下文。

要注意的是,不推荐使用__proto__,因为它已经被弃用了(deprecated)。如果要用__proto__的getter,推荐用 Object.getPrototypeOf来代替。另外,不推荐修改一个对象的原型,不管是通过obj.__proto__ = sth_new还是 Object.setPrototypeOf。修改对象原型在现代的Javascript实现上是一个非常慢的操作。通常,可以通过 Object.create在对象创建时指定新建对象的原型。

###prototype属性 Object、Array、Date、String等内建对象都有一个属性叫prototype,比如Object.prototype,String.prototype。 用户定义的函数对象也会有一个prototype属性。当通过关键字new或者“字面表达式(literal)”的方式创建对象 时,Javascript会把相应对象的prototype属性设置为新建对象的原型,即new_obj.__proto__ === SomeObj.prototype为 真。

var obj = {}                   //obj的原型是Object.prototype
var a = ["hey", "you", "!"];   //a的原型为Array.prototype

var obj2 = new String("hey!")  //obj2的原型是String.prototype
obj2.length                    //length是String.prototype的属性;通过原型,obj2也获得(继承)了这个属性

function Apple() {}
var obj3 = new Apple           //obj3的原型是Apple.prototype

##创建对象

###使用字面表达式(literal)

var o = {a: 1};
//对象o的原型是Object.prototype;原型链如下:
// o ---> Object.prototype ---> null

var a = ["hey", "you", "!"];
//对象o的原型是Array.prototype;继承了indexOf、forEach等方法;原型链如下:
// a ---> Array.prototype ---> Object.prototype ---> null

function f(){
  return "how u doing?";
}
//对象f的原型是Function.prototype;继承了call、bind等方法;原型链如下:
// f ---> Function.prototype ---> Object.prototype ---> null

###通过new关键字和构造函数 JavaScript中的“构造函数”只是普通的函数。当通过new关键字调用一个函数时,这个函数就可以叫做构造函数。

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

Person.prototype.sayHi = function() {  //增加一个方法
  console.log('hi');
}

var p = new Person();
//对象g有一个自有属性,name;p的原型是Person.prototype

###通过Object.create 调用Object.create函数会创建一个新的对象。传入的第一个参数作为新建对象的原型。

var a = {a: 1}; 
//原型链为:a ---> Object.prototype ---> null

var b = Object.create(a);
//原型链为:b ---> a ---> Object.prototype ---> null
console.log(b.a); // 输出1,对象b的属性a继承自它的原型a

var d = Object.create(null);
//原型链为:d ---> null; 这种情况下b没有继承任何Object.prototype里的属性,比如hasOwnProperty

##继承

###自有属性和覆盖 JavaScript对象有自有的属性(own properties),也有继承自原型的属性。当自有属性和继承来的属性同名时, 自有属性会覆盖继承来的属性。

//假设对象o原型链如下:
//{a:1, b:2} ---> {b:3, c:4} ---> null
//'a'和'b'是对象o的自有属性

console.log(o.b); //输出2,对象o的自有属性b覆盖了原型链上的属性b

o.e = 'hello';    //创建了一个新的自有属性e

通常,通过类似o.e = 'hello'的方式,对象会创建一个自有属性,无论原型链上是否已经存在一个同名的属性。 唯一的例外情况是,在原型链上存在一个同名的属性e,且e是一个存取属性(accessor property,有 getter和setter)。比如__proto__就是一个典型的存取属性,

var obj = {}
console.log(obj.hasOwnProperty('__proto__')) //false
obj.__proto__ = 1
console.log(obj.hasOwnProperty('__proto__')) //还是false;上一个语句不会创建一个自有属性

###this 对象调用继承来的函数时,this会指向当前的对象。

var o = {
  data: 1,
  func: function(){
    return this.data + 1;
  }
};

console.log(o.func());       //输出2,这个时候this指向o,this.data的值是1

var p = Object.create(o);    //p“继承”o

p.data = 10;                 //p创建一个自有属性data
console.log(p.func());       //输出11,当func被调用时,this指向p,this.data的值是10

###一个例子 例子来自MDN

var Person = function() {
    this.canTalk = true;
    this.greet = function() {
        if (this.canTalk) {
            console.log("Hi, I'm " + this.name);
        }
    };
};

var Employee = function(name, title) {
    this.name = name;
    this.title = title;
    this.greet = function() {      
        if (this.canTalk) {
            console.log("Hi, I'm " + this.name + ", the " + this.title);
        }
    };
};
Employee.prototype = new Person();        //假设new Person()返回的对象为p1,设置Employee.prototype为p1
                                          //以后通过new Employee创建对象,对象的原型为p1
var bob = new Employee('Bob','Builder');
//原型链为:bob-->Employee.prototype(也就是p1)-->Person.prototype-->Object.prototype-->...
//注:Person.prototype.__proto__ === Object.prototype为真,所以Person.prototype-->Object.prototype
//bob的自有属性有:name,title,greet(覆盖了原型/p1上的greet属性)
//继承自Employee.prototype(也就是p1)的属性有:canTalk,greet
//继承自原型链上其它对象的属性……

var Customer = function(name) {
    this.name = name;
};
Customer.prototype = new Person();
var joe = new Customer('Joe');

Employee.prototype.dept = "";           //给原型添加属性
bob.dept = "RD";                        //虽然bob早已创建,但是上条语句仍会发生作用

Employee.prototype = {foo: 'bar'};      //替换成一个新的对象
var mike = new Employee('mike', 'QA');  //mike的原型是新的对象;bob的原型还是维持不变

##其它

###原型链对性能的影响 在原型链上查找属性会对性能产生一定的影响。特别是查找不存在的属性时,要遍历原型链上的所有属性后才会以失败中止。 另外,要遍历一个对象的属性也会遍历整个原型链上的所有属性。

如果仅仅需要查看自有属性,那么可以使用hasOwnProperty方法。这个方法是JavaScript中唯一不会检查原型 链上属性的处理函数。

###preventExtensions 使用preventExtensions可以防止对象的__proto__被修改,见这里

另外注意下面两种修改的不同,

//假设原型链如下:o-->Apple.prototype-->...
o.__proto__ = obj1;      //现在原型链为:o-->obj1-->...

Apple.prototype = obj2;  //o的原型链保持不变,没有影响
var o2 = new Apple;      //后续新建的对象o2的原型链为o2-->obj2-->...

###其他代码示例

function Foo() {};
( new Foo ).__proto__ === Foo.prototype  //true, new Foo产生的新对象以Foo.prototype为原型
( new Foo ).prototype === undefined      //true, 新对象并没有一个属性叫prototype
var o = Object.create(new Foo)           //o的原型是new Foo返回的对象

###参考文档 Mozilla Developer Network(MDN)上有很多好的文档值得一看。比如,
MDN JavaScript Guide
Inheritance and the prototype chain
Object.prototype.__proto__
Object.prototype
Defining getters and setters
Stackoverflow上的一个回答

(测试环境:Chrome)

2014-07-26 17:30
推荐到豆瓣

如果你觉得这篇文章对你有用,可以微信扫一扫表示🙏 / If you find this post is useful to you, buy me 🍶 via Wechat