你不知道的JavaScript(上) - 閱讀筆記

你不知道的JavaScript(上)


① 做用域和閉包

一. 做用域是什麼?

做用域是一套規則,用於在何處以及如何查找變量(標識符).若是查找的目的是對變量進行賦值,那麼就行使用LHS查詢;若是目的是獲取變量的值,就會使用RHS查詢.賦值操做會致使LHS查詢. =操做符或調用函數時傳入參數的操做都會致使關聯做用域的賦值操做.css

  • PS: 對變量賦值LHS,爲變量取值RHS

JavaScript引擎首先會在代碼執行前對其編譯,在這個過程當中,像var a = 2這樣的聲明被分解成兩個獨立的步驟:git

  1. 首先,var a在其做用域中聲明新變量.這會在最開始的階段,也就是代碼執行前進行.
  2. 接下來,a=2會查詢(LHS查詢)變量a並對其進行賦值 LHSRHS查詢都會在當前執行做用域中開始,若是有須要(沒有找到所需的標識符),就會向上級做用域繼續查找目標標識符,這樣每次上升一級,最後抵達全局做用域,不管找到或沒找到都將中止.
  • PS: 把做用域鏈比喻成一棟建築

不成功的RHS引用會致使拋出ReferenceError異常. 不成功的LHS引用會致使自動隱式地建立一個全局變量(非嚴格模式下),該變量使用LHS引用的目標做爲標識符,或者拋出ReferenceError(嚴格模式下)github

對變量賦值LHS,爲變量取值RHSchrome

LHS與RHS

對變量賦值`LHS`,爲變量取值`RHS`
複製代碼

二. 詞法做用域

詞法做用域意味着做用域是由書寫代碼時函數聲明的位置決定. 編譯的詞法分析階段基本可以知道所有標識符在那裏以及如何聲明的,從而可以預測在執行過程當中如何對它們進行查找.設計模式

JavaScript中有兩個機制能夠"欺騙"詞法做用域: eval(...)with. 前者能夠對一段包含一個或多個聲明的"代碼"字符串進行演算,並藉此來修改已存在的詞法做用域. 後者本質上是經過一個對象的引用看成做用域來處理,將對象的屬性看成做用域中的標識符來處理,從而建立一個新的詞法做用域.數組

這兩個機制的反作用是引擎沒法在編譯時對做用域查找進行優化,由於引擎只能謹慎的認爲這樣的優化是無效的.使用這其中一種機制都將致使代碼運行變慢.不要使用它們瀏覽器

三. 函數做用域和塊做用域

函數是JavaScript中最多見的做用域單元. 本質上,聲明在一個函數內部的變量或者函數會在所處的做用域中被"隱藏"起來,這是有意爲之的良好軟件的設計原則安全

但函數不是惟一的做用域單元. 塊做用域指的是變量和函數不只能夠屬於所處的做用域也能夠屬於某個代碼塊.babel

  • 從ES3開始,try/catch結構在catch分句中具備塊做用域

在ES6中引入了let關鍵字,用來在任何代碼塊中聲明變量,if(..){let a = 2}會聲明一個劫持if{...}塊的變量,並將變量添加到這個塊中.數據結構

有些人認爲塊做用域不該該徹底做爲函數做用域的替代方案.兩種功能應該同時存在,開發者能夠而且也應該根據須要選擇使何種做用域,創造可讀,可維護的優良代碼

四. 變量提高

咱們習慣將var a = 2;看做是一個聲明,而實際上JS引擎並不認爲. 它將var aa=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)
}
複製代碼

解析:

  1. 6從哪兒來?

    循環終止條件是i再也不<=5,條件首次成立時i的值是6

  2. 運行機制

    根據做用域的工做原理,實際狀況是儘管循環中五個函數是在各個迭代中分別定義的,可是它們都被封閉在一個共享的全局做用域中,所以其實是同一個並僅有一個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. 封閉函數必須返回至少一個內部函數,這樣內部函數才能在私有做用域中造成閉包,而且能夠訪問或者修改私有的狀態

小結

閉包其實是一個普通且明顯的事實,那就是咱們在詞法做用域的環境下寫代碼,而其中的函數也是值,咱們能夠開心的傳來傳去

當函數能夠記住並訪問所在的詞法做用域,即便函數是在當前詞法做用域以外執行,這時就產生了閉包

模塊有兩個主要的特徵: (1)爲建立內部做用域而調用一個包裝函數 (2) 包裝函數的返回值必須至少包括一個對內部函數的引用,這樣就會建立涵蓋整個函數內部做用域的閉包.

六. 附錄

動態做用域

動態做用域並不關心函數和做用域是如何聲明以及在何處聲明的,只關心它們從何處調用. 換句話是,做用域鏈是基於調用棧,而不是代碼中的做用域嵌套.

function foo(){
    console.log(a) // 2 (不是3)
}
function bar() {
    var a = 3
    foo()
}
var a = 2
bar()
複製代碼

解析:

事實上JavaScript並不具備動態做用域. 它只有詞法做用域,簡單明瞭. 可是this的動態機制某種程度上很像動態做用域

主要區別

詞法做用域是在寫代碼或者定義時肯定的,而動態做用域是在運行時肯定的.(this也是) 詞法做用域關注函數在何處聲明,而動態做用域關注函數從何處調用.

塊做用域的替代方案

foeExample:
{
  let a = 2
  console.log(2) // 2
}
console.log(a) // ReferenceError

// =====> ES6以前
try{
   throw 2
 }catch (a) {
   console.log(2)
 }
 console.log(a) // ReferenceError

複製代碼
try/catch性能

問: 爲何不直接使用IIFE來建立做用域?

答:

首先,try/catch的性能的確糟糕,但技術層面上沒有合理的理由來講明try/catch必須這麼慢,或者會一直這麼慢下去. 自從TC39支持在ES6的轉換器中使用try/catch後,Traceur團隊已經要求chrome對try/catch的性能進行改進,他們顯然有很充分的動機來作這件事情

其次: IIFE和try/catch並非等價的,由於若是將一段代碼中的任意一部分拿出來用函數進行包裹,會改變這段代碼的含義,其中this,return,break和continue都會發生變化. IIFE並非一個普通的解決方案,它只適應在某些狀況下進行手動操做

匿名函數沒有name標識符,會致使?
  1. 調用棧更難追蹤
  2. 自我引用更難
  3. 代碼更難理解

this的詞法

箭頭函數的this

箭頭函數在涉及this綁定的行爲和普通函數的行爲徹底不一致. 它放棄了因此普通this綁定的規則,取而代之的是用當前的詞法做用域覆蓋了this的值(箭頭函數不止於少寫代碼)

② this和對象原型

一. 關於this

它的做用域

this在任何狀況下都不指向函數的詞法做用域. 在JavaScript內部,做用域確實和對象相似,可見的標識符都是它的屬性. 可是做用域"對象"沒法經過JavaScript代碼訪問,它存在JavaScript引擎內部

每當你想要把this和詞法做用域的查找混合使用時,必定要提醒本身,這是沒法實現的

this是什麼?

this是在運行時進行綁定的,並非在編寫時綁定的,它的上下文取決於函數調用時的各類條件. this的綁定和函數的聲明的位置沒有任何關係,只取決於函數的調用方法

this其實是在函數被調用時發生的綁定,它指向什麼徹底取決於函數在哪裏被調用

二. this的全面解析

調用位置

調用位置就是函數在代碼中被調用的位置

調用棧就是爲了到達當前執行位置所調用的因此函數

this綁定規則

1. 默認綁定

獨立函數調用

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
})()
複製代碼
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

複製代碼

解析:

參數傳遞其實就是一種隱式賦值,所以咱們傳入函數也會被隱式賦值

  • 回調函數丟失this綁定是很是常見的,接下來學習如何經過固定this來修復這個問題
3.顯式綁定

JavaScript提供的絕大多數函數以及咱們本身建立的全部函數均可以使用call(...)apply(...)方法

它們的第一個參數是一個對象,是給this準備的,接着在調用函數時將其綁定到this. 由於你能夠直接指定this的綁定對象,所以咱們稱之爲顯示綁定.

3.1 硬綁定

硬綁定是一種很是常見的模式,ES5提供了內置的方法Function.prototype.bind(...)

bind(...)會返回一個硬編碼的新函數,它會把你指定的參數設置爲this的上下文並調用原始函數

4.new綁定

包括內置函數和自定義函數在內的全部函數均可以用new來調用,這種函數調用被稱爲構造函數調用. 實際上並不存在所謂的"構造函數",只有對於函數的構造調用

使用new來調用函數,或者說發生構造函數調用時,會自動執行下面的操做

  1. 建立一個全新的對象
  2. 這個對象會被執行[[Prototype]]連接
  3. 這個新對象會綁定到函數調用的this
  4. 若是函數沒有返回其餘對象,那麼new表達式中的函數調用會自動返回這個新對象

判斷this(重要!!!)

按照下面的順序進行判斷(記住特例):

  1. 函數是否在new中調用(new 綁定)?若是是的話this綁定的是新建立的對象 var bar = new foo()
  2. 函數是否經過call或apply(顯示綁定)或者硬綁定調用? 若是是的話, this綁定的是指定的對象 var bar = foo.call(obj2)
  3. 函數是否在某個上下文對象中調用(隱式綁定)?若是是的話,this綁定的是那個上下文對象 var bar = obj.foo()
  4. 若是都不是的話,使用的是默認綁定. 若是在嚴格模式下,就綁定到undefined,不然綁定的全局對象 var bar = foo()

不過...凡是都有例外,接下來咱們介紹例外吧

this的綁定例外

  1. 被忽略的this

若是咱們把nullundefined做爲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
複製代碼
  1. 間接引用
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的綁定函數

  1. 由new調用? 綁定到新建立的對象 (new綁定)
  2. 由call或apply(或者bind)調用? 綁定到指定的對象 (顯示綁定)
  3. 由上下文調用? 綁定到那個上下文對象 (隱式綁定)
  4. 默認綁定: 在嚴格模式下綁定到undefined,不然會綁定到全局 (默認綁定)

注意: 有些調用可能在無心中使用默認綁定規則. 若是想'更安全'地忽略this綁定,你可使用一個DMZ對象,

好比 var Ø = Object.create(null),以保護全局對象

ES6中的箭頭函數並不會使用四條標準的綁定規則,而是根據當前的詞法做用域來決定this,具體說,箭頭函數會繼承外層函數調用的this綁定(不管this綁定到什麼). 這其實和ES6以前的var self = this的機制同樣

三. 對象

基本類型

  1. string
  2. number
  3. boolean
  4. null
  5. undefined
  6. object

注意:

null有時會被看成一種對象類型,可是這其實只是語言自己的一個bug,即對null執行typeof null時返回字符串"object". 實際上,null自己是基本類型

黑科技: 原理是這樣的,不一樣的對象在底層都表示爲二進制,在JavaScript中二進制前三位都爲0的話會被判斷爲object類型,null的二進制表示是全0,天然前三位也是0,因此typerof時會返回"object"

內置對象

  1. String
  2. Number
  3. Boolean
  4. Object
  5. Function
  6. Array
  7. Date
  8. RegExp
  9. Error

必要時語言會自動把字符串字面量轉換成一個String對象

在對象中,屬性名永遠都是字符串。若是使用string之外的其餘值做爲屬性名,它首先會被轉換爲一個字符串。

複製對象(待解決)

淺拷貝
深拷貝(JSON.stringify(obj)
屬性描述符
1. writable

是否能夠修改屬性的值

2.configurable

屬性是否可配置,把configurable修改爲false是單向操做,沒法撤銷

除了沒法修改,configurable:false還會禁止這個屬性

3.Enumerable

屬性是否會出如今對象的屬性枚舉中

4.不變性

全部的方法建立的都是淺不變性,它們只會影響目標對象和它的直接屬性.若是目標對象引用了其餘對象,其餘對象的內容不受影響,仍然是可變的

4.1 對象常量

結合writable:falseconfigurable: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,這樣就沒法修改它們的值

這個方法是能夠應用在對象上的級別最高的不可變性

5.[[Get]]

在語言規範中,obj.a在obj上其實是實現了[[Get]]操做. 對象默認的內置[[Get]]操做首先在對象上查找是否有名稱相同的屬性,若是找到就會返回這個屬性的值

若是不管如何都沒有找到名稱相同的屬性,那麼[[Get]]操做會返回值undefined

var myObj = {
    a: 2
}
myObj.b // undefined
複製代碼

這種方法和訪問變量是不同的. 若是你引用了一個當前詞法做用域中不存在的變量,並不會像對象屬性同樣返回undefined,而是會拋出一個ReferenceError異常

故,僅經過返回值,你沒法判斷一個屬性是存在而且持有一個undefined值,仍是變量不存在,因此[[Get]]沒法返回某個特定值而返回默認的undefined

6.[[Put]]

[[Put]]被觸發時,實際行爲分紅兩種:

  1. 已經存在這個屬性
    1. 屬性是不是訪問描述符? 若是是而且存在setter就調用setter
    2. 屬性的數據描述符中writable是不是false? 若是是,在非嚴格模式下靜默失敗,在嚴格模式下拋出TypeError異常
    3. 若是都不是,將該值設置爲屬性值
  1. 不存在這個屬性
7.Getter函數和Setter函數

在ES5中可使用getter和setter部分改寫默認操做,可是隻能應用在單個屬性上,沒法應用在整個屬性上.

getter和setter都會覆蓋單個屬性默認的[[Getter]]和[[Setter]]操做

8.判斷屬性的存在性

當咱們經過屬性名訪問某個值時可能返回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
複製代碼
9.遍歷

最好只在對象上應用for...in循環中

小結

JavaScript中的對象有字面形式(var a= {...})和構造形式(var a = new Array(...))

"萬物皆對象"的概念是錯誤的. 對象是6個或者7個(null)基礎類型之一. 對象有包括function在內的子類型,不一樣子類型具備不一樣的行爲,好比內部標籤[object Array]表示是對象的子類型數組

對象就是鍵值對的集合. 能夠經過.propName或者['propName']語法來獲取屬性值. 訪問屬性時,引擎實際上會調用內部的默認[[Get]]操做(在設置屬性值時是[[Put]]),[[Get]]操做檢查對象自己是否包含這個屬性,若是沒找到的話還會查找[[Prototype]]

屬性的特性能夠經過屬性描述符來控制,好比writableconfigurable. 還可使用Object.preventExtensions(...),Object.seal(...)Object.freeze(...)來設置對象的不可變性級別,其中Object.freeze(...)是應用在對象上不可變性的最高級別.

屬性不必定包含值-它們多是具有getter/setter的"訪問描述符". 此外屬性能夠是可枚舉或不可枚舉的,這決定了它們是否會出如今for...in循環中

可使用for...of遍歷數據結構(數組,對象等等)中的值,for...of會尋找內置或者定義的@@iterator對象並調用它的next()方法來遍歷數據值

四. 混合對象"類"

構造函數

類實例是由一個特殊的類方法構造的,這個方法名一般和類名相同,被稱爲構造函數. 這個方法的任務就是初始化實例須要的全部信息

類的繼承

多態(super關鍵字)

在傳統的面向對象的語言中super還有一個功能,就是從子類的構造函數中經過super能夠直接調用父類的構造函數.一般來講這沒什麼問題,由於對於真正的類來講,構造函數是屬於類的.

然而,在JavaScript中剛好相反-實際上類是屬於構造函數的. 因爲JavaScript中父類和子類的關係只存在於二者構造函數對應的.prototype對象中,所以它們的構造函數之間並不存在直接關係,從而沒法簡單地實現二者的相對引用.

小結

類是一種設計模式. 許多語言提供了對於面向對象類軟件設計的原生語法. JavaScript也有相似的語法,可是和其餘語言中的類徹底不同

類意味着複製

傳統的類實例化時,它的行爲會被複制到實例中. 類被繼承時,行爲也會複製到子類中

多態(在繼承鏈的不一樣層次名稱相同可是功能不一樣的函數)看起來彷佛是從子類引用父類,可是本質上引用的實際上是複製的結果

JavaScript不會(像類那樣)自動建立對象的副本, 只能複製引用,沒法複製被引用的對象或者函數自己

混入模式(利用for...in遍歷判斷對象不存在的屬性,不存在則添加)能夠用來模擬類的複製行爲,可是一般會產生醜陋而且脆弱的語法,好比顯式僞多態(Object.methidName.call(this,....)),這會讓代碼更難懂而且難以維護.

顯示混入實際上沒法徹底模擬類的複製行爲,由於對象(和函數,函數也是對象)只能複製引用,沒法複製被引用的對象或者函數自己.

總地來講,在JavaScript中模擬類是得不償失的,雖然能解決當前的問題,可是可能會埋下更多的隱患.

五. 原型

1.[[Prototype]]

JavaScript中的對象有一個特殊的[[prototype]]內置屬性,其實就是對於其餘對象的引用. 幾乎全部的對象在建立時[[Prototype]]屬性都會被賦予一個非空的值

var anotherObj = {
    a:2
}
// 建立一個關聯到 antherObj 的對象
var myObj = Object.create(anotherObj)
複製代碼
1.1 Object.prototype

全部普通的[[prototype]]鏈最終都會指向內置的Object.prototype

1.2屬性設置和屏蔽

在[第三部分對象中]提到過,給一個對象設置屬性並不只僅是添加一個新屬性或者修改已有的屬性值

myObj.foo = 'bar'
複製代碼
  • 若是myObj對象中包含名爲foo的普通數據訪問屬性,這條賦值語句只會修改已有的屬性值
  • 若是foo不是直接存在於myObj中,[[Prototype]]鏈就會被遍歷,相似[[Get]]操做. 若是原型鏈找不到foo,foo就會被直接添加到myObj
  • 若是foo存在原型鏈上層,賦值語句myObj.foo = 'bar'的行爲就會有些不一樣,下面是詳細的介紹
  • 若是屬性名foo既出如今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(...)並不會受到影響

2."類"

2.1 類函數

經過調用new Foo()建立的每一個對象將最終被[[Prototype]]連接到這個Foo.prototype對象

function Foo() {
    ...
}
var a = new Foo()
Object.getPrototypeOf(a) === Foo.prototype // true
複製代碼

在面向對象的語言中,類能夠被複制屢次,就像模具製做東西同樣.

可是在JavaScript中,並無相似的複製機制. 咱們不能建立一個類的多個實例,只能建立多個對象,它們的[[Prototype]]關聯的是同一個對象. 可是在默認狀況下並不會進行復制,所以這些對象之間並不徹底失去聯繫,它們是互相關聯的.

關於名稱

"原型繼承"嚴重影響了你們對JavaScript機制真實原理的理解

繼承意味着複製操做,JavaScript並不會複製對象屬性. 相反,JavaScript會在兩個對象之間建立一個關聯,這樣一個對象就能夠經過委託訪問到另外一個對象的屬性和函數

2.2 "構造函數"
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時,函數調用會變成"構造函數調用"

構造函數返回值的問題
  1. 沒有返回值的狀況像其餘傳統語言同樣,返回實例化的對象
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"}
複製代碼
  1. 若是存在返回值則檢查其返回值是否爲引用類型,若是爲非引用類型,如(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"}
複製代碼
  1. 若是存在返回值是引用類型,則實際返回該引用類型
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"}
複製代碼

3.(原型)繼承

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_ (是可設置屬性)

4.對象關聯
原型鏈的概念

若是在對象上沒有找到須要的屬性或者方法引用,引擎就會繼續在[[Prototype]]關聯的對象上進行查找. 若是後者中也沒有找到須要的引用就會繼續查找它的[[Prototype]],以此類推,這一系列的連接稱爲"原型鏈"

Object.create(...)

Object.create(...)會建立一個對象並把它關聯到咱們指定的對象

Object.create(null)會建立一個擁有空[[Prototype]]連接的對象,這個對象沒法進行委託. 因爲這個對象沒有原型鏈,因此instanceOf操做符沒法進行判斷

小結

若是要訪問對象中並不存在的一個屬性,[[Get]]操做就會查找對象內部[[Prototype]]關聯的對象. 這個關聯關係實際上定義一條"原型鏈",在查找屬性時就會對它進行遍歷

全部普通對象都有內置的Object.prototype,指向原型鏈的頂端,若是在原型鏈中找不到指定的屬性就會中止. toString(),valuOf()和其餘一些通用的功能都存在於Object.prototype對象上,所以語言中全部的對象均可以使用他沒

關聯兩個對象最經常使用的方法是使用new關鍵字進行函數調用,在調用的4個步驟中會建立一個關聯其餘對象的新對象

  1. 建立一個全新的對象
  2. 這個對象會被執行[[Prototype]]連接
  3. 這個新對象會綁定到函數調用的this
  4. 若是函數沒有返回其餘對象,那麼new表達式中的函數調用會自動返回這個新對象

使用new調用函數時會把新對象的.prototype屬性關聯到"其餘對象". 帶new的函數調用一般被稱爲"構造函數調用",儘管它們實際上和傳統面向類語言中的構造函數不同

雖然這些JavaScript機制和傳統面向對象的"類初始化"和"類繼承"很類似,可是JavaScript中的機制有一個核心區別,那就是不會進行復制,對象之間是經過內部的[[Prototype]]鏈關聯的

出於各類緣由,以"繼承"結尾的術語和其它面向對象的術語都沒法幫助你理解JavaScript的真實機制,相比之下,"委託"是一個更合適的術語,由於對象之間的關係不是複製而是委託

六.行爲委託

JavaScript中原型鏈這個機制的本質就行對象之間的關聯關係

Js中函數之因此能夠訪問call(...),apply(...),bind(...)是由於函數自己是對象

1.委託理念

行爲委託認爲對象之間是兄弟關係,互相委託,而不是父類和子類關係.JavaScript的[[Prototype]]機制本質上就是行爲委託節制. 也就是說,咱們能夠選擇在Js中努力實現類機制,也能夠擁抱更天然的[[Prototype]]委託機制

2.類與對象

ES6的class語法糖

見下章

七. ES6中的class

傳統面向類的語言中父類和子類,子類和實例之間實際上是複製操做,可是在[[Prototype]]中沒有複製,相反,它們之間只有委託關聯

1.class

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還解決了什麼問題?

  1. 再也不引用雜亂的.prototype了
  2. Button聲明直接"繼承"了Widget,再也不須要經過Object.create(...)來替換.prototype對象,也不須要設置._proto_或者Object.setPrototypeOf(...)
  3. 能夠經過super(...)來實現相對多態,這樣任何方法均可以引用原型鏈上層的同名方法. 構造函數不屬於類,因此沒法相互引用---super()能夠完美解決構造函數的 問題
  4. class字面語法不能聲明屬性.看起來這是一種限制,可是它會排除掉許多很差的狀況,若是沒有這種限制的話,原型鏈末端的"實例"可能會意外地獲取其餘地方的屬性
  5. 能夠經過extends很天然地擴展對象類型,甚至是內置的對象類型,好比Array或RegExp

2.class陷阱

class基本上只是現有[[Prototype]]機制(委託)的一種語法糖

也就是說class並不會像傳統面向類的語言同樣在聲明時靜態複製全部行爲.若是修改或者替換了父"類"中的一個方法,那麼子"類"和全部實例都會受到影響,由於它們在定義時並無進行復制,只是使用基於[[Prototype]]的實時委託

class語法沒法定義類成員屬性(只能定義方法)


原文地址: 傳送門

Github: 歡迎Startwq93

相關文章
相關標籤/搜索