做用域是一套規則,用於在何處以及如何查找變量(標識符).若是查找的目的是對變量進行賦值,那麼就行使用LHS
查詢;若是目的是獲取變量的值,就會使用RHS
查詢.賦值操做會致使LHS
查詢. =
操做符或調用函數時傳入參數的操做都會致使關聯做用域的賦值操做.css
LHS
,爲變量取值RHS
JavaScript
引擎首先會在代碼執行前對其編譯,在這個過程當中,像var a = 2
這樣的聲明被分解成兩個獨立的步驟:git
var a
在其做用域中聲明新變量.這會在最開始的階段,也就是代碼執行前進行.a=2
會查詢(LHS查詢)變量a並對其進行賦值 LHS
和RHS
查詢都會在當前執行做用域中開始,若是有須要(沒有找到所需的標識符),就會向上級做用域繼續查找目標標識符,這樣每次上升一級,最後抵達全局做用域,不管找到或沒找到都將中止.不成功的RHS
引用會致使拋出ReferenceError
異常. 不成功的LHS
引用會致使自動隱式地建立一個全局變量(非嚴格模式下),該變量使用LHS
引用的目標做爲標識符,或者拋出ReferenceError
(嚴格模式下)github
對變量賦值LHS
,爲變量取值RHS
chrome
對變量賦值`LHS`,爲變量取值`RHS`
複製代碼
詞法做用域意味着做用域是由書寫代碼時函數聲明的位置決定. 編譯的詞法分析階段基本可以知道所有標識符在那裏以及如何聲明的,從而可以預測在執行過程當中如何對它們進行查找.設計模式
JavaScript中有兩個機制能夠"欺騙"詞法做用域: eval(...)
和with
. 前者能夠對一段包含一個或多個聲明的"代碼"字符串進行演算,並藉此來修改已存在的詞法做用域. 後者本質上是經過一個對象的引用看成做用域來處理,將對象的屬性看成做用域中的標識符來處理,從而建立一個新的詞法做用域.數組
這兩個機制的反作用是引擎沒法在編譯時對做用域查找進行優化,由於引擎只能謹慎的認爲這樣的優化是無效的.使用這其中一種機制都將致使代碼運行變慢.不要使用它們瀏覽器
函數是JavaScript中最多見的做用域單元. 本質上,聲明在一個函數內部的變量或者函數會在所處的做用域中被"隱藏"起來,這是有意爲之的良好軟件的設計原則安全
但函數不是惟一的做用域單元. 塊做用域指的是變量和函數不只能夠屬於所處的做用域也能夠屬於某個代碼塊.babel
try/catch
結構在catch
分句中具備塊做用域在ES6中引入了let
關鍵字,用來在任何代碼塊中聲明變量,if(..){let a = 2}
會聲明一個劫持if
的{...}
塊的變量,並將變量添加到這個塊中.數據結構
有些人認爲塊做用域不該該徹底做爲函數做用域的替代方案.兩種功能應該同時存在,開發者能夠而且也應該根據須要選擇使何種做用域,創造可讀,可維護的優良代碼
咱們習慣將var a = 2
;看做是一個聲明,而實際上JS引擎並不認爲. 它將var a
和 a=2
看成兩個單獨的聲明,第一個是編譯階段
的任務,而第二個則是執行階段
的任務
這意味着不管做用域中的聲明出如今什麼地方,都將在代碼自己被執行前首先進行處理,能夠將這個過程形象地想象成全部的聲明(變量和函數)都會被"移動"到各自做用域的最頂端,這個過程稱爲提高
聲明自己會被提高,而包括函數表達式的賦值在內的賦值操做並不會提高.
要注意避免重複聲明,特別是當普通的var
聲明和函數聲明混合在一塊兒的時候,不然會引發不少危險的問題!
PS: 函數聲明和變量聲明都會被提高. 可是一個值得注意的細節(這個細節能夠出如今有多個'重複'聲明的代碼中)是函數會首先被提高,而後纔是變量.
foo() //1
var foo
function foo() {
console.log('1')
}
foo = function() {
console.log('2')
}
複製代碼
閉包無處不在,你只須要識別並擁抱它
function foo() {
var a = 2
function bar() {
console.log(a)
}
return bar
}
var baz = foo()
baz() //2
複製代碼
在這個例子中,它在本身定義的詞法做用域之外的地方執行
在foo()
執行後,一般會期待foo()
的整個內部做用域都被銷燬,由於咱們知道引擎有垃圾回收器用來釋放再也不使用的內存空間. 因爲看上去foo()
的內容不會再被使用,因此咱們會認爲垃圾回收機制會將其回收
而閉包能夠阻止這件事的發生. 拜bar()
所聲明的位置所賜,它擁有涵蓋foo()
內部做用域的閉包,使得該做用域一直存活,以供bar()
在以後任什麼時候間進行引用
bar()
依然持有對該做用域的引用,而這個引用就叫做閉包
固然,不管使用何種方式對函數類型的值進行傳遞,當函數在別處被調用時均可以觀察到閉包
function foo () {
var a = 2
function baz() {
console.log(a) //2
}
bar(baz)
}
function bar(fn) {
fn() //這就是閉包
}
複製代碼
var fn
function foo() {
var a = 2
function baz() {
console.log(a)
}
fn = baz // 將baz分配給全局變量
}
function bar() {
fn() // 這就是閉包
}
foo()
bar() //2
複製代碼
不管經過何種手段將內部函數傳遞到所在詞法做用域之外,它都會持有對原始定義做用域的引用,不管在何處執行這個函數都會使用閉包
PS:定時器中的閉包
function wait(mes) {
setTimeout(function timer(){
console.log(mes) //這就是閉包
},1000)
}
複製代碼
解析: 將一個內部函數傳遞給
setTimeout(...)
.timer
具備涵蓋wait(...)
做用域的閉包,所以還保有對變量message的引用.
定時器,事件監聽,Ajax請求,垮窗口通訊,Web Workers或者任何其的異步任務中,
只要使用了回調函數,實際上就是在使用閉包PS:典型例子
for(var i = 1;i<=5;i++) {
setTimeout(function timer(){
console.log(i) // 每秒一次的頻率輸出五次6
},i*1000)
}
複製代碼
解析:
6從哪兒來?
循環終止條件是i再也不
<=5
,條件首次成立時i的值是6運行機制
根據做用域的工做原理,實際狀況是儘管循環中五個函數是在各個迭代中分別定義的,可是它們都被封閉在一個共享的全局做用域中,所以其實是同一個並僅有一個i
改進:
for(var i=1;i<=5;i++) {
(function(j){
setTimeout(function timer(){
console.log(j)
},j*1000)
})(i)
}
複製代碼
解析:
在迭代中使用IIFE會爲每次迭代都生成一個新的做用域,使得延遲函數的回調能夠將新的做用域封閉在每一個迭代內部,每一個迭代中都會含有一個具備正確值的變量提供咱們訪問
進一步改進:
for(var i=1;i<=5;i++){
let j=i // 閉包的塊做用域
setTimeout(function timer(){
console.log(j)
},j*1000)
}
複製代碼
終極模式:
for(let i=1;i<=5;i++){
setTimeout(function timer(){
console.log(i)
},i*1000)
}
複製代碼
解析:
let
聲明變量有一個特殊行爲,指的是變量在循環過程當中不止聲明一次,每次迭代都會聲明. 隨後的每一個迭代都會使用上一個迭代結束的值來初始化這個變量
function CoolModule(){
var something = 'cool'
var another = [1,2,3]
function doSomething() {
console.log(something)
}
function doAnother() {
console.log(another.join('!'))
}
return {
doSomething:doSomething,
doAnother:doAnother
}
}
複製代碼
CoolModule()
只是一個函數,必需要經過調用它來建立一個模塊實例. 若是不執行外部函數,內部做用域和閉包都沒法建立.
- 必須有外部的封閉函數,該函數必須至少調用一次(每次調用都會建立一個新的模塊實例)
- 封閉函數必須返回至少一個內部函數,這樣內部函數才能在私有做用域中造成閉包,而且能夠訪問或者修改私有的狀態
閉包其實是一個普通且明顯的事實,那就是咱們在詞法做用域的環境下寫代碼,而其中的函數也是值,咱們能夠開心的傳來傳去
當函數能夠記住並訪問所在的詞法做用域,即便函數是在當前詞法做用域以外執行,這時就產生了閉包
模塊有兩個主要的特徵: (1)爲建立內部做用域而調用一個包裝函數 (2) 包裝函數的返回值必須至少包括一個對內部函數的引用,這樣就會建立涵蓋整個函數內部做用域的閉包.
動態做用域並不關心函數和做用域是如何聲明以及在何處聲明的,只關心它們從何處調用. 換句話是,做用域鏈是基於調用棧,而不是代碼中的做用域嵌套.
function foo(){
console.log(a) // 2 (不是3)
}
function bar() {
var a = 3
foo()
}
var a = 2
bar()
複製代碼
解析:
事實上JavaScript並不具備動態做用域. 它只有詞法做用域,簡單明瞭. 可是this的動態機制某種程度上很像動態做用域
詞法做用域是在寫代碼或者定義時肯定的,而動態做用域是在運行時肯定的.(this也是) 詞法做用域關注函數在何處聲明,而動態做用域關注函數從何處調用.
{
let a = 2
console.log(2) // 2
}
console.log(a) // ReferenceError
// =====> ES6以前
try{
throw 2
}catch (a) {
console.log(2)
}
console.log(a) // ReferenceError
複製代碼
問: 爲何不直接使用IIFE來建立做用域?
答:
首先,
try/catch
的性能的確糟糕,但技術層面上沒有合理的理由來講明try/catch
必須這麼慢,或者會一直這麼慢下去. 自從TC39支持在ES6的轉換器中使用try/catch
後,Traceur團隊已經要求chrome對try/catch
的性能進行改進,他們顯然有很充分的動機來作這件事情其次: IIFE和try/catch並非等價的,由於若是將一段代碼中的任意一部分拿出來用函數進行包裹,會改變這段代碼的含義,其中this,return,break和continue都會發生變化. IIFE並非一個普通的解決方案,它只適應在某些狀況下進行手動操做
箭頭函數在涉及this綁定的行爲和普通函數的行爲徹底不一致. 它放棄了因此普通this綁定的規則,取而代之的是用當前的詞法做用域覆蓋了this的值(箭頭函數不止於少寫代碼)
this在任何狀況下都不指向函數的詞法做用域. 在JavaScript
內部,做用域確實和對象相似,可見的標識符都是它的屬性. 可是做用域"對象"沒法經過JavaScript
代碼訪問,它存在JavaScript
引擎內部
每當你想要把this和詞法做用域的查找混合使用時,必定要提醒本身,這是沒法實現的
this是在運行時進行綁定的,並非在編寫時綁定的,它的上下文取決於函數調用時的各類條件. this的綁定和函數的聲明的位置沒有任何關係,只取決於函數的調用方法
this其實是在函數被調用時發生的綁定,它指向什麼徹底取決於函數在哪裏被調用
調用位置就是函數在代碼中被調用的位置
調用棧就是爲了到達當前執行位置所調用的因此函數
獨立函數調用
function foo() {
console.log(this.a)
}
var a = 2
foo() // 2
複製代碼
解析: foo()是直接使用不帶任何修飾的函數引用進行調用的,所以只能使用
默認綁定
,沒法應用其餘規則
若是使用嚴格模式(strict mode)
,則不能將全局對象用於默認綁定,所以this會綁定到undefined
function foo() {
"use strict"
console.log(this.a)
}
var a = 2
foo() // TypeError: this is undefined
複製代碼
這裏有一個微妙但很是重要的細節,雖然this的綁定規則徹底取決於調用位置,可是隻有foo()運行在非strict mode
下時,默認綁定才能綁定到全局對象;在嚴格模式下調用foo()
則不影響默認綁定.
function foo() {
console.log(this.a)
}
var a = 2
(function(){
"use strict"
foo() //2
})()
複製代碼
forexample:
function foo () {
console.log(this.a)
}
var obj = {
a: 2,
foo:foo
}
obj.foo() //2
複製代碼
當函數引用有上下文對象時,隱式綁定規則會把函數調用中的this綁定到這個上下文對象
對象屬性引用鏈中只有上一層或者說最後一層在調用位置中起做用.
function foo() {
console.log(this.a)
}
var obj2 = {
a:42,
foo:foo
}
var obj1 = {
a:2,
obj2:obj2
}
obj1.obj2.foo() // 42
複製代碼
一個最多見的this
綁定的問題就是被隱式綁定的函數丟失綁定對象,也就是說它會應用默認綁定,從而把this綁定到全局對象或者undefined
上,取決因而否是嚴格模式
function foo() {
console.log(this.a)
}
var obj = {
a:2,
foo:foo
}
var bar = obj.foo // 函數別名
var a = 'oops,global'
bar() // oops,global
複製代碼
解析:
雖然bar是
obj.foo
的一個引用,可是實際上,它引用的是foo
函數自己,所以此時的bar()
實際上是一個不帶任何修飾的函數調用,使用的默認綁定
function foo() {
console.log(this.a)
}
function doFoo(fn){
// fn其實引用的是foo
fn(); // <--調用位置
}
var obj = {
a:2
foo:foo
}
var a = 'oops,global'
doFoo(obj.foo) // oops,global
複製代碼
解析:
參數傳遞其實就是一種隱式賦值,所以咱們傳入函數也會被隱式賦值
JavaScript
提供的絕大多數函數以及咱們本身建立的全部函數均可以使用call(...)
和apply(...)
方法
它們的第一個參數是一個對象,是給this準備的,接着在調用函數時將其綁定到this. 由於你能夠直接指定this的綁定對象,所以咱們稱之爲顯示綁定.
硬綁定是一種很是常見的模式,ES5提供了內置的方法Function.prototype.bind(...)
bind(...)
會返回一個硬編碼的新函數,它會把你指定的參數設置爲this的上下文並調用原始函數
包括內置函數和自定義函數在內的全部函數均可以用new來調用,這種函數調用被稱爲構造函數調用. 實際上並不存在所謂的"構造函數",只有對於函數的構造調用
使用new來調用函數,或者說發生構造函數調用時,會自動執行下面的操做
按照下面的順序進行判斷(記住特例):
var bar = new foo()
var bar = foo.call(obj2)
var bar = obj.foo()
var bar = foo()
不過...凡是都有例外,接下來咱們介紹例外吧
若是咱們把null
或undefined
做爲this
的綁定對象傳入call
apply
或者bind
,這些值在調用時會被忽略,實際應用的是默認綁定
function foo() {
console.log(this.a)
}
var a = 2
foo.call(null) // 2
複製代碼
柯里化傳入更安全的this
在JavaScript中建立一個空對象最簡單的方法是Object.create(null)
,它不會建立Object.prototype
這個委託,因此比{}
更空
function foo(a,b) {
console.log("a" + a + ", b :" + b)
}
// DMZ空對象
var Ø = Object.create(null)
// 把數組展開成參數
foo.apply(Ø,[2,3]) //a:2,b:3
//使用bind(...)進行柯里化
var bar = foo.bind(Ø,2)
bar(3) //a:2,b:3
複製代碼
var a = 2
var o = {a:3,foo:foo}
var p = {a:4}
o.foo() // 3
(p.foo = o.foo)() // 2
複製代碼
賦值表達式p.foo = o.foo
的返回值是目標函數的引用,所以調用位置是foo()而不是p.foo()
或0.foo()
,故這裏會應用默認綁定
Element.prototype.hide = () => { this.style.display = 'none' }
複製代碼
會報錯,查看babel解析後的代碼,發現this沒有綁定上:
Element.prototype.hide = function() {
undefined.style.display = 'none'
}
複製代碼
箭頭函數的 this 是靜態的,也就是說,只須要看箭頭函數在什麼函數做用域下聲明的,那麼這個 this 就會綁定到這個函數的上下文中。即「穿透」箭頭函數。
例子裏的箭頭函數並無在哪一個函數裏聲明,因此 this 會 fallback 到全局/undefined
"穿透"到最近的詞法做用域(注意對象的{}不算外層做用域),若是外層沒有被函數包裹,那麼就是window
例如:
let a = {
foo() {
console.log(this)
},
foo1: () => {
console.log(this)
},
foo2: function foo2() {
console.log(this)
}
}
a.foo() // a
a.foo1() // window
a.foo2() // a
複製代碼
若是要斷定一個運行函數的this綁定,就須要找到這個函數的直接調用位置. 找到以後能夠順序應用下面這四條規則來判斷this的綁定函數
(new綁定)
(顯示綁定)
(隱式綁定)
(默認綁定)
注意: 有些調用可能在無心中使用默認綁定規則. 若是想'更安全'地忽略this綁定,你可使用一個DMZ對象,
好比
var Ø = Object.create(null)
,以保護全局對象
ES6中的箭頭函數並不會使用四條標準的綁定規則,而是根據當前的詞法做用域來決定this,具體說,箭頭函數會繼承外層函數調用的this綁定(不管this綁定到什麼). 這其實和ES6以前的var self = this的機制同樣
注意:
null
有時會被看成一種對象類型,可是這其實只是語言自己的一個bug,即對null
執行typeof null
時返回字符串"object". 實際上,null
自己是基本類型黑科技: 原理是這樣的,不一樣的對象在底層都表示爲二進制,在
JavaScript
中二進制前三位都爲0的話會被判斷爲object
類型,null
的二進制表示是全0,天然前三位也是0,因此typerof
時會返回"object"
必要時語言會自動把字符串字面量轉換成一個String對象
在對象中,屬性名永遠都是字符串。若是使用string之外的其餘值做爲屬性名,它首先會被轉換爲一個字符串。
JSON.stringify(obj)
)是否能夠修改屬性的值
屬性是否可配置,把configurable修改爲false是單向操做,沒法撤銷
除了沒法修改,configurable:false
還會禁止這個屬性
屬性是否會出如今對象的屬性枚舉中
全部的方法建立的都是淺不變性,它們只會影響目標對象和它的直接屬性.若是目標對象引用了其餘對象,其餘對象的內容不受影響,仍然是可變的
4.1 對象常量
結合writable:false
和configurable:flase
就能夠建立一個真正的常量屬性(不可修改,不可從新定義或刪除)
var object1 = {}
Object.defineProperty(object1,"FAVORITE_NUMBER",{
value:42,
writable:false,
configurable:false
})
複製代碼
4.2 禁止擴展
若是想禁止一個對象添加新屬性而且保留已有屬性,可使用Object.preventExtensions()
var myObj = { a : 2 }
Object.preventExtensions(myObj)
myObj.a = 3
myObj.a = undefined
複製代碼
在非嚴格模式下,建立屬性a會靜默失敗. 在嚴格模式下,將會拋出TypeError
錯誤
4.3 密封
Object.seal(...)
會建立一個密封的對象,這個方法實際上會在一個現有對象上調用Object.preventExtensions(...)
並將全部現有的屬性標記爲configurable:flase
因此,密封后的對象不能添加屬性,不能從新配置屬性或者刪除現有屬性(只能修改屬性的值
)
4.4 凍結
Object.freeze(...)
會建立一個凍結對象,這個方法實際上會在一個現有對象上調用Object.seal(...)
並把全部"數據訪問"屬性標記爲writable:fasle
,這樣就沒法修改它們的值
這個方法是能夠應用在對象上的級別最高的不可變性
在語言規範中,obj.a
在obj上其實是實現了[[Get]]操做. 對象默認的內置[[Get]]操做首先在對象上查找是否有名稱相同的屬性,若是找到就會返回這個屬性的值
若是不管如何都沒有找到名稱相同的屬性,那麼[[Get]]操做會返回值undefined
var myObj = {
a: 2
}
myObj.b // undefined
複製代碼
這種方法和訪問變量是不同的. 若是你引用了一個當前詞法做用域中不存在的變量,並不會像對象屬性同樣返回
undefined
,而是會拋出一個ReferenceError
異常故,僅經過返回值,你沒法判斷一個屬性是存在而且持有一個
undefined
值,仍是變量不存在,因此[[Get]]
沒法返回某個特定值而返回默認的undefined
[[Put]]被觸發時,實際行爲分紅兩種:
writable
是不是false? 若是是,在非嚴格模式下靜默失敗,在嚴格模式下拋出TypeError
異常在ES5中可使用getter和setter部分改寫默認操做,可是隻能應用在單個屬性上,沒法應用在整個屬性上.
getter和setter都會覆蓋單個屬性默認的[[Getter]]和[[Setter]]操做
當咱們經過屬性名訪問某個值時可能返回undefined
,這個值多是對象屬性中存儲的undefined
,也有多是屬性不存在返回的undefined
,那麼咱們怎麼區分呢?
Forexample:
var obj = {
a:2
}
('a' in obj) // true
('b' in obj) // false
obj.hasOwnProperty('a') //true
obj.hasOwnProperty('b') // fasle
複製代碼
in 操做符會檢查屬性是否在對象及其[[Prototype]]
原型鏈中,hasOwnProperty(...)
只會檢查屬性是否在對象中,不會去檢查[[Prototype]]
鏈
看起來in操做符能夠檢查容器是否有某個值,可是它實際上檢查的是某個屬性名是否存在.
PS:
4 in [1,2,4] // false // 該數組包含的屬性名是0 1 2 並無咱們要找的4 複製代碼
最好只在對象上應用for...in循環中
JavaScript中的對象有字面形式(var a= {...}
)和構造形式(var a = new Array(...)
)
"萬物皆對象"的概念是錯誤的. 對象是6個或者7個(null)基礎類型之一. 對象有包括function在內的子類型,不一樣子類型具備不一樣的行爲,好比內部標籤[object Array]
表示是對象的子類型數組
對象就是鍵值對的集合. 能夠經過.propName
或者['propName']
語法來獲取屬性值. 訪問屬性時,引擎實際上會調用內部的默認[[Get]]
操做(在設置屬性值時是[[Put]]
),[[Get]]
操做檢查對象自己是否包含這個屬性,若是沒找到的話還會查找[[Prototype]]
鏈
屬性的特性能夠經過屬性描述符來控制,好比writable
和configurable
. 還可使用Object.preventExtensions(...)
,Object.seal(...)
和Object.freeze(...)
來設置對象的不可變性級別,其中Object.freeze(...)
是應用在對象上不可變性的最高級別.
屬性不必定包含值-它們多是具有getter/setter
的"訪問描述符". 此外屬性能夠是可枚舉或不可枚舉的,這決定了它們是否會出如今for...in
循環中
可使用for...of
遍歷數據結構(數組,對象等等)中的值,for...of
會尋找內置或者定義的@@iterator對象並調用它的next()
方法來遍歷數據值
類實例是由一個特殊的類方法構造的,這個方法名一般和類名相同,被稱爲構造函數. 這個方法的任務就是初始化實例須要的全部信息
在傳統的面向對象的語言中super
還有一個功能,就是從子類的構造函數中經過super
能夠直接調用父類的構造函數.一般來講這沒什麼問題,由於對於真正的類來講,構造函數是屬於類的.
然而,在JavaScript
中剛好相反-實際上類是屬於構造函數的. 因爲JavaScript中父類和子類的關係只存在於二者構造函數對應的.prototype
對象中,所以它們的構造函數之間並不存在直接關係,從而沒法簡單地實現二者的相對引用.
類是一種設計模式. 許多語言提供了對於面向對象類軟件設計的原生語法. JavaScript也有相似的語法,可是和其餘語言中的類徹底不同
類意味着複製
傳統的類實例化時,它的行爲會被複制到實例中. 類被繼承時,行爲也會複製到子類中
多態(在繼承鏈的不一樣層次名稱相同可是功能不一樣的函數)看起來彷佛是從子類引用父類,可是本質上引用的實際上是複製的結果
JavaScript不會(像類那樣)自動建立對象的副本, 只能複製引用,沒法複製被引用的對象或者函數自己
混入模式
(利用for...in遍歷判斷對象不存在的屬性,不存在則添加)能夠用來模擬類的複製行爲,可是一般會產生醜陋而且脆弱的語法,好比顯式僞多態(Object.methidName.call(this,....)
),這會讓代碼更難懂而且難以維護.
顯示混入實際上沒法徹底模擬類的複製行爲,由於對象(和函數,函數也是對象)只能複製引用,沒法複製被引用的對象或者函數自己.
總地來講,在JavaScript中模擬類是得不償失的,雖然能解決當前的問題,可是可能會埋下更多的隱患.
JavaScript
中的對象有一個特殊的[[prototype]]
內置屬性,其實就是對於其餘對象的引用. 幾乎全部的對象在建立時[[Prototype]]
屬性都會被賦予一個非空的值
var anotherObj = {
a:2
}
// 建立一個關聯到 antherObj 的對象
var myObj = Object.create(anotherObj)
複製代碼
全部普通的[[prototype]]鏈最終都會指向內置的Object.prototype
在[第三部分對象中]提到過,給一個對象設置屬性並不只僅是添加一個新屬性或者修改已有的屬性值
myObj.foo = 'bar'
複製代碼
myObj
對象中包含名爲foo
的普通數據訪問屬性,這條賦值語句只會修改已有的屬性值foo
不是直接存在於myObj
中,[[Prototype]]
鏈就會被遍歷,相似[[Get]]
操做. 若是原型鏈找不到foo
,foo
就會被直接添加到myObj
上foo
存在原型鏈上層,賦值語句myObj.foo = 'bar'
的行爲就會有些不一樣,下面是詳細的介紹myObj
中也出如今myObj
的[[Prototype]]
鏈上層,那麼就會發生屏蔽. myObj
中包含的foo
屬性會屏蔽原型鏈上層的全部foo
屬性,由於myObj.foo
老是會選擇原型鏈中最底層的foo
屬性分析若是foo不直接存在
myObj
中而是存在原型鏈上層時,myObj.foo = 'bar'
會出現三種狀況 >>>
- 若是在
[[Prototype]]
鏈上層存在名爲foo的普通數據訪問屬性而且沒有
被標記只讀,那會直接在myObj
中添加一個名爲foo的新屬性,它是屏蔽屬性
- 若是在
[[Prototype]]
鏈上層存在foo,可是它被標記爲只讀,那麼沒法修改已有屬性或者在myObj上建立屏蔽屬性. 若是運行在嚴格模式下,代碼會拋出一個錯誤. 不然, 這條賦值語句會被忽略. 總之,不會發生屏蔽- 若是在
[[Prototype]]
鏈上層存在foo屬性而且它是一個setter,那就必定會調用這個setter
.foo
不會被添加到myObj
,也不會從新定義foo
這個setter
只讀屬性會阻止
[[Prototype]]
鏈下層隱式建立(屏蔽)屬. 這看起來有點奇怪,myObj對象會由於有一個只讀foo就不能包含foo屬性. 更奇怪的是,這個限制只存在於=賦值中,使用object.defineProperty(...)
並不會受到影響
經過調用new Foo()
建立的每一個對象將最終被[[Prototype]]
連接到這個Foo.prototype
對象
function Foo() {
...
}
var a = new Foo()
Object.getPrototypeOf(a) === Foo.prototype // true
複製代碼
在面向對象的語言中,類能夠被複制屢次,就像模具製做東西同樣.
可是在JavaScript
中,並無相似的複製機制. 咱們不能建立一個類的多個實例,只能建立多個對象,它們的[[Prototype]]
關聯的是同一個對象. 可是在默認狀況下並不會進行復制,所以這些對象之間並不徹底失去聯繫,它們是互相關聯的.
"原型繼承"嚴重影響了你們對JavaScript
機制真實原理的理解
繼承意味着複製操做,JavaScript
並不會複製對象屬性. 相反,JavaScript
會在兩個對象之間建立一個關聯,這樣一個對象就能夠經過委託訪問到另外一個對象的屬性和函數
function Foo() {
// ...
}
Foo.prototype.constructor === Foo // true
var a = new Foo()
a.constructor === Foo // true
複製代碼
Foo.prototype
默認有一個公有而且不可枚舉的屬性.constructor
,這個屬性引用的是對象關聯的函數.能夠看到經過"構造函數"調用new Foo(...)
建立的對象也有一個.constructor
屬性,指向"建立這個對象的函數"
new
會劫持全部普通函數並構造對象的形式來調用它
function NothingSpecial() {
console.log("Don't mind me")
}
var a = new NothingSpecial()
// Don't mind me
a // {}
複製代碼
在JavaScript
中對於"構造函數"最準確的解釋是,全部帶new的函數調用
函數不是構造函數,可是當且使用new時,函數調用會變成"構造函數調用"
function Person(){
this.name="monster1935";
this.age='24';
this.sex="male";
}
console.log(Person()); //undefined
console.log(new Person());//Person {name: "monster1935", age: "24", sex: "male"}
複製代碼
string
,number
,boolean
,null
,undefined
),上述幾種類型的狀況與沒有返回值的狀況相同,實際返回實例化的對象function Person(){
this.name="monster1935";
this.age='24';
this.sex="male";
return "monster1935";
}
console.log(Person()); //monster1935
console.log(new Person());//Person {name: "monster1935", age: "24", sex: "male"}
複製代碼
function Person(){
this.name="monster1935";
this.age='24';
this.sex="male";
return {
name:'Object',
age:'12',
sex:'female'
}
}
console.log(Person()); //Object {name: "Object", age: "12", sex: "female"}
console.log(new Person());//Object {name: "Object", age: "12", sex: "female"}
複製代碼
function Foo(name) {
this.name = name
}
Foo.prototype.myName = function() {
return this.name
}
function Bar(name,label) {
Foo.call(this,name)
this.label = label
}
// 建立一個新的Bar.prototype對象並關聯到Foo.prototype
Bar.prototype = Object.create(Foo.prototype)
//注意 如今沒有Bar.prototype.constructor了
Bar.prototype.myLabel = function() {
return this.label
}
var a = new Bar("a","obj.a")
a.myName() // "a"
a.myLabel() // "obj.a"
複製代碼
注意: 下面這兩種方式是常見的錯誤作法,實際上它們都存在一些問題
Bar.prototype = Foo.prototype // 直接引用的是Foo.prototype對象,賦值語句會互相修改 // 基本知足要求,可是可能會產生一些反作用 Bar.prototype = new Foo() 複製代碼
所以,要建立一個合適的對象,咱們必須使用
Object.create(...)
而不是使用具備反作用的Foo(...)
這樣惟一的缺點就是建立一個新對象而後把舊對象拋棄掉,不能直接修改已有的默認對象
// ES6以前須要拋棄默認的Bar.prototype
Bar.prototype = Object.create(Foo.prototype)
// ES6開始能夠直接修改現有的Bar.prototype
Object.setPrototypeOf(Bar.prototype,Foo.prototype)
複製代碼
instanceOf
a instanceOf Foo
, 左邊是一個普通的對象,右邊是一個函數. 回答的問題是:"在a的整條[[Prototype]]鏈中是否有Foo.prototype指向的對象?"
isPrototypeOf
Foo.isPrototypeOf(a)
, 回答的問題是:"在a的整條[[Prototype]]鏈中是否出現過Foo.prototype"
Object.getPrototypeOf(a)
瀏覽器也支持一種非標準的方法訪問內部的[[Prototype]]屬性
a._proto_
(是可設置屬性)
若是在對象上沒有找到須要的屬性或者方法引用,引擎就會繼續在[[Prototype]]關聯的對象上進行查找. 若是後者中也沒有找到須要的引用就會繼續查找它的[[Prototype]],以此類推,這一系列的連接稱爲"原型鏈"
Object.create(...)
Object.create(...)會建立一個對象並把它關聯到咱們指定的對象
Object.create(null)會建立一個擁有空[[Prototype]]連接的對象,這個對象沒法進行委託. 因爲這個對象沒有原型鏈,因此instanceOf操做符沒法進行判斷
若是要訪問對象中並不存在的一個屬性,[[Get]]操做就會查找對象內部[[Prototype]]關聯的對象. 這個關聯關係實際上定義一條"原型鏈",在查找屬性時就會對它進行遍歷
全部普通對象都有內置的Object.prototype,指向原型鏈的頂端,若是在原型鏈中找不到指定的屬性就會中止. toString()
,valuOf()
和其餘一些通用的功能都存在於Object.prototype對象上,所以語言中全部的對象均可以使用他沒
關聯兩個對象最經常使用的方法是使用new關鍵字進行函數調用,在調用的4個步驟中會建立一個關聯其餘對象的新對象
使用new調用函數時會把新對象的.prototype屬性關聯到"其餘對象". 帶new的函數調用一般被稱爲"構造函數調用",儘管它們實際上和傳統面向類語言中的構造函數不同
雖然這些JavaScript機制和傳統面向對象的"類初始化"和"類繼承"很類似,可是JavaScript
中的機制有一個核心區別,那就是不會進行復制,對象之間是經過內部的[[Prototype]]鏈關聯的
出於各類緣由,以"繼承"結尾的術語和其它面向對象的術語都沒法幫助你理解JavaScript
的真實機制,相比之下,"委託"是一個更合適的術語,由於對象之間的關係不是複製而是委託
JavaScript
中原型鏈這個機制的本質就行對象之間的關聯關係
Js中函數之因此能夠訪問call(...)
,apply(...)
,bind(...)
是由於函數自己是對象
行爲委託認爲對象之間是兄弟關係,互相委託,而不是父類和子類關係.JavaScript
的[[Prototype]]機制本質上就是行爲委託節制. 也就是說,咱們能夠選擇在Js中努力實現類機制,也能夠擁抱更天然的[[Prototype]]委託機制
見下章
傳統面向類的語言中父類和子類,子類和實例之間實際上是複製操做,可是在[[Prototype]]
中沒有複製,相反,它們之間只有委託關聯
class Widget{
constructor(width,height) {
this,width = width || 50
this.height = height || 50
this.$elem = null
}
render($where) {
if(this.$elem) {
this.$elem.css({
width:this.width + 'px',
height:this.height+'px'
}).appendTo($where)
}
}
}
class Button extends Widget {
constructor(width, height, label) {
super(width, height)
this.label = label || 'Default'
this.$elem = $("<button>").text(this.label)
}
render($where) {
super.render($where)
this.$elem.click(this.onClick.bind(this))
}
onClick(evt) {
console.log("Button" + this.label + 'clicked!')
}
}
複製代碼
除了語法更好看以外,ES6還解決了什麼問題?
- 再也不引用雜亂的.prototype了
- Button聲明直接"繼承"了Widget,再也不須要經過Object.create(...)來替換
.prototype
對象,也不須要設置._proto_
或者Object.setPrototypeOf(...)
- 能夠經過
super(...)
來實現相對多態,這樣任何方法均可以引用原型鏈上層的同名方法. 構造函數不屬於類,因此沒法相互引用---super()
能夠完美解決構造函數的 問題- class字面語法不能聲明屬性.看起來這是一種限制,可是它會排除掉許多很差的狀況,若是沒有這種限制的話,原型鏈末端的"實例"可能會意外地獲取其餘地方的屬性
- 能夠經過extends很天然地擴展對象類型,甚至是內置的對象類型,好比Array或RegExp
class基本上只是現有[[Prototype]]機制(委託)的一種語法糖
也就是說
class
並不會像傳統面向類的語言同樣在聲明時靜態複製全部行爲.若是修改或者替換了父"類"中的一個方法,那麼子"類"和全部實例都會受到影響,由於它們在定義時並無進行復制,只是使用基於[[Prototype]]
的實時委託
class語法沒法定義類成員屬性(只能定義方法)
原文地址: 傳送門
Github: 歡迎Startwq93