JavaScript 原型链

发布于 2016-11-21 21:00 阅读数 375

本文必须得到作者授权后,方可转载,摘要引流随意。
By 依韵 , From https://blog.cdswyda.com/post/20161121

大部分面向对象的编程语言,都是以“类”(class)作为对象体系的语法基础。JavaScript语言中是没有class的概念的(ES6之前,ES6中虽然提供了class的写法,但实现原理并不是传统的“类”class概念,仅仅是一种写法), 但是它依旧可以实现面向对象的编程,这就是通过JavaScript中的“原型对象”(prototype)来实现的。

prototype 属性

请看这样一个例子:

function Person(name, gender) {
    this.name = name;
    this.gender = gender;
    this.sayHello = function() {
        console.log('Hello,I am', this.name, '. I\'m a', this.gender);
    };
}

这样定义了一个构造函数,我们创建对象就可以使用这个构造函数作为模板来生成。不过以面向对象的思想来看,不难发现其中的一点问题:namegender属性是每个实例都各不相同,作为一个自身的属性没有问题,而sayHello方法,每个实例对象应该都有,而且都一样,给每个实例对象一个全新的、完全不同(虽然代码内容一样,但JavaScript中每个sayHello的值都在内存中单独存在)的sayHello方法是没有必要的。

1
2
3
4
5
6
7
var zs = new Person('zhang san', 'male'),
    xh = new Person('xiao hong', 'female');

zs.sayHello(); // Hello,I am zhang san . I'm a male
xh.sayHello(); // Hello,I am xiao hong . I'm a female

zs.sayHello === xh.sayHello;  // false

上面代码中展示了zs.sayHellxh.sayHello这两个作用相同,而且看起来代码内容也是完全一样的对象,实际是两个独立的,互不相关的对象。

面向对象思想中,是将公共的、抽象的属性和方法提取出来,作为一个基类,子类继承这个基类,从而继承到这些属性和方法。而JavaScript中则可以通过prototype属性来实现类似的作用。以下是上面代码的改进示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function Person(name, gender) {
    this.name = name;
    this.gender = gender;
}
Person.prototype.sayHello = function() {
    console.log('Hello,I am', this.name, '. I\'m a', this.gender);
};

var zs = new Person('zhang san', 'male'),
    xh = new Person('xiao hong', 'female');

zs.sayHello(); // Hello,I am zhang san . I'm a male
xh.sayHello(); // Hello,I am xiao hong . I'm a female

zs.sayHello === xh.sayHello;  // true

这时将sayHello方法定义到Person对象上的prototype属性上,取代了在构造函数中给每个实例对象添加sayHello方法。可以看到,其还能实现和之前相同的作用,而且zs.sayHellxh.sayHello是相同的内容,这样就很贴近面向对象的思想了。那么zsxh这两个对象,是怎么访问到这个sayHello方法的呢?

在浏览器控制台中打印出zs,将其展开,可以看到下面的结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
zs;
/**
 * 
Person
    gender: "male"
    name: "zhang san"
    __proto__: Object
        constructor: function Person(name, gender) 
            arguments: null
            caller: null
            length: 2 
            name: "Person"
            prototype: Object
        sayHello:function()
            arguments:null
            caller:null
            length:0
            name:""
            prototype:Object
*/

zs这个对象只有两个自身的属性gendername,这和其构造函数Person的模板相同,并且可以在Person对象的__proto__属性下找到sayHello方法。那么这个__proto__是什么呢?它是浏览器环境下部署的一个对象,它指的是当前对象的原型对象,也就是构造函数的prototype属性。

现在就可以明白了,我们给构造函数Person对象的prototype属性添加了sayHello方法,zsxh这两个通过Person构造函数产生的对象,是可访问到Person对象的prototype属性的,所以我们定义在prototype下的sayHello方法,Person的实例对象都可以访问到。

关于构造函数的new命令原理是这样的:

  1. 创建一个空对象,作为将要返回的对象实例
  2. 将这个空对象的原型,指向构造函数的prototype属性
  3. 将这个空对象赋值给函数内部的this关键字
  4. 开始执行构造函数内部的代码

constructor 属性

prototype下有一个属性constructor,默认指向此prototype对象所在的构造函数。

如上例中的zs__proto__constructor值为function Person(name, gender)

由于此属性定义在prototype属性上,所以它可以在所有的实例对象中获取到。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
zs.constructor;
// function Person(name, gender) {
//     this.name = name;
//     this.gender = gender;
// }

zs.hasOwnProperty('constructor'); // false
zs.constructor === Person; // true

zs.constructor === Function; // false
zs.constructor === Object; // false

constructor属性放在prototype属性中的一个作用是,可以通过这个属性来判断这个对象是由哪个构造函数产生的,上面代码中,zs是由Person构造函数产生的,而不是Function或者Object构造函数产生。

constructor属性的另一个作用就是:提供了一种继承的实现模式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function Super() {
    // ...
}

function Sub() {
    Sub.superclass.constructor.call(this);
    // ...
}

Sub.superclass = new Super();

上面代码中,SuperSub都是构造函数,在Sub内部的this上调用Super,就会形成Sub继承Super的效果,miniui中是这样实现继承的:

1
2
3
4
5
mini.Control = function(el) {    
    mini.Control.superclass.constructor.apply(this, arguments);
    // ...
}
// 其中的superclass指代父类的prototype属性

我们自己写一个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 父类
function Animal(name) {
    this.name = name;
    this.introduce = function() {
        console.log('Hello , My name is', this.name);
    }
}
Animal.prototype.sayHello = function() {
    console.log('Hello, I am:', this.name);
}

// 子类
function Person(name, gender) {
    Person.superclass.constructor.apply(this, arguments);
    this.gender = gender;
}
Person.superclass = new Animal();

// 子类
function Dog(name) {
    Dog.superclass.constructor.apply(this, arguments);  
}
Dog.superclass = new Animal();

基本原理就是在子类中使用父类的构造函数。在PersonDog中均没有对name属性和introduce方法进行操作,只是使用了父类Animal的构造函数,就可以将name属性和introduce方法继承来,请看下面例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var zs = new Person('zhang san', 'male');

zs; // Person {name: "zhang san", gender: "male"}
zs.sayHello(); // Uncaught TypeError: zs.sayHello is not a function(…)
zs.introduce(); // Hello , My name is zhang san

var wangCai = new Dog("旺财");

wangCai; // Dog {name: "旺财"}
wangCai.introduce(); // Hello , My name is 旺财

确实实现了我们需要的效果。可是我们发现在调用zs.sayHello()时报错了。为什么呢?

其实不难发现问题,我们的Person.superclassAnimal的一个实例,是有sayHello方法的,但是我们在Perosn构造函数的内部,只是使用了Person.superclass.constructor。而Person.superclass.constructor指的仅仅是Animal构造函数本身,并没有包括Animal.prototype,所以没有sayHello方法。

一种改进方法是:将自定义的superclass换为prototype,即:

1
2
3
4
5
6
7
8
9
function Person(name, gender) {
    Person.prototype.constructor.apply(this, arguments);
    this.gender = gender;
}
Person.prototype = Animal.prototype;

var zs = new Person('zhang san', 'male');
zs.sayHello(); // Hello, I am: zhang san
zs.introduce() // Hello , My name is zhang san

这样就全部继承到了Animal.prototype下的方法。

但是一般不要这样做,上面写法中Person.prototype = Animal.prototype; 等号两端都是一个完整的对象,进行赋值时,Person.prototype的原对象完全被Animal.prototype替换,切断了和之前原型链的联系,而且此时Person.prototypeAnimal.prototype是相同的引用,给Person.prototype 添加的属性方法也将添加到Animal.prototype,反之亦然,这将引起逻辑混乱。

因此我们在原型上进行扩展是,通常是添加属性,而不是替换为一个新对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 好的写法
Person.prototype.sayHello = function() {
    console.log('Hello,I am', this.name, '. I\'m a', this.gender);
};
Person.prototype. // .. 其他属性 

// 不好的写法
Person.prototype = {
    sayHello:function(){
        console.log('Hello,I am', this.name, '. I\'m a', this.gender);
    },
    // 其他属性方法 ...
}

JavaScript 原型链

JavaScript的所有对象都有构造函数,而所有构造函数都有prototype属性(其实是所有函数都有prototype属性),所以所有对象都有自己的原型对象。

对象的属性和方法,有可能是定义在自身,也有可能是定义在它的原型对象。由于原型本身也是对象,又有自己的原型,所以形成了一条原型链(prototype chain)。

1
2
3
zs.sayHello(); // Hello,I am zhang san . I'm a male

zs.toString(); // "[object Object]"

例如上面的zs对象,它的原型对象是Personprototype属性,而Personprototype本身也是一个对象,它的原型对象是Object.prototype

zs本身没有sayHello方法,JavaScript通过原型链向上继续寻找,在Person.prototype上找到了sayHello方法。toString方法在zs对象本身上没有,Person.prototype上也没有,因此继续沿原型链查找,最终可以在Object.prototype上找到了toString方法。

Object.prototype的原型指向null,由于null没有任何属性,因此原型链到Object.prototype终止,所以Object.prototype是原型链的最顶端。

“原型链”的作用是,读取对象的某个属性时,JavaScript引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的Object.prototype还是找不到,则返回undefined

如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做“覆盖”(overiding)。

JavaScript中通过原型链实现了类似面向对象编程语言中的继承,我们在复制一个对象时,只用复制其自身的属性即可,无需将整个原型链进行一次复制,Object.prototype下的hasOwnProperty方法可以判断一个属性是否是该对象自身的属性。

实例对象构造函数prototype之间的关系可用下图表示:

instranceof 运算符

instanceof运算符返回一个布尔值,表示指定对象是否为某个构造函数的实例。由于原型链的关系,所谓的实例并不一定是某个构造函数的直接实例,更准确的描述,应该是:返回一个后者的原型对象是否在前者的原型链上

1
2
3
4
5
6
zs instanceof Person; // true
zs instanceof Object ;// true 

var d = new Date();
d instanceof Date; // true
d instanceof Object; // true

原型链相关属性和方法

Object.prototype.hasOwnProperty()

hasOwnProperty()方法用来判断某个对象是否含有指定的自身属性。这个方法可以用来检测一个对象是否含有特定的自身属性,和 in 运算符不同,该方法会忽略掉那些从原型链上继承到的属性。

1
2
3
4
5
6
7
8
zs.hasOwnProperty('name'); // true
zs.hasOwnProperty('gender'); // true

zs.hasOwnProperty('sayHello'); // fasle
Person.prototype.hasOwnProperty('sayHello'); // true 

zs.hasOwnProperty('toString'); // fasle
Object.prototype.hasOwnProperty('toString'); // true

Object.prototype.isPrototypeOf()

对象实例的isPrototypeOf方法,用来判断一个对象是否是另一个对象的原型。

1
2
3
4
5
6
var o1 = {};
var o2 = Object.create(o1);
var o3 = Object.create(o2);

o2.isPrototypeOf(o3) // true
o1.isPrototypeOf(o3) // true

上面代码表明,只要某个对象处在原型链上,isProtypeOf都返回true

1
2
3
4
Object.prototype.isPrototypeOf({}) // true
Object.prototype.isPrototypeOf([]) // true
Object.prototype.isPrototypeOf(/xyz/) // true
Object.prototype.isPrototypeOf(Object.create(null)) // false

看起来这个方法和instanceof运算符作用类似,但实际使用是不一样的

例如:

1
2
3
4
zs instanceof Person ; // true;

Person.isPrototypeOf(zs);// false
Person.prototype.isPrototypeOf(zs); // true

zs instanceof Person可理解为判断Person.prototype在不在zs的原型链上。 而Person.isPrototypeOf(zs)指的就是Person本身在不在zs的原型链上,所以返回false,只有Person.prototype.isPrototypeOf(zs)才为true

Object.getPrototypeOf()

ES5Object.getPrototypeOf方法返回一个对象的原型。这是获取原型对象的标准方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 空对象的原型是Object.prototype
Object.getPrototypeOf({}) === Object.prototype
// true

// 函数的原型是Function.prototype
function f() {}
Object.getPrototypeOf(f) === Function.prototype
// true

// f 为 F 的实例对象,则 f 的原型是 F.prototype
var f = new F();
Object.getPrototypeOf(f) === F.prototype
// true

Object.getPrototypeOf("foo");
// TypeError: "foo" is not an object (ES5 code)
Object.getPrototypeOf("foo");
// String.prototype                  (ES6 code)

此方法是ES5方法,需要IE9+。在ES5中,参数只能是对象,否则将抛出异常,而在ES6中,此方法可正确识别原始类型。

Object.setPrototypeOf()

ES5Object.setPrototypeOf方法可以为现有对象设置原型,返回一个新对象。接受两个参数,第一个是现有对象,第二个是原型对象。

1
2
3
4
5
6
var a = {x: 1};
var b = Object.setPrototypeOf({}, a);
// 等同于
// var b = {__proto__: a};

b.x // 1

上面代码中,b对象是Object.setPrototypeOf方法返回的一个新对象。该对象本身为空、原型为a对象,所以b对象可以拿到a对象的所有属性和方法。b对象本身并没有x属性,但是JavaScript引擎找到它的原型对象a,然后读取ax属性。

new命令通过构造函数新建实例对象,实质就是将实例对象的原型,指向构造函数的prototype属性,然后在实例对象上执行构造函数。

1
2
3
4
5
6
7
var F = function () {
  this.foo = 'bar';
};

// var f = new F();等同于下面代码
var f = Object.setPrototypeOf({}, F.prototype);
F.call(f);

Object.create()

ES5Object.create方法用于从原型对象生成新的实例对象,它接收两个参数:第一个为一个对象,新生成的对象完全继承前者的属性(即新生成的对象的原型此对象);第二个参数为一个属性描述对象,此对象的属性将会被添加到新对象。(关于属性描述对象可参考:MDN - Object.defineProperty())

上面代码举例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var zs = new Person('zhang san', 'male');

var zs_clone = Object.create(zs);

zs_clone; // {}
zs_clone.sayHello(); // Hello,I am zhang san . I'm a male
zs_clone.__proto__ === zs; // true
// Person
//  __proto__: Person
//      gender: "male"
//      name: "zhang san"
//      __proto__: Object

可以 看出 创建的新对象zs_clone的原型为zs,从而获得了zs的全部属性和方法。但是其自身属性为空,若需要为新对象添加自身属性,则使用第二个参数即可。

1
2
3
4
5
6
var zs_clone = Object.create(zs, {
    name: { value: 'zhangsan\'s clone' },
    gender: { value: 'male' },
    age: { value: '25' }
});
zs_clone; // Person {name: "zhangsan's clone", gender: "male", age: "25"}

参考链接


Comments
Write a Comment