這一節梳理對象的繼承。javascript
咱們主要使用繼承來實現代碼的抽象和代碼的複用,在應用層實現功能的封裝。java
javascript 的對象繼承方式真的是百花齊放,屬性繼承、原型繼承、call/aplly繼承、原型鏈繼承、對象繼承、構造函數繼承、組合繼承、類繼承... 十幾種,每一種都細講須要花不少時間,這裏大體梳理經常使用的幾種。 javascript 中的繼承並非明確規定的,而是經過模仿實現的。下面咱們簡單梳理幾種有表明性的繼承。jquery
ECMAScript 5 中引入了一個新方法: Object.create
。能夠調用這個方法來建立一個新對象。新對象的原型就是調用 create
方法時傳入的參數:編程
let too = { a: 1 } let foo = Object.create(too) console.log(foo.a) // 1
經過使用Object.create
方法, 對象 too 會被自動加入到 foo 的原型上。咱們能夠手動模擬實現一個Object.create
相同功能的函數:數組
let too = { a: 1 } function create (prot) { let o = function () {} o.prototype = prot return new o() } let foo = create(too) console.log(foo.a) // 1
或者用更簡單直白的方式來寫:閉包
function Foo() {} Foo.prototype = { a: 1 } let too = new Foo() console.log(too.a) // 1
原型繼承是基於函數的prototype
屬性app
function Foo (id) { this.a = 1234 this.b = id || 0 } Foo.prototype.showData = function () { console.log(`${this.a}, id: ${this.b}`) } function Too (id) { Foo.apply(this, arguments) } Too.prototype = new Foo() let bar = new Too(999) bar.showData() // 1234, id: 999
上面構造函數TOO
經過從新指定prototype
屬性,指向了構造函數Foo
的一個實例,而後在Too
構造函數中調用Foo
的構造函數,從而完成對構造函數Foo
功能的繼承。實例bar
經過屬性__proto__
來訪問原型鏈上的共享屬性和方法。ide
javascript 中的 class繼承又稱模擬類繼承。ES6中正式引入了 class 關鍵字來實現類語言方式建立對象。今後咱們也可使用抽象類的方式來實現繼承。函數
// 父類 class Polygon { constructor(height, width) { this.height = height; this.width = width; } } // 子類 class Square extends Polygon { constructor(sideLength) { super(sideLength, sideLength); // 調用父對象的搞糟函數 } get area() { return this.height * this.width; } set sideLength(newLength) { this.height = newLength; this.width = newLength; } } var square = new Square(2);
在JavaScript中沒有類的概念,只有對象。雖然咱們使用class
關鍵字,這讓 JavaScript
看起來彷佛是擁有了」類」,可表面看到的不必定是本質,class
只是語法糖,實質仍是原型鏈那一套。所以,JavaScript中的繼承只是對象與對象之間的繼承。反觀繼承的本質,繼承即是讓子類擁有父類的一些屬性和方法,在JavaScript中即是讓一個對象擁有另外一個對象的屬性和方法。性能
繼承的實現是有不少種,這裏不一一列舉。須要注意的是 javascript 引擎在原型鏈上查找屬性是比較耗時的,對性能有反作用。與此同時咱們遍歷對象時,原型上的屬性也會被枚舉出來。要識別屬性是在對象上仍是從原型上繼承的,咱們可使用對象上的hasOwnProperty
方法:
let foo = { a: 1 } foo.hasOwnProperty('a') // true foo.hasOwnProperty('toString') // false
使用hasOwnProperty
方法檢測屬性是否直接存在於該對象上並不會遍歷原型鏈。
javascript 支持的是實現繼承,不支持接口繼承,實現繼承主要依賴的是原型鏈。
前面咱們講到的基本是 javascript 怎麼實現面向對象編程的一些知識點。
不從概念來說,簡單來講當咱們有屬性和方法須要被重複使用,或者屬性須要被多個對象共享時就須要去考慮繼承的問題。在函數層面,你們一般的作法是使用做用域鏈來實現內層做用域對外層做用域屬性或函數的共享訪問。舉個栗子吧~~
function car (userName) { let color = 'red' let wheelNumber = 4 let user = userName let driving = function () { console.log(`${user} 的汽車,${wheelNumber}個輪子滾啊滾...`) } let stop = function () { console.log(`${user} 的汽車,${wheelNumber}個輪子滾不動了,嘎。。。`) } return { driving: driving, stop: stop } } var maruko = car('小丸子') maruko.driving() // 小丸子 的汽車,4個輪子滾啊滾... maruko.stop() // 小丸子 的汽車,4個輪子滾不動了,嘎。。。 var nobita = car('大雄') nobita.driving() // 大雄 的汽車,4個輪子滾啊滾... nobita.stop() // 大雄 的汽車,4個輪子滾不動了,嘎。。。
這。。。什麼鬼。是否是有種似曾相識的感受,這其實就是經典的閉包
,jquery 年代不少插件 js 庫都採用這種方式去封裝獨立的功能。說閉包也是繼承是否是有點勉強,可是 javascript 裏函數也是對象,閉包利用函數的做用域鏈來訪問上層做用域的屬性和函數。固然像閉包這樣不使用this
去實現私有屬性比較麻煩, 閉包只適合單實例的場景。再舉一個栗子:
function GoToBed (name) { console.log(`${name}, 睡覺了...`) } function maruko () { let name = '小丸子' function dinner () { console.log(`${name}, 吃完晚餐`) GoToBed(name) } dinner() } function nobita () { let name = '大雄' function homework () { console.log(`${name}, 作完做業`) GoToBed(name) } homework() } maruko() nobita() // 小丸子, 吃完晚餐 // 小丸子, 睡覺了... // 大雄, 作完做業 // 大雄, 睡覺了...
像上面栗子中這樣,以面向過程的方式將公共方法抽離到上層做用域的用法比較常見, 至少我很長時間都是這麼幹的。將GoToBed
函數抽離到全局對象中,函數maruko
、nobita
內部直接經過做用域鏈查找GoToBed
函數。這種鬆散結構的代碼塊組織其實跟上面閉包含義是差很少的。
因此依據做用域鏈來進行公共屬性、方法的管理嚴格意義上不能算是繼承, 只能算是 javascript 面向過程的一種代碼抽象分解的方式,一種編程範式。這種範式編程是基於做用域鏈
,與前面講的繼承是基於原型鏈
的本質區別是屬性查找方式的不一樣。
全局對象 window 造成一個閉合上下文,若是咱們將整個 window 對象假設爲一個全局函數,全部建立的局部函數都是該函數的內部函數。當咱們使用這個假設時不少問題就要清晰多了,全局函數在頁面被關閉前是一直存在的,且在存活期間爲內嵌函數提供執行環境,全部內嵌函數都共享對全局環境的讀寫權限。
這種函數調用時命令式的,函數組織是嵌套的,使用閉包(函數嵌套)的方式來組織代碼流是無模式的一種常態。