大部分面向对象的编程言语,都是以“类”(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);
};
}
如许定义了一个组织函数,我们建立对象就能够运用这个组织函数作为模板来天生。不过以面向对象的头脑来看,不难发明个中的一点题目:name
和gender
属性是每一个实例都各不雷同,作为一个自身的属性没有题目,而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.sayHell
和xh.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.sayHell
和xh.sayHello
是雷同的内容,如许就很切近面向对象的头脑了。那末zs
和xh
这两个对象,是怎样访问到这个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
这个对象只需两个自身的属性gender
和name
,这和其组织函数Person
的模板雷同,而且能够在Person
对象的__proto__
属性下找到sayHello
要领。那末这个__proto__
是什么呢?它是浏览器环境下布置的一个对象,它指的是当前对象的原型对象,也就是组织函数的prototype
属性。
如今就能够邃晓了,我们给组织函数Person
对象的prototype
属性增加了sayHello
要领,zs
和xh
这两个经由过程Person
组织函数发生的对象,是可访问到Person
对象的prototype
属性的,所以我们定义在prototype
下的sayHello
要领,Person
的实例对象都能够访问到。
关于组织函数的new
敕令道理是如许的:
建立一个空对象,作为将要返回的对象实例
将这个空对象的原型,指向组织函数的
prototype
属性将这个空对象赋值给函数内部的
this
关键字最先实行组织函数内部的代码
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();
上面代码中,Super
和Sub
都是组织函数,在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();
基本道理就是在子类中运用父类的组织函数。在Person
和Dog
中均没有对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.superclass
是Animal
的一个实例,是有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.prototype
和Animal.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
对象,它的原型对象是Person
的prototype
属性,而Person
的prototype
自身也是一个对象,它的原型对象是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
运算符返回一个布尔值,示意指定对象是不是为某个组织函数的实例。因为原型链的关联,所谓的实例并不一定是某个组织函数的直接实例,更准确的形貌,应当是:返回一个后者的原型对象是不是在前者的原型链上
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
,然后读取a
的x
属性。
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 原型链