JavaScript 原型链

大部分面向对象的编程言语,都是以“类”(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要领是没有必要的。

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属性来完成相似的作用。以下是上面代码的革新示例:

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,将其睁开,能够看到下面的结果:

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属性上,所以它能够在一切的实例对象中猎取到。

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属性的另一个作用就是:供应了一种继承的完成形式。

function Super() {
    // ...
}

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

Sub.superclass = new Super();

上面代码中,SuperSub都是组织函数,在Sub内部的this上挪用Super,就会构成Sub继承Super的结果,miniui中是如许完成继承的:

mini.Control = function(el) {    
    mini.Control.superclass.constructor.apply(this, arguments);
    // ...
}
// 个中的superclass指代父类的prototype属性

我们自身写一个例子:

// 父类
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要领继承来,请看下面例子:

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,即:

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,反之亦然,这将引发逻辑杂沓。

因而我们在原型上举行扩大是,通常是增加属性,而不是替代为一个新对象。

// 好的写法
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)。


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之间的关联可用下图示意:

《JavaScript 原型链》

instranceof 运算符

instanceof运算符返回一个布尔值,示意指定对象是不是为某个组织函数的实例。因为原型链的关联,所谓的实例并不一定是某个组织函数的直接实例,更准确的形貌,应当是:返回一个后者的原型对象是不是在前者的原型链上

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 运算符差别,该要领会疏忽掉那些从原型链上继承到的属性。

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要领,用来推断一个对象是不是是另一个对象的原型。

var o1 = {};
var o2 = Object.create(o1);
var o3 = Object.create(o2);

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

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

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

看起来这个要领和instanceof运算符作用相似,但现实运用是不一样的

比方:

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要领返回一个对象的原型。这是猎取原型对象的规范要领。

// 空对象的原型是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要领能够为现有对象设置原型,返回一个新对象。吸收两个参数,第一个是现有对象,第二个是原型对象。

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属性,然后在实例对象上实行组织函数。

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())

上面代码举例:

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的悉数属性和要领。然则其自身属性为空,若须要为新对象增加自身属性,则运用第二个参数即可。

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"}

参考链接

更多可见JavaScript 原型链

    原文作者:依韵_宵音
    原文地址: https://segmentfault.com/a/1190000008766297
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞