原文發佈在個人博客javascript
咱們都知道 JavaScript 是一門基於原型的語言。當咱們調用一個對象自己沒有的屬性時,JavaScript 就會從對象的原型對象上去找該屬性,若是原型上也沒有該屬性,那就去找原型的原型,一直找原型鏈的末端也就是 Object.prototype
的原型 null
。這種屬性查找的方式咱們稱之爲原型鏈。java
因爲 JavaScript 自己是沒有的類的感念的。因此咱們若是要實現一個類,通常是經過構造函數來模擬類的實現:segmentfault
function Person(name,age){ //實現一個類 this.name = name; this.age = age; } var you = new Person('you',23); //經過 new 來新建實例
首先新建一個 Person
的構造函數,爲了和通常的函數區別,咱們會使用 CamelCase 方式來命名構造函數。
而後經過 new
操做符來建立實例,new
操做符其實幹了這麼幾件事:ruby
建立一個繼承自 Person.prototype
的新對象函數
構造函數 Person
執行時,相應的參數傳入,同時上下文被指定爲這個新建的對象。this
若是構造函數返回了一個對象,那麼這個對象會取代 new
的結果。若是構造函數返回的不是對象,則會忽略這個返回值。prototype
返回值不是對象 function Person(name){ this.name = name; return 'person' } var you = new Person('you'); // you 的值: Person {name: "you"} 返回值是對象 function Person(name){ this.name = name; return [1,2,3] } var you = new Person('you'); // you的值: [1,2,3]
若是類的實例須要共享類的方法,那麼就須要給構造函數的 prototype
屬性添加方法了。由於 new
操做符建立的對象都繼承自構造函數的 prototype
屬性。他們能夠共享定義在類 prototype
上的方法和屬性。code
function Person(name,age){ this.name = name; this.age = age; } Person.prototype = { sayName: function(){ console.log('My name is',this.name); } } var you = new Person('you',23); var me = new Person('me',23); you.sayName() // My name is you. me.sayName() // My name is me.
JavaScript 中經常使用的繼承方式是組合繼承,也就是經過構造函數和原型鏈繼承同時來模擬繼承的實現。對象
//Person 構造函數如上 function Student(name,age,clas){ Person.call(this,name,age) this.clas = clas; } Student.prototype = Object.create(Person.prototype); // Mark 1 Student.constructor = Student; //若是不指明,則 Student 會找不到 constructor Student.prototype.study = function(){ console.log('I study in class',this.clas) }; var liming = new Student('liming',23,7); liming instanceof Person //true liming instanceof Student //true liming.sayName(); // My name is liming liming.study(); // I study in class 7
代碼中 Mark 1 用到了 Object.create
方法。這個是 ES5 中新增的方法,用來建立一個擁有指定原型的對象。若是環境不兼容,能夠用下面這個 Polyfill 來實現(僅實現第一個參數)。繼承
if(!Object.create){ Object.create = function(obj){ function F(){}; F.prototype = obj; return new F(); } }
其實就是把 obj
賦值給臨時函數 F
,而後返回一個 F
的實例。這樣經過代碼 Mark 1 Student
就獲得了 Person.prototype
上的全部屬性。有人會問了,那麼爲何不乾脆把 Person.prototype
直接賦值給 Student.prototype
呢?
是的,直接賦值是能夠達到子類共享父類 prototype
的目的,可是它破壞了原型鏈。即:子類和父類共用了同一個 prototype
,這樣當某一個子類修改 prototype
的時候,其實同時也修改了父類的 prototype
,那麼就會影響到全部基於這個父類建立的子類,這並非咱們想要的結果。看例子:
//Person 同上 //Student 同上 Student.prototype = Person.prototype; Student.prototype.sayName = function(){ console.log('My name is',this.name,'my class is',this.clas) } var liming = new Student('liming',23,7) liming.sayName() //My name is liming,my class is 7; //另外一個子類 function Employee(name,age,salary){ Person.call(name,age); this.salary = salary; } Employee.prototype = Person.prototype; var emp = new Employee('emp',23,10000); emp.sayName() //Mark 2
大家猜 Mark 2 會輸出什麼?
咱們指望的 Mark 2 應該會輸出 "My name is emp". 但實際上報錯,爲何呢?由於咱們改寫 Student.prototype
的時候,也同時修改了 Person.prototype
,最終致使 emp
繼承的 prototype
是咱們所不指望的,它的 sayName 方法是 My name is',this.name,'my class is',this.clas
,這樣天然是會報錯的。
隨着 ECMAScript 6 的發佈,咱們有了新的方法來實現繼承。也就是經過 class
關鍵字。
class Person { constructor(name,age){ this.name = name; this.age = age; } sayHello(){ console.log(`My name is ${this.name},i'm ${this.age} years old`) } } var you = new Person('you',23); you.sayHello() //My name is you,i'm 23 years old.
ES6 裏面的繼承也很方便,經過 extends
關鍵字來實現。
class Student extends Person{ constructor(name,age,cla){ super(name,age); this.class = cla; } study(){ console.log(`I'm study in class ${this.class}`) } } var liming = new Student('liming',23,7) liming.study() // I'm study in class 7.
這個繼承相比上面的 ES5 裏面實現的繼承要方便了不少,但其實原理是同樣的,提供的這些關鍵字方法只是語法糖而已,並無改變 Js 是基於原型這麼一個事實。不過 extends
這樣實現的繼承有一個限制,就是不能定義屬性,只能定義方法。要新添屬性,仍是得經過修改 prototype
來達到目的。
Student.prototype.teacher = 'Mr.Li' var liming = new Student('liming',23,7) var hanmeimei = new Student('hanmeimei',23,7) liming.teacher //Mr.Li hanmeimei.teacher //Mr.Li
ES6 還提供了 static
關鍵字,來實現靜態方法。靜態方法能夠繼承,但只能由類自己調用,不能被實例調用。
class Person{ constructor(name,age){ this.name = name; this.age = age; } static say(){ console.log('Static') } } class Student extends Person{} Person.say() // Static Student.say() // Static var you = new Person('you',23); you.say() // TypeError: liming.say is not a function
能夠看到,在實例上調用的時候會直接報錯。
在子類中能夠經過 super
來調用父類,根據調用位置的不一樣,行爲也不一樣。在 constructor
中調用,至關於調用父類的 constructor
方法,而在普通方法裏面調用則至關與調用父類自己。
class Person { constructor(name,age){ this.name = name; this.age = age; } sayHello(){ console.log(`My name is ${this.name},i'm ${this.age} years old`) } } class Student extends Person{ constructor(name,age,cla){ super(name,age); // 必須在子類調用 this 前執行,調用了父類的 constructor this.class = cla; } sayHello(){ super.sayHello; // 調用父類方法 console.log('Student say') } } var liming = new Student('liming',23,7); liming.say() // My name is liming,i'm 23 years old.\n Student say.
至此,咱們能夠看到:在 ES6 發佈之後,JavaScript 中實現繼承有了一個標準的方法。雖然它們只是語法糖,背後的本質仍是經過原型鏈以及構造函數實現的,不過在寫法上更易於咱們理解並且也更加清晰。
參考: