在典型的面向對象的語言中,如java,都存在類(class)的概念,類就是對象的模板,對象就是類的實例。可是在Javascript語言體系中,是不存在類(Class)的概念的,javascript中不是基於‘類的’,而是經過構造函數(constructor)和原型鏈(prototype chains)實現的。可是在ES6中提供了更接近傳統語言的寫法,引入了Class(類)這個概念,做爲對象的模板。經過class
關鍵字,能夠定義類。基本上,ES6的class
能夠看做只是一個語法糖,它的絕大部分功能,ES5均可以作到,新的class
寫法只是讓原型對象的寫法更加清晰、更像面向對象編程的語法而已。javascript
按照個人習慣,在寫文章前我會給出文章目錄。html
如下內容會分爲以下小節:java
1.構造函數的簡單介紹編程
2.構造函數的缺點數組
3.prototype屬性的做用函數
4.原型鏈(prototype chain)post
5.constructor屬性性能
5.1:constructor屬性的做用this
6.instanceof運算符url
1.構造函數的簡單介紹
在個人一篇Javascript 中構造函數與new命令的密切關係文章中,詳細了介紹了構造函數的概念和特色,new命令的原理和用法等,若是對於構造函數不熟悉的同窗,能夠前往細細品味。如下作一個簡單的回顧。
所謂構造函數,就是提供了一個生成對象的模板並描述對象的基本結構的函數。一個構造函數,能夠生成多個對象,每一個對象都有相同的結構。總的來講,構造函數就是對象的模板,對象就是構造函數的實例。
構造函數的特色有:
a:構造函數的函數名首字母必須大寫。
b:內部使用this對象,來指向將要生成的對象實例。
c:使用new操做符來調用構造函數,並返回對象實例。
看一個最簡單的一個例子。
1 function Person(){ 2 this.name = 'keith'; 3 } 4 5 var boy = new Person(); 6 console.log(boy.name); //'keith'
2.構造函數的缺點
全部的實例對象均可以繼承構造函數中的屬性和方法。可是,同一個對象實例之間,沒法共享屬性。
1 function Person(name,height){ 2 this.name=name; 3 this.height=height; 4 this.hobby=function(){ 5 return 'watching movies'; 6 } 7 } 8 9 var boy=new Person('keith',180); 10 var girl=new Person('rascal',153); 11 12 console.log(boy.name); //'keith' 13 console.log(girl.name); //'rascal' 14 console.log(boy.hobby===girl.hobby); //false
上面代碼中,一個構造函數Person生成了兩個對象實例boy和girl,而且有兩個屬性和一個方法。可是,它們的hobby方法是不同的。也就是說,每當你使用new來調用構造函數放回一個對象實例的時候,都會建立一個hobby方法。這既沒有必要,又浪費資源,由於全部hobby方法都是一樣的行爲,徹底能夠被兩個對象實例共享。
因此,構造函數的缺點就是:同一個構造函數的對象實例之間沒法共享屬性或方法。
3.prototype屬性的做用
爲了解決構造函數的對象實例之間沒法共享屬性的缺點,js提供了prototype屬性。
js中每一個數據類型都是對象(除了null和undefined),而每一個對象都繼承自另一個對象,後者稱爲「原型」(prototype)對象,只有null除外,它沒有本身的原型對象。
原型對象上的全部屬性和方法,都會被對象實例所共享。
經過構造函數生成對象實例時,會將對象實例的原型指向構造函數的prototype屬性。每個構造函數都有一個prototype屬性,這個屬性就是對象實例的原型對象。
1 function Person(name,height){ 2 this.name=name; 3 this.height=height; 4 } 5 6 Person.prototype.hobby=function(){ 7 return 'watching movies'; 8 } 9 10 var boy=new Person('keith',180); 11 var girl=new Person('rascal',153); 12 13 console.log(boy.name); //'keith' 14 console.log(girl.name); //'rascal' 15 console.log(boy.hobby===girl.hobby); //true
上面代碼中,若是將hobby方法放在原型對象上,那麼兩個實例對象都共享着同一個方法。我但願你們都能理解的是,對於構造函數來講,prototype是做爲構造函數的屬性;對於對象實例來講,prototype是對象實例的原型對象。因此prototype便是屬性,又是對象。
原型對象的屬性不是對象實例的屬性。對象實例的屬性是繼承自構造函數定義的屬性,由於構造函數內部有一個this關鍵字來指向將要生成的對象實例。對象實例的屬性,其實就是構造函數內部定義的屬性。只要修改原型對象上的屬性和方法,變更就會馬上體如今全部對象實例上。
1 Person.prototype.hobby=function(){ 2 return 'swimming'; 3 } 4 console.log(boy.hobby===girl.hobby); //true 5 console.log(boy.hobby()); //'swimming' 6 console.log(girl.hobby()); //'swimming'
上面代碼中,當修改了原型對象的hobby方法以後,兩個對象實例都發生了變化。這是由於對象實例實際上是沒有hobby方法,都是讀取原型對象的hobby方法。也就是說,當某個對象實例沒有該屬性和方法時,就會到原型對象上去查找。若是實例對象自身有某個屬性或方法,就不會去原型對象上查找。
1 boy.hobby=function(){ 2 return 'play basketball'; 3 } 4 console.log(boy.hobby()); //'play basketball' 5 console.log(girl.hobby()); //'swimming'
上面代碼中,boy對象實例的hobby方法修改時,就不會在繼承原型對象上的hobby方法了。不過girl仍然會繼承原型對象的方法。
總結一下:
a:原型對象的做用,就是定義全部對象實例所共享的屬性和方法。
b:prototype,對於構造函數來講,它是一個屬性;對於對象實例來講,它是一個原型對象。
4.原型鏈(prototype chains)
對象的屬性和方法,有多是定義在自身,也有多是定義在它的原型對象。因爲原型對象自己對於對象實例來講也是對象,它也有本身的原型,因此造成了一條原型鏈(prototype chain)。好比,a
對象是b
對象的原型,b
對象是c
對象的原型,以此類推。全部一切的對象的原型頂端,都是Object.prototype,即Object構造函數的prototype屬性指向的那個對象。
固然,Object.prototype對象也有本身的原型對象,那就是沒有任何屬性和方法的null對象,而null對象沒有本身的原型。
1 console.log(Object.getPrototypeOf(Object.prototype)); //null 2 console.log(Person.prototype.isPrototypeOf(boy)) //true
原型鏈(prototype chain)的特色有:
a:讀取對象的某個屬性時,JavaScript引擎先尋找對象自己的屬性,若是找不到,就到它的原型去找,若是仍是找不到,就到原型的原型去找。若是直到最頂層的Object.prototype
仍是找不到,則返回undefined
。
b:若是對象自身和它的原型,都定義了一個同名屬性,那麼優先讀取對象自身的屬性,這叫作「覆蓋」(overiding)。
c:一級級向上在原型鏈尋找某個屬性,對性能是有影響的。所尋找的屬性在越上層的原型對象,對性能的影響越大。若是尋找某個不存在的屬性,將會遍歷整個原型鏈。
看概念可能比較晦澀,咱們來看一個例子。可是理解了概念真的很重要。
1 var arr=[1,2,3]; 2 console.log(arr.length); //3 3 console.log(arr.valueOf()) //[1,2,3] 4 console.log(arr.join('|')) //1|2|3
上面代碼中,定了一個數組arr,數組裏面有三個元素。咱們並無給數組添加任何屬性和方法,但是卻在調用length,join(),valueOf()時,卻不會報錯。
length屬性是繼承自Array.prototype的,屬於原型對象上的一個屬性。join方法也是繼承自Array.prototype的,屬於原型對象上的一個方法。這兩個方法是全部數組所共享的。當實例對象上沒有這個length屬性時,就會去原型對象查找。
valueOf方法是繼承自Object.prototype的。首先,arr數組是沒有valueOf方法的,因此就到原型對象Array.prototype查找。而後,發現Array.prototype對象上沒有valueOf方法。最後,再到它的原型對象Object.prototype查找。
來看看Array.prototype對象和Object.prototype對象分別有什麼屬性和方法。
1 console.log(Object.getOwnPropertyNames(Array.prototype)) 2 //["length", "toSource", "toString", "toLocaleString", "join", "reverse", "sort", "push", "pop", "shift", "unshift", "splice", "concat", "slice", "lastIndexOf", "indexOf", "forEach", "map", "filter", "reduce", "reduceRight", "some", "every", "find", "findIndex", "copyWithin", "fill", "entries", "keys", "values", "includes", "constructor", "$set", "$remove"] 3 console.log(Object.getOwnPropertyNames(Object.prototype)) 4 // ["toSource", "toString", "toLocaleString", "valueOf", "watch", "unwatch", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "__defineGetter__", "__defineSetter__", "__lookupGetter__", "__lookupSetter__", "__proto__", "constructor"]
我相信,你們看到這裏,對prototype仍是似懂非懂的。這很正常,畢竟是js中比較重要又比較抽象的概念,不可能那麼快就掌握,再啃多幾篇,說不定掌握其精髓。在某乎上,有一個活生生的實例,可能也是你們會遇到的問題。能夠看看 js構造函數和原型對象。
5.constructor屬性
prototype
對象有一個constructor
屬性,默認指向prototype
對象所在的構造函數。
1 function A(){}; 2 console.log(A.prototype.constructor===A) //true
要注意的是,prototype是構造函數的屬性,而constructor則是構造函數的prototype屬性所指向的那個對象,也就是原型對象的屬性。注意不要混淆。
1 console.log(A.hasOwnProperty('prototype')); //true 2 console.log(A.prototype.hasOwnProperty('constructor')); //true
因爲constructor
屬性是定義在原型(prototype)
對象上面,意味着能夠被全部實例對象繼承。
1 function A(){}; 2 var a=new A(); 3 4 console.log(a.constructor); //A() 5 console.log(a.constructor===A.prototype.constructor);//true
上面代碼中,a
是構造函數A
的實例對象,可是a
自身沒有contructor
屬性,該屬性實際上是讀取原型鏈上面的A.prototype.constructor
屬性。
5.1:constructor屬性的做用
a:分辨原型對象到底屬於哪一個構造函數
1 function A(){}; 2 var a=new A(); 3 4 console.log(a.constructor===A) //true 5 console.log(a.constructor===Array) //false
上面代碼表示,使用constructor
屬性,肯定實例對象a
的構造函數是A
,而不是Array
。
b:從實例新建另外一個實例
1 function A() {}; 2 var a = new A(); 3 var b = new a.constructor(); 4 console.log(b instanceof A); //true
上面代碼中,a
是構造函數A
的實例對象,能夠從a.constructor
間接調用構造函數。
c:調用自身的構造函數成爲可能
1 A.prototype.hello = function() { 2 return new this.constructor(); 3 }
d:提供了一種從構造函數繼承另一種構造函數的模式
1 function Father() {} 2 3 function Son() { 4 Son.height.constructor.call(this); 5 } 6 7 Son.height = new Father();
上面代碼中,Father
和Son
都是構造函數,在Son
內部的this
上調用Father
,就會造成Son
繼承Father
的效果。
e:因爲constructor屬性是一種原型對象和構造函數的關係,因此在修改原型對象的時候,必定要注意constructor的指向問題。
解決方法有兩種,要麼將constructor屬性指向原來的構造函數,要麼只在原型對象上添加屬性和方法,避免instanceof失真。
6.instanceof運算符
instanceof
運算符返回一個布爾值,表示指定對象是否爲某個構造函數的實例。
1 function A() {}; 2 var a = new A(); 3 console.log(a instanceof A); //true
由於instanceof對整個原型鏈上的對象都有效,因此同一個實例對象,可能會對多個構造函數都返回true。
1 function A() {}; 2 var a = new A(); 3 console.log(a instanceof A); //true 4 console.log(a instanceof Object); //true
注意,instanceof對象只能用於複雜數據類型(數組,對象等),不能用於簡單數據類型(布爾值,數字,字符串等)。
1 var x = [1]; 2 var o = {}; 3 var b = true; 4 var c = 'string'; 5 console.log(x instanceof Array); //true 6 console.log(o instanceof Object); //true 7 console.log(b instanceof Boolean); //false 8 console.log(c instanceof String); //false
此外,null和undefined都不是對象,因此instanceof 老是返回false。
1 console.log(null instanceof Object); //false 2 console.log(undefined instanceof Object); //false
利用instanceof
運算符,還能夠巧妙地解決,調用構造函數時,忘了加new
命令的問題。
1 function Keith(name,height) { 2 if (! this instanceof Keith) { 3 return new Keith(name,height); 4 } 5 this.name = name; 6 this.height = height; 7 }
上面代碼中,使用了instanceof運算符來判斷函數體內的this關鍵字是否指向構造函數Keith的實例,若是不是,就代表忘記加new命令,此時構造函數會返回一個對象實例,避免出現意想不到的結果。
由於限於篇幅的緣由,暫時介紹到這裏。
我會在下次的分享中談談原型(prototype)對象的一些原生方法,如Object.getPrototypeOf(),Object.setPrototypeOf()等,而且介紹獲取原生對象方法的比較。
完。
感謝你們的閱讀。