大部分面向对象的编程语言,都是以“类”(class
)做为对象体系的语法基础。JavaScript
语言中是没有class
的概念的(ES6以前,ES6中虽然提供了class
的写法,但实现原理并非传统的“类”class
概念,仅仅是一种写法), 可是它依旧能够实现面向对象的编程,这就是经过JavaScript
中的“原型对象”(prototype
)来实现的。javascript
请看这样一个例子:html
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
方法是没有必要的。java
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
方法的呢?app
在浏览器控制台中打印出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
的实例对象均可以访问到。oop
关于构造函数的new
命令原理是这样的:post
建立一个空对象,做为将要返回的对象实例
将这个空对象的原型,指向构造函数的
prototype
属性将这个空对象赋值给函数内部的
this
关键字开始执行构造函数内部的代码
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
的全部对象都有构造函数,而全部构造函数都有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
之间的关系可用下图表示:
instanceof
运算符返回一个布尔值,表示指定对象是否为某个构造函数的实例。因为原型链的关系,所谓的实例并不必定是某个构造函数的直接实例,更准确的描述,应该是:返回一个后者的原型对象是否在前者的原型链上
zs instanceof Person; // true zs instanceof Object ;// true var d = new Date(); d instanceof Date; // true d instanceof Object; // true
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
对象实例的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
。
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中,此方法可正确识别原始类型。
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);
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 原型链