對JavaScript中this和原型的理解
theme: juejin
書接上篇:《對Javascript中作用域和閉包的理解》。本篇文章也是對《你不知道的JavaScript》(上)中關於this和原型的理解和總結。
僅僅是簡單的讀完書中的知識遠遠是不夠的,更多的是需要應用到我們日常的程式碼編寫當中,學習不應該只是停留在知道和理解的階段,更重要的是反饋和應用。所以輸出這篇書中知識的總結,也當作一篇筆記留給以後的自己看。
廢話不多說下面開始關於this和原型的講解:
this
說起this這個東西(暫時稱為東西...),前端開發同學應該都不陌生,總是能說上兩句(因為這應該是面試必問的知識點),但是如果詳細把this的知識說清楚的話,好像又不是那麼明白,別慌,下面我們來一步步揭開蓋在this上面神祕的面紗
WHAT?
嗯... 時髦一點,用個英文來當標題 😏 ,其實意思就是this到底是什麼
理解誤區
首先來說明兩個關於this的理解誤區: 1. this指的就是當前的函式
使用書中的一個示例來反駁這個觀點:
```javascript
/*
我們想要來記錄一下函式被呼叫的次數
*/
function foo(){ console.log('foo被呼叫'+ ++this.count +'次') }
// 初始化count
foo.count = 0
// 第一次呼叫
foo() // foo被呼叫NaN次 why????
```
我們來看上面的例子,如果this指向的是foo的話,那麼應該輸出的是:foo被呼叫1次,但實際輸出的確是:foo被呼叫NaN次,由此可見this不是指向當前的函式的。
但是為什麼會輸出這個呢?在這裡賣個關子,下面this的繫結規則看完之後你就會明白為什麼,可以在評論區留下你的理解。
當然看過上一篇的同學可能會機智的指出我只需要定義一個變數來儲存count,利用上一篇學的詞法作用域的知識就可以實現想要的功能了,是的,可以的,但這跟我們講this沒有啥關係,就不在這裡貼程式碼了。
- this指的是函式的作用域(不理解作用域的同學自己去看上一篇文章)
這個說法不能說完全不對只能說這只是一種簡單的情況,同樣還是可以通過一個例子來說明一下:
```javascript
function foo(){ var a = 0 console.log(this.a) }
foo() // undefined why???
```
看上面的程式碼,如果this指向的是foo函式的作用域,那麼a變數肯定是存在於foo的作用域中的,但實際上輸出的是undefined
引用書中一句話:在JavaScript內部,作用域確實和物件類似,可見的識別符號都是他的屬性,但是作用域‘物件’無法通過JavaScript程式碼訪問(也就是說無法顯式的賦值給一個識別符號),它存在於JavaScript引擎內部(引擎可以隱式的讓我們訪問)
this到底是什麼(完全照抄)
this是在執行時進行繫結的,並不是在編寫時繫結,他的上下文取決於函式呼叫時的各種條件,this的繫結和函式宣告的位置沒有任何關係,只取決於函式的呼叫方式
當一個函式被呼叫時,會建立一個活動記錄(有時也會稱為執行上下文)。這個記錄會包含函式在哪裡被呼叫/函式的呼叫方式,傳入的引數等資訊,this就是這個記錄的一個屬性,會在函式執行過程中用到
也就是說,this在函式定義的時候是啥,誰都不知道(那它跟詞法作用域沒得關係了),只有在函式呼叫的時候才知道this到底是誰(有可能是函式自己哦...)
this繫結規則
上面我們只是簡單的理解了一下this是什麼以及說明了兩個常見的誤區,下面我們來具體說明一下當函式中出現this的時候不同的場景下,this指的到底是什麼?也就是this的繫結規則
應該是有四個規則,其他的場景都是這幾個規則的組合,下面我們來一一介紹這四個規則:預設繫結/隱式繫結/顯式繫結/new繫結
預設繫結
這個規則也是當無法應用其他規則時的預設規則:獨立函式呼叫
下面我們來通過一個例子來說明:
```javascript
function foo(){ console.log(this.a) }
var a = 1
foo() // 1
```
上面例子中foo()沒有任何修飾的函式引用呼叫的,所以是預設呼叫(就是說直接呼叫函式的時候一般就是預設呼叫,不管是在什麼位置)
隱式繫結
呼叫位置是否有上下文物件,或者說是否被某個物件擁有或者包含(是否是當作物件的屬性來呼叫),此時這個this指向上下文物件或者某個物件
同樣還是看一個例子:
```javascript
function foo(){ console.log(this.a) }
var obj = { a:2, foo:foo }
obj.foo() // 2
```
通過obj.foo()呼叫方式呼叫的話,隱式繫結規則會把this繫結到obj上面(具體到函式執行的上下文的話,感興趣的同學可以自行查閱其他資料)
隱式丟失
一個很常見的問題就是隱式繫結時this丟失的問題:當我們函式當作引數傳遞時或者我們把物件方法重新賦值給其他變數在被呼叫時,都會出函式丟失繫結物件,這個時候會應用預設繫結規則,下面還是通過兩個例子來看一下:
```javascript
function foo(){ console.log(this.a) }
var obj = { a:1, foo:foo }
var bar = obj.foo
var a = 'global props'
// 當我們重新賦值給bar的時候,會出現隱式繫結物件丟失的情況
bar() // 'global props'
// 當被當做引數傳遞的時候也會發生隱式丟失的情況
function doFoo(fn){ fn() }
doFoo(obj.foo) // 'global props'
```
顯式繫結
如果我就想在一個物件上強制呼叫一個函式,而不是把這個函式賦值給這個物件的屬性呢?js也給我們提供了這樣的顯式繫結的機制:call/apply/bind,下面來介紹一下這幾個函式:
call/apply方法
把這兩個函式放到一起來說,是因為他們兩個區別就是傳參的區別(所以為啥提供兩個函式呢?),他們第一個引數都是你想要顯式繫結的物件(如果是基礎型別,會轉換成對應的物件形式:string->String...),call除了這個之外是一個引數列表,apply方法剩下的引數可以傳遞一個數組,會把陣列中的引數傳遞給呼叫函式
下面還是來舉個例子來說明一下
```javascript
function add(num1,num2){ return this.basicNum + num1 + num2 }
var obj = { basicNum:1 } add.call(obj,1,2) // 4
add.apply(obj,[1,2]) // 4
// 那如果apply傳多於兩個引數呢?會被忽略
add.apply(obj,[1,2],3) // 4
add.apply(obj,[1,2,3]) // 4
add.apply(obj,[1]) // NaN => 1 + undefined = NaN
add.apply(obj,1) // Uncaught TypeError: CreateListFromArrayLike called on non-object 報錯了
// 既然上面報錯說不是一個物件,那如果傳一個物件呢?
dd.apply(obj,{}) // NaN 咱也不知道實際是怎麼呼叫的。。。
```
bind方法(硬繫結)
上面我們說了兩種顯式繫結的方式,那為什麼還會有bind方法呢?會想一下上一節說的繫結丟失的情況,有一種情況是把函式當作引數傳遞給其他的函式(常見的就是回撥函式),這種情況下我們也不知道傳給的目標函式是怎麼呼叫我們的函式的,也就是說當函式呼叫的時候我們不知道this的指向到底是不是預期的物件,這種情況下我們應該怎麼解決?這個時候就可以使用硬繫結的方式來實現:
```javascript
function foo(){ console.log(this.a) }
var obj = { a:1 }
var bar = function(){ foo.call(obj) }
bar() // 2
setTimeout(bar,2000) // 2
// 硬繫結的bar就不能在修改他的this了 bar.call(window) // 2
```
通過上面的例子我們可以看到,硬繫結的意思就是在這個函式外面在包裹一層,上面的是繫結固定的物件,同樣我們可以優化一下,寫一個更通用的硬繫結的函式:
```javascript
// 實現一個更通用的硬繫結函式
function bind (fn,target){ return function (){ return fn.apply(target,arguments) } }
function foo(num){ return this.a + num }
var obj = { a:1 }
var bar = bind(foo,obj)
var b = bar(1) // foo.call(obj,1)
console.log(b) // 2
```
由於硬繫結是一個非常常見的模式,所以ES5提供了內建的方法:Function.prototype.bind,用法跟上面的bind函式類似:
``` javascript
function foo(){...}
var obj = {...}
var bar = foo.bind(obj)
console.log(bar(1)) // 2
```
bind方法的其他用處
看到這裡的同學可能會有個疑問,bind方法除了第一個引數之外,還可以傳遞其他的引數嗎?那傳遞之後有什麼用處呢?
其實bind函式呼叫的時候是可以類似call那樣傳遞引數的,也就說除了繫結物件之外,可以傳一個引數列表進來,當呼叫返回的新函式的時候這個引數列表會傳給被硬繫結的函式去執行。
舉個例子來說明一下:
``` javascript
function add(num1=0,num2=0){ return this.basicNum + num1 + num2 }
var obj = {basicNum:1}
// 這個時候相當於 num1 = 1
var add_one = foo.bind(obj,1)
// num2 = 2
add_one(2) // 1+1+2
```
ok,這個特點我們已經知道了 那它具體有什麼用處呢?
比如說我們有一個給DOM節點繫結事件的工廠函式:
```javascript
function domBindEvent(dom,fn_type,fn){ dom.addEventListener(even_type,fn) }
```
這種情況下當給不同dom繫結不同事件的時候可以使用bind的這個特性:
```javascript
const div = document.getElementById('id')
const eventInfo = { 'click':function(){...}, 'hover':function(){...} }
// 此時可以生成一個針對div的繫結函式,然後把event_type和fn傳進去 var divBindFn = domBindEvent.bind(null,div)
for(let type in eventInfo){ divBindFn(type,eventInfo[type]) }
```
API呼叫的‘上下文’
第三方的許多函式,以及JavaScript語言和宿主環境中許多新的內建函式,都提供了一個可選的引數,通常被稱為上下文(context),作用和bind一樣的
同樣舉例來說:
```javascript
function foo(el){ console.log(el,this.id) }
var obj = {id:"awesome"}
[1,2,3].forEach(foo,obj) // 1 awesome 2 awesome 3 awesome
```
總結一下,我們關於顯式繫結的東西都已經說完了,比較雜,主要就是使用call/apply/bind函式來實現顯式繫結,也詳細講解了這三個函式的用法。
new繫結
最後我們來看一下這個new繫結方式,當函式當作建構函式呼叫時,this會指向新生成的物件:
``` javascript
function Foo(a){ this.a = a }
var bar = new Foo(2) console.log(bar.a) // 2
```
在這裡我們來補充一下new操作符實際幹了什麼事情,也就是說當我們使用new來呼叫函式,或者說發生建構函式呼叫時,會發生什麼:
-
建立一個新物件
-
這個新物件會被執行【prototype】連結
-
這個新物件會繫結到函式的this
-
如果函式沒有返回其他物件(注意:如果返回基礎型別,會被忽略),那麼new表示式中的函式呼叫會自動返回這個新物件
這個就是new Foo()時發生的事情,後面我們會繼續講關於JavaScript中建構函式的細節,這個地方就不再贅述了。
上面就是this繫結的四種規則,我們都一一講解過了,更復雜的場景就是上面四種繫結規則的同時應用,所以下面我們來看一下這幾個規則繫結的優先順序,也就是說當函式同時應用這幾個繫結規則時,this到底時通過什麼樣的規則來實現繫結的
規則優先順序
首先預設規則應該是優先順序最低的,因為其他規則應用不上時候都是應用的預設規則
然後我們來看一下其他幾種規則的優先順序:
隱式繫結 PK 顯式繫結
顯式繫結比隱式繫結的優先順序高,同樣我們還是通過一段程式碼來說明:
```javascript
function foo(){ console.log(this.a) }
var obj1 = { a:1, foo:foo } var obj2 = { a:2 foo:foo }
// 隱式繫結
obj1.foo() // 1
obj2.foo() // 2
// 顯式繫結 > 隱式繫結
obj1.foo.call(obj2) // 2
obj2.foo.call(obj1) // 3
```
看完上面的例子有的同學可能會說 obj1.foo.call(obj2) 實際應該就是foo.call(obj2)吧?因為程式碼執行順序的原因所以foo函式繫結的this是obj2(也可能只有我自己有這個疑問😄),那下面我們來調換一下呼叫順序:
```javascript
var bindFoo = foo.bind(obj2)
var obj3 = { a:3, foo: bindFoo }
obj3.foo() // ? 實際輸出的是2
```
通過上面兩個例子 好像顯式繫結就是比隱式繫結的優先順序更高(也可能是上面講的bind方法繫結之後this就不能修改導致),當然也可以想想,你都專門綁定了,肯定是覆蓋隱式的繫結規則的。
new繫結 PK 隱式繫結
下面我們再來分析一下new繫結和隱式繫結的優先順序,話不多說,看程式碼:
```javascript
var foo = function(props){this.a = props}
var obj1 = {foo:foo}
var obj2 = {}
// 這個就是隱式繫結
obj1.foo(1)
console.log(obj1.a) // 1
// 顯式繫結比隱式繫結優先順序高
obj1.foo.call(obj2,2)
console.log(obj2.a) // 2
// new繫結比隱式繫結優先順序高
var bar = new obj1.foo(3)
console.log(bar.a) // 3
console.log(obj1.a) //1
```
通過上面的例子我們可以看出來,new繫結優先順序是比隱式繫結的優先順序更高的
new繫結 PK bind繫結
因為call/apply使用之後函式就直接執行了,沒有辦法在當作建構函式被呼叫,所以我們使用bind繫結來跟new繫結比較優先順序:
同樣還是看一段程式碼來看一下優先順序是怎樣的:
```javascript
function foo(something){ this.a = something }
var obj = {}
var bar = foo.bind(obj)
bar(1)
console.log(obj.a) // 1
var baz = new bar(3)
console.log(obj.a) // 1
// 看一下 這個的話說明new繫結優先順序更高
console.log(baz.a) // 3
```
總結
通過上面的程式碼我們大致知道了優先順序的順序:new繫結 > 顯式繫結 > 隱式繫結 > 預設繫結
物件
這個地方我們來講一下JavaScript中的物件,首先說明一下為什麼講的是原型和this,卻還要講一下物件這個呢?因為不管是理解this的指向還是原型,都離不開物件的使用,所以我們這個地方來詳細的講一下JavaScript中的物件到底是什麼:
語法
先從簡單的講起,物件定義的兩種形式:宣告(文字)形式和構造形式
- 宣告(文字)形式
```javascript
var obj = { key:value, ... }
```
- 構造形式
```javascript
var obj = new Object()
obj.key = value
```
兩種形式生成的物件都是一樣的,區別就是宣告(文字)形式的話可以一次宣告多個屬性,而構造形式需要一個一個新增
型別
JavaScript中一共有6種主要型別:
-
string
-
number
-
boolean
-
null
-
undefined
-
Object
通過上面的分類我們可以看出來,簡單基本型別不是物件(JavaScript中一切皆是物件?)
當然除了這幾個主要型別,JavaScript中還有許多特殊的物件子型別,可以稱之為複雜基礎型別
例如:Function/Array,所以可以像操作物件一樣操作函式或者陣列,比如給他們新增屬性值等
內建物件
還有一些物件子型別,通常被稱為內建物件:
-
String
-
Number
-
Boolean
-
Object
-
Function
-
Array
-
Date
-
RegExp
-
Error
在JavaScript中,他們實際上是一些內建函式,這些函式可以當做建構函式使用,下面我們來主要看一下String/Number/Boolean這幾個內建物件,因為他們跟我們前面說的基礎型別很類似,但實際上他們更加複雜,同樣的是我們還是來用程式碼來說明一下:
```javascript
var str = 'I am a string'
typeof str // 'string' 基礎型別
str instanceof String // false
var strObj = new String('I am a String')
typeof strObj // 'Object'
strObj instanceof String // true
```
通過上面的例子我們可以看出來這兩個是不同的型別,而且String是Object的子型別,那提供這個內建物件是有什麼用處呢?為什麼要提供這些基礎型別對應的物件型別呢?下面我們來思考一下下面這段程式碼:
```javascript
var str = 'I am a string'
console.log(str.length) // 13
console.log(str.charAt(3)) // 'm'
```
不知道同學們發現沒有如果我們給一個string型別的變數繫結一個屬性的話是不會生效的:
```javascript
var str = 'I am a string'
str.key = 'error' // 不會報錯
console.log(str.key) // undefined
```
通過上面兩個例子的說明,有的同學可能已經猜到了,正常來說string型別的資料只是一個字面量,並且是一個不可變的值,如果要在一個字面量上執行一些操作怎麼辦呢?那就把他轉換成對應的物件型別String形式,然後就可以執行一些操作了。
像上面我們直接訪問str.length的話 實際上引擎會幫我們把string轉換成對應的String型別,呼叫完成後在把String型別的物件銷燬掉,所以我們可以在程式碼中直接使用這些方法不會報錯,因為這些方法都是繫結在String.prototype上面的(通過原型鏈我們可以訪問到,後面會講到)
內容(屬性)
上面我們說明了物件的宣告方式和型別,下面我們來講一下物件是幹啥的?我感覺物件可以理解為一個儲存資料的資料結構
這個資料結構通過一個類似指標的方式來指向對應的資料的儲存位置,從而把多個數據有序的聯絡在一起。
也就是說,物件中儲存的值實際上不是放在物件的內部的,在引擎內部,這些值得儲存方式是多種多樣的,而儲存在物件這個容器中的只是這些屬性的名稱(類似指標或者說是一個引用),指向這些值得真正的儲存位置。
訪問方式
訪問物件中的值有兩種方式:.操作符和[]操作符
這兩個的區別就是.操作符要求屬性名滿足識別符號的命名規範,而[]操作符可以接受任意的UTF-8/Unicode字串作為屬性名(具體啥意思咱也不太清楚,理解下來就是不符合我們常用的命名規範的屬性值都可以通過這種方式來訪問),說了這麼多,感覺我們需要知道[]操作符中可以使用一個變數來訪問值,舉個例子看一下:
```javascript
var obj = { 'foo':'I am Foo' },
var fooStr = 'foo'
console.log(obj[fooStr]) // I am Foo
```
ES6中支援我們在[]中使用表示式
```javascript
var preStr = 'f'
var obj = { [preStr+'00'] : 'I am Foo' }
```
屬性描述符
屬性描述符就是用來描述物件屬性的特性的,有三個:writable(可寫)、enumerable(可列舉)、configurable(可配置)
可以使用Object.getOwnPropertyDescriptor(object,key)方法來拿到屬性描述符,設定屬性描述符的話可以使用Object.defineProperty(object,key,porpsConfig),下面我們還是來通過一段程式碼來具體說一下:
```javascript
var obj = {}
Object.defineProperty(obj,'a',{ value:2, writable:true, configurable:true, enumable:true })
// 獲取屬性描述符
Object.getOwnPropertyDescriptor(obj,'a') //{value: 2, writable: true, enumerable: false, configurable: true}
obj.b = 3
// 獲取預設的屬性描述符
Object.getOwnPropertyDescriptor(obj,'b') // {value: 3, writable: true, enumerable: true, configurable: true}
```
上面我們來具體說明一下每個屬性代表的什麼意思:
- writable(可寫)
writable決定是否可以修改屬性的值
```javascript
var obj = {}
Object.defineProperty(obj,'a',{ value:2, wirtable:false, // 不可修改 enumerable: true, configurable:true, })
console.log(obj.a) // 2
obj.a = 3
console.log(obj.a) // 2 也就是我們的賦值操作靜默失敗,如果是嚴格模式會直接報錯:TypeError錯誤,表示我們無法修改一個不可寫的屬性
```
- enumerable(可列舉)
這個屬性描述符控制的是屬性是否會出現在物件的屬性列舉中,比如for...in迴圈等。如果值為false,就不會出現迴圈中,但是值仍然可以正常的訪問
- configurable(可配置)
只要屬性是可配置的,我們就可以使用defineProperty()方法來修改屬性的描述符:
``` javascript
var obj = {}
obj.a = 2
Object.defineProperty(obj,'a',{ value:3, configurable:false, // 不可配置 enumerable: true, wirtable:true })
console.log(obj.a) // 3
// 在嘗試使用defineProperty修改屬性描述符的話會報TypeError錯誤,所以configurable:false是單向的,無法撤銷
Object.defineProperty(obj,'a',{ // TypeError value:4, configurable:true, writable: true, enumerable:true })
```
除此之外,如果屬性的configurable為false的話也不能使用delete操作符刪除該屬性
應用
我們講完了上面的屬性描述符之後,可以用這幾個特性來定義一些特殊的物件,來滿足我們不同的要求
- 物件常量
結合writable:false和configurable:false就可以建立一個物件常量(不可修改,重定義和刪除):
```javascript
var obj = {}
Object.defineProperty(obj,'FAVORITE_NUMBER',{ value:2, wriable:false, configurable:false })
```
- 禁止擴充套件
如果想要禁止一個物件新增新屬性並且保留已有的屬性,可以使用Object.preventExtensions()方法:
```javascript
var obj = { a:2 }
Object.preventExtensions(obj)
obj.b = 3
console.log(obj.b) // undefined
```
- 密封
Object.seal()會建立一個 密封的物件,這個 方式實際上會在一個現有物件上呼叫Object.preventExtensions()並把現有所有屬性標記為configurable:false
所以密封之後的物件不僅 不能新增新屬性,也不能重新配置或者刪除任何現有屬性(但是可以修改屬性的值)
- 凍結
Object.freeze()會建立一個凍結物件,這個實際上會在一個現有物件上呼叫Object.seal()並把所有資料訪問屬性標記為writable:false,這樣就無法修改他們的值
他會禁止對物件本身以及其任意直接屬性的修改(這個物件引用的其他物件是不受影響的)
訪問描述符
- [[Get]]
當我們訪問物件的屬性時,實際上就是執行的[[Get]]操作,這個操作首先會在物件內部查詢有沒有這個屬性,如果沒有的話會繼續查詢該物件的原型鏈上有沒有該屬性,如果都沒有的話返回undefined,下面我們還是來看一段程式碼:
``` javascript
var obj = { a:undefined }
console.log(obj.a) // undefined
console.log(obj.b) // undefined
```
上面兩個雖然輸出結果一樣的,但是[[Get]]操作是不是一樣的:obj.a 是在物件中找到了這個屬性,這個屬性值為undefined;obj.b是在物件中沒有找到,然後又去原型鏈中也沒有找到,所以返回了undefined。
- [[Put]]
這個操作跟我們剛才講的[[Get]]操作相對應,就是賦值操作,但是會更加複雜一點,大致就分為兩種情況:一種是物件存在這個屬性,一種是不存在這個屬性,並且[[Put]]也不是一個簡單的賦值操作,還需要結合我們前面講的屬性描述符和訪問描述符來執行賦值的操作:
- 物件中已經存在該屬性:
首先檢查屬性是否是訪問描述符,如果是就呼叫setter方法
然後檢查屬性的資料訪問符中writable是否為false,是的話嚴格模式丟擲錯誤,非嚴格模式靜默失敗
如果不是以上兩種場景,則將該值賦值為屬性的值
- 物件中不存在該屬性:
這個場景更加複雜,我們在後面的原型鏈中會繼續講到,這個地方先知道一下就好了
- getter/setter
物件的[[Get]]和[[Put]]操作分別可以控制屬性值得獲取和設定
js也給我們提供了修改屬性訪問和設定的預設操作的方式:getter/setter,這兩個都是函式,getter是在屬性被訪問的時候呼叫,setter是在設定物件屬性值得時候被呼叫
當我們給一個屬性定義getter、setter或者兩個都有時,這個屬性會被定義為訪問描述符
對於訪問描述符來說,會忽略他的value和writable屬性,關注他的get和set方法:
```javascript
var obj = { get a(){ return 2 } }
Object.defineProperty(obj,'b',{ get:function(){ return this.a * 2 }, enumerable:true })
console.log(obj.a) // 2
console.log(obj.b) // 4
// 如果重新給a/b賦值的話 因為沒有set方法,會靜默失敗
obj.a = 4
console.log(obj.a) // 2
```
由於上面我們只是定義了屬性a的getter方法,所以對屬性a的賦值操作會靜默失敗
下面我們來定義一下屬性a的setter方法:
```javascript
var obj = { b:2, get a(){ return this.b }, set a(val){ this.b = val *2 } }
obj.a = 2
console.log(obj.a) //4
console.log(obj.b) // 4
```
總結
我們上面詳細講了JavaScript中的物件是什麼以及物件宣告以及他的一些特性,所以 JavaScript中一切皆是物件??
原型
[[Prototype]]
JavaScript中的物件有一個特殊的[[Prototype]]內建屬性,其實就是對其他物件的引用。幾乎所有的物件在建立時[[Portotpye]]屬性都會被賦予一個非空的值
我們前面說過了當我們訪問和設定物件屬性的時候會有兩個預設操作:[[Get]]和[[Put]],在這裡我們在來詳細的說一下這兩個預設操作的執行邏輯:
- [[Get]]
當我們試圖訪問物件的屬性時會觸發[[Get]]操作,第一步就是檢查物件是否有這個屬性,如果有點話就返回這個屬性的值,如果要是沒有的話就會使用到[[Prototype]]鏈了
也就是說當物件中找不到該屬性的話就會繼續訪問物件的[[Prototype]]鏈:
```javascript
var anotherObj = { a:2 }
var myObj = Object.create(anotherObj) // Object.create方法就是會建立一個物件,然後把這個物件的[[Prototype]]關聯到這個物件
console.log(myObj.a) // 2
```
通過上面的例子我們可以看出,myObj中是沒有a屬性的,但是我們通過把myObj的[[Prototype]]關聯到了anotherObject物件,所以我們可以訪問到myObj.a的值,
當然如果一直找不到的話,就會返回undefined,那到底什麼時候才是[[Prototype]]鏈的盡頭呢:Object.prototpye,對就是他,一般他的值為null。
- [[Put]]
前面的時候我們沒有太詳細的講解關於賦值的預設操作,因為這個預設的賦值操作涉及[[Prototpye]]鏈,這個地方我們再來詳細的說一下這個預設操作的具體的場景:
首先我們來看一下下面這段賦值程式碼:obj.a = 'foo'
如果obj物件中存在a屬性,那麼就會重新給他賦值(按照我們上節所說的物件中的屬性賦值操作)
如果a不是直接存在於obj物件中,會遍歷[[Prototype]]鏈,如果[[Prototype]]鏈上仍然沒有找到的話,那就會把a屬性新增到obj物件上
如果a既存在於obj中,又存在於[[Prototpye]]鏈中,那麼就會發生 遮蔽 也就說當我們訪問obj.a的時候訪問的永遠是最底層的a屬性,在obj上面找到之後就會再去他的原型鏈上面查找了
最後,如果a不直接存在於obj中,而是存在於原型鏈上層,那麼這個時候就會變得有趣了,我們來看一下下面幾種場景:
-
如果在[[Prototype]]鏈上層存在名為a的普通資料訪問屬性,並且沒有被標記為writable:false,那麼就會直接在obj上面新增一個名為a的新屬性,他是遮蔽屬性
-
如果在[[Prototype]]鏈上層存在名為a的普通資料訪問屬性,但是被標記為writable:false,那麼就無法修改已有屬性或者在obj上面建立一個遮蔽屬性
-
如果在[[Prototpye]]鏈上層存在a,並且它是一個setter,那就一定會呼叫這個setter,而且a不會被新增到obj,也不會重新定義這個setter
所以好多的前端開發同學都認為在當前物件設定一個存在於原型鏈上層的屬性值會發生遮蔽,其實並不是這麼回事的哦!!!
類與委託
類是什麼-類也是一種資料結構,用來封裝資料和操作資料的行為,同樣類比我們JavaScript中的物件,它也是一種資料結構,用來封裝資料和操作資料的行為
但是js中是沒有類的,而是物件直接定義自己的行為,也就是說JavaScript中只有物件(資料型別)
但是很長時間以來,我們都在模仿類,像建構函式/new Function()等等,我們在前面可以看到實際就是根據[[Prototype]]鏈這個特性來實現類似類的組織程式碼的方式
建構函式和new操作符
首先我們來看一下建構函式這個概念:我們通常把通過new呼叫的Function稱為 建構函式
我們應該都知道 所有的函式預設都會擁有一個名為prototype的共有並且不可列舉的屬性,他會指向另一個物件
通常我們通過new來呼叫函式的時候會被新生成的物件的[[Prototype]]連結到這個物件上,我們來通過一段程式碼來理解一下這句話:
```javascript
function Foo(){}
var a = new Foo()
Object.getPrototpyeOf(a) === Foo.prototype // true
```
實際上上面的程式碼就是 呼叫new Foo()時會建立a, 其中一步就是講a內部的[[Prototpye]]連結到Foo.prototype指向的物件
OK,下面我們來說明一下我們常說的‘建構函式’/constructor
- 建構函式?
實際上,JavaScript中是沒有建構函式這麼一說的,我們認為的建構函式其實就是一個普通函式,只不過時函式呼叫的時候時通過new操作符來呼叫的而已。而new操作符就是做了一些關聯,
函式不是建構函式,對於‘建構函式’更準確的說法應該是:帶new的函式呼叫
- constructor
Foo.prototype預設有一個公有且不可列舉的屬性 constructor 這個屬性指向的是 建立這個物件的函式(Foo)
```javascript
function Foo(){}
var a = new Foo()
Foo.prototype.constructor === Foo // true
a.constructor === Foo // true 實際上時根據原型鏈查詢的,我們前面也講過了。
```
實際上因為Foo.prototype可以隨意修改,這個屬性也可能會指向不同的物件,constructor屬性只是Foo函式生命時的預設屬性,如果建立一個新物件並替換預設的。prototype物件,新物件是不會自動獲得constructor屬性的:
```javascript
function Foo(){}
Foo.prototype = {}
var a = new Foo()
a.constructor === Foo // false
a.constructor === Object // true
```
我們上面講了關於在JavaScript程式中模仿類的方法,那實際上在JavaScript中是如何實現‘繼承’這種機制的呢?
(原型)繼承
實際上JavaScript中使用原型鏈這個特性實現的繼承,也就是說在JavaScript中是通過原型鏈來把不同的物件關聯起來的
上面這張圖展示了JavaScript中實現’繼承‘的方式,下面我們用程式碼來實現一下圖中的關聯關係:
```javascript
function Foo(){ this.name = name }
Foo.prototype.myName = function (){ return this.name }
function Bar(name,label){ Foo.call(this,name) this.label = label }
// 我們建立一個新的Bar.prototype物件並關聯到Foo.prototpye
Bar.prototype = Object.create(Foo.prototpye)
// 現在的話 是沒有Bar.prototype.constructor ,需要使用的話可能需要手動來修復一下了
Bar.prototype.myLabel = function(){ return this.label }
var a = new Bar('a','obj a')
a.myName()// 'a'
a.myLabel() // 'b'
```
通過上面的程式碼和圖片我們可以看出來,在JavaScript中實現 ‘繼承’的話,實際上是一種委託的關係,當呼叫a.sayName()的時候實際上是呼叫的Bar.prototype物件上的方法,而我們也知道定義在原型上面方法或者屬性是所有例項共享的,而不是每一個例項都有單獨的一份,所以說在JavaScript中實現是通過委託(依賴原型鏈這個特性)這種方式來把幾個物件組織起來的。
物件關聯
現在我們知道了[[Prototpye]]機制就是存在於物件中的一個內部連結,他會引用其他物件
這個連結的作用是:如果物件上沒有找到需要的屬性或者方法引用,引擎就會繼續在[[Prototpye]]關聯的物件上進行查詢,同理,如果在後者中也沒有找到需要的引用就會繼續查詢他的[[Prototype]],以此類推。這一系列物件的連結被稱為 原型鏈
ok,現在我們已經知道了[[Prototype]]機制(委託),那為什麼會提供這樣的機制呢?
我感覺就是我們的應用程式都是由簡單的程式碼組成的,那需要提供一種可以把簡單的程式碼組織起來的方式,也就是說需要一種機制來把他們關聯起來,JavaScript中選擇的是通過[[Prototpye]]鏈這種方式,其他的類語言選擇的是使用類這種方式來組織程式碼。
Object.create()
前面我們也一直在用這個方法,Object.create()會建立一個新物件並把它關聯到我們制定的物件上,這樣我們就可以充分發揮[[Prototype]]機制的威力並且避免不必要的麻煩
因此我們不需要類來建立兩個物件之間的關係,只需要通過委託來關聯物件就足夠了,而Object.create()不包含任何的 ’類的詭計‘,所以我們可以完美的建立我們想要的關聯關係。
總結
上面我們詳細的講解了關於this和原型的知識,當然有的地方說的可能不是很清楚或者有些地方不正確,但這也算是自己的總結和思考,有問題很正常,人總是進步的嘛,遇見問題不要慌,因為這是我們要進步的標誌。