閉包與原型繼承
閉包是面試必考題,他將函式內的變數能夠保存在當下的 scope,而原型繼承則是由一條原型鏈組成,是 JS 達成物件導向的封裝與繼承的關鍵
閉包
閉包的 function + Lexical scope
先上一段程式碼
function a() {
let grandpa = 'grandpa'
return function b(){
let father = 'father'
return function c(){
let son = 'son'
return `${grandpa} > ${father} > ${son}` }
}
}
const one = a()()() // call fn c
當 a 被呼叫時,會產生一個作用域,想像會有一條鍊子將此函式與 global 勾住,然後這 a 執行完畢時,裡面宣告的變數(grandpa)被放置於閉包當中,為何會這樣呢,因為 JS 的 GC(垃圾回收) 機制,他發現有東西參照這個變數grandpa,然後繼續執行 b 完畢後,照樣被放置於閉包,最後 c 執行 return grandpa + father 他就會從閉包去找,因為 c 裡面有這兩個變數參照著外部的變數
HOF 搭配箭頭函式版本
//closures and higher order functionfunction boo(string) {return function(name) {return function(name2) {console.log(`hi ${name2}`)}}}const boo2 = (string) => (name) => (name2) => console.log(`hi ${name2}`)boo('hi')('john')('tanya');// AHH! HOW DOES IT REMEMBER THIS 5 years from now?booString = boo2('sing');booStringName = booString('John');booStringNameName2 = booStringName('tanya')
Memory & Encapsulation
使用閉包有兩個重要的優點,記憶體效率與私有變數
function heavyDuty(item) {const bigArray = new Array(7000).fill('😄')console.log('created!');return bigArray[item]}
heavyDuty(699)heavyDuty(699)heavyDuty(699)
當我們呼叫同一函式並在裡面創造 Array 時,未創建閉包的 function 內也會同時創造同樣次數的 Array,這樣會耗費掉一堆記憶體,如果用閉包的話...
function heavyDuty2(item) {const bigArray = new Array(7000).fill('😄')console.log('created Again!')return function(item) {return bigArray[item]}}const getHeavyDuty = heavyDuty2();getHeavyDuty(699)getHeavyDuty(699)getHeavyDuty(699)
函式中的記憶體被宣告後存在閉包裡面,裡面的 bigArray[item] 會參照外部的記憶體位置,所以當每次呼叫時就會從閉包內去找 bigArray ,這樣只需要創造一次 Array 就能給多次重複使用,同時記憶體也不會因為 function 執行完畢而被 GC 掉
私有變數
JS 用閉包來實現私有變數,為了不讓不必要的資訊出現,這跟之後提到的 module 有相同的概念
const makeNuclearButton = () => {let timeWithoutDestruction = 0;const passTime = () => timeWithoutDestruction++;const totalPeaceTime = () => timeWithoutDestruction;const launch = () => {timeWithoutDestruction = -1;return '💥';}setInterval(passTime, 1000);return {totalPeaceTime}}const ww3 = makeNuclearButton();ww3.totalPeaceTime()
我們創建一個 function 中裡面有許多方法可以調用,這樣只要回傳你想執行的方法,就能保護其他 function 不被外部環境看到
setTimeout 與閉包是面試常考題,思路是當 for 迴圈要將每次的執行環境儲存起來,就能用閉包來存每次的迴圈
第一種: 使用標準閉包將 setTimeout 的執行結果包起來,因為 array[i] 有參照外部的參數,所以會將此執行結果存在閉包中
const array = [1,2,3,4];for(var i=0; i < array.length; i++) {function one(){return function(i){return setTimeout(function(){console.log('I am at index ' + array[i])}, 3000)}}let b =one()b(i)}
第二種: 利用 IIFE 立即執行當下函式,這樣就不會讓 setTimeout 放到 callback queue,所以雖然返回是三秒後,但是結果是當下的執行結果
const array = [1,2,3,4];for(var i=0; i < array.length; i++) {(function(closureI) {setTimeout(function(){console.log('I am at index ' + array[closureI])}, 3000)})(i)}
第三種: let ,把 var 改成 let 就能讓變數變成 block scope,每次執行都是每個for 迴圈內的環境,即使 callback 也不會被外部影響
再來我想來看一下所謂閉包的定義與每個程式語言的不同
在一些語言中,在函式中可以(巢狀)定義另一個函式時,如果內部的函式參照了外部的函式的變數,則可能產生閉包。執行時,一旦外部的 函式被執行,一個閉包就形成了,閉包中包含了內部函式的程式碼,以及所需外部函式中的變數的參照。其中所參照的變數稱作上值(upvalue)。
所以代表說只要有語言函式裡面可以定義函式就會有閉包的出現
這是基於只要內部函式變數有參照外部變數,就不會被GC掉
Go 裡有函式型別的變數,這樣,雖然不能在一個函式裡直接宣告另一個函式,但是可以在一個函式中宣告一個函式型別的變數,此時的函式稱為閉包(closure)
packagemain
import”fmt”
func main(){
add:=func(base int)func(int)(int){
return func(i int)(int){
return base i
}
}
add5:=add(5)
fmt.Println(“add5(10)=”,add5(10))
}
我們將 add(5) 賦予在 add5 這裡面,最後這個 5 會在 add 的匿名函式中,與匿名函式的參數相加返回
在 python 的閉包中,函式都有個 __closure__ 屬性,意味著如果是閉包的話這個,這屬性將會返回一個物件,裡面會有存在裡面的變數
>>> adder.__closure__ >>>
adder5.__closure__ (<cell at 0x103075910: int object at 0x7fd251604518>,)
>>> adder5.__closure__[0].cell_contents 5
感覺其實多半 JS 的閉包與其他語言來講都是相同概念,只差在語言特性與宣告方式而已
原型繼承
原型鏈與 proto
把原型想像成有一條很長的鏈子,鏈子勾著每個物件,而每個物件的原型都指向前面的物件
let array = []
array.__proto__
__proto__ 會返回這個物件所繼承的物件,所以 array 的原型就是有著 array 這樣板的方法,只要調用這樣版就能繼承 array 的所有屬性
繼承
前面閉包提到了物件導向三大原則之一的封裝,接下來換繼承,什麼是繼承,你有了人類的這個種族的基因就是繼承,有了繼承我們可以複製許多有著相同屬性或方法的物件
let dragon = { name: 'Tanya', fire: true, fight() { return 5 }, sing() { if (this.fire) { return `I am ${this.name}, the breather of fire` } }}
先來創造一個龍的物件,並且裡面有一些屬性,他的名字,會不會噴火,還有他的方法,也就是他所會的技能
let lizard = { name: 'Kiki', fight() { return ${this.name} }}
接下來有個蜥蜴的物件,裡面也有一些他的屬性與方法,但我想要改變這隻蜥蜴的方法,所以這時我們想到上上一篇提到的 bind 方法,可以把物件內的屬性綁到另一個方法上
let a = lizard.fight.bind(dragon)console.log(a())//tanya
但這樣綁實在是滿麻煩的,而且只能綁在方法上面沒辦法將整個屬性方法搬過來,這時就出現了繼承
lizard.__proto__ = dragon
console.log(lizard.sing())
console.log(lizard.fire)
console.log(lizard.fight)
蜥蜴的原型繼承了龍這個物件,代表著蜥蜴也有龍的技能與屬性,就像是生物演化的過程一樣,我們也有著猴子的技能與屬性
dragon.isPrototypeOf(lizard);
isPrototypeOf 方法可以判斷這物件是否為另一個物件的原型
hasOwnProperty 方法可以判斷這物件自身屬性方法非繼承的有哪些
特別注意一點是當最上層沒有原型時,會直接返回 null 代表說這物件沒有原型了
obj.__proto__.__proto__ //null
function 的 prototype
函式的 prototype 代表著對這函式進行操作的方法,例如: call, apply, bind
function a(){
console.log(a)
}
a.__proto__
從圖中看出 multiplyBy5 的原型是 Function,這 Function 代表的是製作 function 的原型,就像是在衣服有著不同的樣式,Function 代表著還沒加入樣式的素色衣服,Function 裡面包含了 call, apply 那些方法供底下繼承的 function 使用
Prototype
multiplyBy5.__proto__ = Function.prototype,__proto__ 方法指向父物件的 prototype,所以只要 Function.prototype,就能看到那些 call, reply….等方法,只有 function 才有 prototype
Object.create
這方法可以創造一個新的相同屬性方法物件,我們可以用此達成繼承
var human = {mortal: true}
var socrates = Object.create(human);
human.isPrototypeOf(socrates) //true
socrates 繼承了 human 屬性方法,再來看看下面的例子
Date.prototype.lastYear = function(){ return this.getFullYear() -1}new Date('1900-10-10').lastYear()
我們為 Date 物件新增一個方法,這個 this 是關鍵,this 在這邊代表著
new Date(‘1900–10–10’) ,記得前幾篇說過的在哪裡創造函式 this 就在哪裡,所以這個 this 指的就是 Date 物件,而 Date 物件有個 getFullYear 方法來返回指定日期的年份,所以記得不能用 ()=>{} 箭頭函式,這樣會讓外部的 this 被綁進去
總結
閉包是封裝,原型是繼承,除了 OOP 外,JS 還有 Functional Programing,跟先前提到的函式 Curry 化有關,下一篇就會來講講這兩個程式設計模式。