作者:*5102
轉發鏈接:
https://juejin.im/post/5e9f0bdce51d4546f5791989
前言
本篇文章屬於知識總結型,歸納出許多比較零散的知識點,都是乾貨噢~
如果你是小白那麼這篇文章正好適合你,如果你是老手那麼不妨鞏固一下看看還有哪些邊角料沒補!
建議:適合有js基礎的小夥伴觀看,篇幅較長,建議先收藏再慢慢瀏覽
整整花了一週時間總結了一些比較重點也有些比較偏的知識,希望各位小夥伴慢慢品嚐,如果有不對的地方或者是需要優化的地方望請告知,儘量給大家呈現最有價值的文章。個人水平有限,還請各位大佬指點迷津。希望各位看了這篇文章能有自己的想法,在前端道路上還很漫長,與我一同探索吧!
2.6萬字JS乾貨分享總共分二部分知識點總結,分別為【基礎篇】、【實踐篇】,希望小夥們按照順序一次閱讀完。
一、變量類型
==與===
對於==的判斷
- 並不是那麼嚴謹的判斷左右兩端是否相等
- 它會優先對比數據的類型是否一致
- 不一致則進行隱式轉換,一致則判斷值的大小,得出結果
- 繼續判斷兩個類型是否為null與undefined,如果是則返回true
- 接著判斷是否為string與number,如果是把string轉換為number再對比大小
- 判斷其中一方是否為boolean,如果是就轉為number再進一步判斷
- 判斷一方是否為object,另一方為string、number、symbol,如果是則把object轉為原始類型再判斷
比較情況
- 數組 == 值,(值類型指的是原始類型)會先轉成數值再比較,與字符串比較會先轉成字符串再比較
- 引用 == 值,會把引用類型轉成原始類型再比較
- 值 == 值,直接比較類型再比較值的大小
- 字符串 == 數字,則把字符串轉為數值再比較
- 其他類型 == boolean,則把boolean轉成數值再進一步比較
- undefined == null,也會發生隱式轉換,且2者可以相互轉換,即2者相等,與自身也相等
- 對象 == 非對象,如果非對象為string或number,則返回ToPrimitive(對象) == 非對象,的結果;ToPrimitive方法的參數如果是原始類型則直接返回;如果是對象,則調用valueOf方法,如果是原始值再進行原始類型轉換和大小對比;如果不是原始值則調用toString,且結果為原始值則進行原始類型比較,如果不是原始值則拋出錯誤
<code>//
以下結果都為true
console
.log([5
]==5
,['5'
]==5
)console
.log({name:'5'
}=='[object Object]'
)console
.log('5'
==5
,true
==1
,false
==0
)console
.log(undefined
==null
)console
.log([5
,6
]=='5,6'
,['5'
,'6'
]=='5,6'
) 複製代碼/<code>
大白話:優先比較類型,同類型,比大小,非原始,調ToPrimitive,為對象調valueOf,還非原始調toString,最後還非原始則報錯,如果為原始則進行類型對比,如果不同類型再轉換,之後對比大小。
所謂==比較就是要轉換成同類型比較,如果無法轉成同類型就報錯
優先比類型,再比null與undefined,再比string和number,再比boolean與any,再比object與string、number、symbol;以上如果轉為原始類型比較,則進行類型轉換,直到類型相同再比較值的大小。這就是==的隱式轉換對比,比較繞,給個圖就清晰了!
如下為判斷步驟
思考?如何判斷此表達式(注意==!與!==) []==![]
- 基於運算符的優先級此式會先運算![]的結果
- !優先於==,且[]為真值(轉成boolean,結果為true的就為真值,包括{};轉成false的就為假值),![]結果為false,所以當前表達式轉化為 []==false
- 通過之前總結的轉換關係,任何類型與boolean類型比較,所以[]==false 轉化為 []==0 比較
- 此時變為object與0比較,調用object的轉換成原始類型的方法valueOf其結果還是valueOf
- 再調用toString結果為'',再進行string轉成number,則[]轉成數字類型0
- 表達式進一步轉換成0==0,結果為true。
雖然過程複雜,記住判斷的思路即可,非對象之間,先類型轉換再比大小,對象比較則調用獲取原始值方法再進一步比較。
如下為toString與valueOf轉換
對於===的判斷
- ===屬於嚴格判斷,直接判斷兩者類型是否相同,不同則返回false
- 如果相同再比較大小,不會進行任何隱式轉換
- 對於引用類型來說,比較的都是引用內存地址,所以===這種方式的比較,除非兩者存儲的內存地址相同才相等,反之false
<code>const
a=[]const
b=a a===b ---------------const
a=[]const
b=[] a===b 複製代碼/<code>
7大原始類型與Object類型
- Boolean
- Null
- Undefined
- Number
- BigInt
- String
- Symbol
- Object
類型判斷
原始類型判斷
- 原始類型string、number、undefined、boolean、symbol、bigint都能 通過typeof(返回字符串形式)直接判斷類型,還有對象類型function也可判斷
- 除了null無法通過typeof(為object)直接判斷類型(歷史遺留),包括對象類型,typeof把null當作對象類型處理,所以typeof無法判斷對象類型,typeof也能判斷function
非原始類型判斷(以及null)
判斷數組
- 使用Array.isArray()判斷數組
- 使用[] instanceof Array判斷是否在Array的原型鏈上,即可判斷是否為數組
- [].constructor === Array通過其構造函數判斷是否為數組
- 也可使用Object.prototype.toString.call([])判斷值是否為'[object Array]'來判斷數組
判斷對象
- Object.prototype.toString.call({})結果為'[object Object]'則為對象
- {} instanceof Object判斷是否在Object的原型鏈上,即可判斷是否為對象
- {}.constructor === Object通過其構造函數判斷是否為對象
判斷函數
- 使用func typeof function判斷func是否為函數
- 使用func instanceof Function判斷func是否為函數
- 通過func.constructor === Function判斷是否為函數
- 也可使用Object.prototype.toString.call(func)判斷值是否為'[object Function]'來判斷func
判斷null
- 最簡單的是通過null===null來判斷是否為null
- (!a && typeof (a) != 'undefined' && a != 0)判斷a是否為null
- Object.prototype.__proto__===a判斷a是否為原始對象原型的原型即null
判斷是否為NaN
- isNaN(any)直接調用此方法判斷是否為非數值
一些其他判斷
- Object.is(a,b)判斷a與b是否完全相等,與===基本相同,不同點在於Object.is判斷+0不等於-0,NaN等於自身
- 一些其他對象類型可以基於原型鏈判斷和構造函數判斷
- prototypeObj.isPrototypeOf(object)判斷object的原型是否為prototypeObj,不同於instanceof,此方法直接判斷原型,而非instanceof 判斷的是右邊的原型鏈
一個簡單的類型驗證函數
<code>function isWho(x) {if
(x ===null
)return
'null'
const
primitive = ['number'
,'string'
,'undefined'
,'symbol'
,'bigint'
,'boolean'
,'function'
] let type = typeof xif
(primitive.includes(type))return
typeif
(Array.isArray(x))return
'array'
if
(Object.prototype.toString.call(x) ==='[object Object]'
)return
'object'
if
(x.hasOwnProperty('constructor'
))return
x.constructor
.nameconst
proto = Object.getPrototypeOf(x)if
(proto)return
proto.constructor
.namereturn
"can't get this type"
} 複製代碼/<code>
二、深拷貝與淺拷貝
在項目中有許多地方需要數據克隆,特別是引用類型對象,我們無法使用普通的賦值方式克隆,雖然我們一般使用第三方庫如lodash來實現深拷貝,但是我們也需要知道一些其中的原理
淺拷貝
- Object.assign({},obj)淺拷貝object
- obj1={...obj2}通過spread展開運算符淺拷貝obj2
- Object.fromEntries(Object.entries(obj))通過生成迭代器再通過迭代器生成對象
- Object.create({},Object.getOwnPropertyDescriptors(obj))淺拷貝obj
- Object.defineProperties({},Object.getOwnPropertyDescriptors(obj))淺拷貝obj
簡單實現淺拷貝
<code>for
(const
keyin
a) { b[key] = a[key] } ------------------------------------------for
(const
keyof
Object
.keys(a)) { b[key] = a[key] } 複製代碼/<code>
淺拷貝只拷貝一層屬性對於引用類型無法拷貝
深拷貝
- JSON.parse(JSON.stringify(obj))通過JSON的2次轉換深拷貝obj,不過無法拷貝undefined與symbol屬性,無法拷貝循環引用對象
- 自己實現深拷貝
簡單深拷貝
<code>function
simpleDeepClone
(a
) {const
b=Array
.isArray(a) ? [] : {}for
(const
key ofObject
.keys(a)) {const
type
=typeof
a[key]if
(type
!=='object'
|| a[key] ===null
) { b[key] = a[key] }else
{ b[key] = simpleDeepClone(a[key]) } }return
b }function
deepClone
(a, weakMap =
new
WeakMap()) {if
(typeof
a !=='object'
|| a ===null
)return
aif
(s = weakMap.get(a))return
sconst
b =Array
.isArray(a) ? [] : {} weakMap.set(a, b)for
(const
key ofObject
.keys(a)) b[key] = clone(a[key], weakMap)return
b }function
JSdeepClone
(data
) {if
(!data || !(datainstanceof
Object
) || (typeof
data =="function"
)) {return
data ||undefined
; }const
constructor
= data.constructor
; const result = newconstructor
(); for (const
keyin
data) {if
(data.hasOwnProperty(key)) { result[key] = deepClone(data[key]); } }return
result; } 複製代碼/<code>
比較完善的深拷貝
<code>function
deepClonePlus
(a, weakMap = new WeakMap(
)) {const
type =typeof
aif
(a ===
null
|| type !=='object'
) return aif
(s = weakMap.
get
(a)) return sconst
allKeys = Reflect.ownKeys(a)const
newObj = Array.isArray(a) ? [] : {} weakMap.set
(a, newObj)for
(const
key of allKeys) {const
value
= a[key]const
T =typeof
value
if
(value
===null
|| T !=='object'
) { newObj[key] =value
continue
}const
objT = Object.prototype.toString.call(value
)if
(objT ==='[object Object]'
|| objT ==='[object Array]'
) { newObj[key] = deepClonePlus(value
, weakMap)continue
}if
(objT ==='[object Set]'
|| objT ==='[object Map]'
) {if
(objT ==='[object Set]'
) { newObj[key] =new
Set()value
.forEach(v => newObj[key].add
(deepClonePlus(v, weakMap))) }else
{ newObj[key] =new
Map()value
.forEach((v, i) => newObj[key].set
(i, deepClonePlus(v, weakMap))) }continue
}if
(objT ==='[object Symbol]'
) { newObj[key] = Object(Symbol.prototype.valueOf.call(value
))continue
} newObj[key] =new
a[key].constructor(value
) }return
newObj } 複製代碼/<code>
刨析深拷貝(個人思路)
- 本人使用遞歸算法來實習深拷貝,由於使用遞歸,會讓代碼看起來更加易懂,在不觸及調用棧溢出的情況下,推薦使用遞歸
- 深拷貝,其實考驗的就是如何把引用類型給拷貝過來,還有Symbol類型比較特殊,如何實現一個比較完整的深拷貝就要涉及不同類型的拷貝方式
- 首先考慮簡單的原始類型,由於原始類型在內存中保存的是值可以直接通過值的賦值操作,先判斷傳入參數是否為原始類型,包括null這裡歸為原始類型來判斷,沒必要進入對象環節,函數直接賦值不影響使用
- 經過原始類型的篩選,剩下對象類型,取出所有對象的鍵,通過Reflect.OwnKeys(obj)取出對象自身所有的鍵,包括Symbol的鍵也能取出
- 由於對象有2種體現形式,數組和普通對象,對於這2者要單獨判斷,先生成一個拷貝容器即newObj
- 接下來就可以開始遍歷 步驟2 中獲取到對象所有的鍵(僅自身包含的鍵),通過for..of 遍歷,取出當前要拷貝的對象a,對應於當前遍歷鍵的值,即a[key]
- 對a[key]值的類型進行判斷,此值類型的可能性包括所有的類型,所以又回到步驟1中先判斷原始類型數據;如果是原始類型可以直接賦值跳過這一輪,進行下一輪遍歷
- 經過上一步的篩選,此時剩下的只是對象類型,由於對象類型無法通過typeof直接區分,所以可以借用原始對象原型方法 Object.prototype.toString.call(obj) 來進行對象具體類型的判斷
- toString判斷的結果會以'[object xxx]',xxx為對應對象類型形式體現,基於這種轉換可以清晰判斷對象的具體類型,之後再對各種類型進行相應的深拷貝即可
- 以上並未使用遞歸,由於上述的拷貝,還未涉及多層次的嵌套關係並不需要使用遞歸
- 接下來將要判斷嵌套類型數據,(此順序可變,不過出現頻率高的儘量放在前頭)首先判斷普通對象和數組,如果是,則直接扔給遞歸處理,由於處理數組和普通對象的邏輯已經在這之前處理好了,現在只需重複上面的步驟,所以直接遞歸調用就好,遞歸到最後一層,應該是原始類型的數據,不會進入無限調用
- 接下來是判斷2種特殊類型Set和Map,由於這2種類型的拷貝方式不同,進一步通過if分支對其判斷,遍歷裡邊所存放的值,Set使用add方法向新的拷貝容器添加與拷貝對象相同的值,此處值的拷貝也應該使用深拷貝,即直接把值丟給遞歸函數,它就會返回一個拷貝好的值。Map類似,調用set方法設置鍵和值,不過正好Map的鍵可以存放各種類型
- 到了拷貝Symbol環節,這個類型相對特殊一點,Symbol的值是唯一的,所以要獲取原Symbol所對應的Symbol值,則必須通過借用Symbol的原型方法來指明要獲取Symbol所對應Symbol的原始值,基於原始值創建一個包裝器對象,則這個對象的值與原來相同
- 篩選到這裡,剩餘的對象,基本上就是一些內置對象或者是不需要遞歸遍歷屬性的對象,那麼就可以基於這些對象原型的構造函數來實例化相應的對象
- 最後遍歷完所有的屬性就可以返回這個拷貝後的新容器對象,作為拷貝對象的替代
- 基於循環引用對象的解析,由於循環引用對象會造成循環遞歸導致調用棧溢出,所以要考慮到一個對象不能被多次拷貝。基於這個條件可以使用Map對象來保存一個拷貝對應的表,因為Map的鍵的特殊效果可以保存對象,因此正好適用於對拷貝對象的記錄,且值則是對應的新拷貝容器,當下次遞歸進來的時候先在拷貝表裡查詢這個鍵是否存在,如果存在說明已經拷貝過,則直接返回之前拷貝的結果,反之繼續
- 由於Map存放的鍵屬於強引用類型,且深拷貝的數據量也不小,如果這些拷貝後的拷貝表不及時釋放可能會造成垃圾堆積影響性能,因此需要使用到weakMap方法代替Map,weakMap存放的鍵為弱引用類型,且鍵必須為對象類型,正好之前的newObj就是對象類型可以存放,使用弱引用的好處,可以優化垃圾回收,weakMap存放的是拷貝表,此拷貝表在拷貝完成之後就沒有作用了,之前存放的拷貝對象,經過深拷貝給新拷貝容器,則這些舊對象在銷燬之後,對應於拷貝表裡的對象也應該隨之清除,不應該還保留,這就是使用弱引用來保存表的原因。
以上就是本人在實現過程中的思路,可能講的比較囉嗦,但是我還是希望使用通俗的話讓各位明白,表達能力有限,望諒解。
接下來讓我們看看WeakMap的好處
<code>let
obj = {name
: {age
: [{who
:'me'
}] } }let
wm =new
WeakMap
() deepClonePlus(obj, wm) obj=null
console
.dir(wm) 複製代碼/<code>
從上面可以看出如果原拷貝對象被清空那麼WeakMap保存的拷貝表也將被清空,總的來說方便一點,總比麻煩一點好
看看這種情況
<code>const
obj = {name
: {age
: [{who
:'me'
}] } }let
wm =new
WeakMap
()console
.time('start'
)for
(let
i =0
; i1000000
; i++) { deepClonePlus(obj, wm) wm =new
WeakMap
() }console
.timeEnd('start'
) ------------------------------------------------let
wm =new
WeakMap
()let
mconsole
.time('start'
)for
(let
i =0
; i1000000
; i++) { deepClonePlus(obj, wm) m =new
WeakMap
() }console
.timeEnd('start'
) 複製代碼/<code>
從以上對比可以看出如果是多次拷貝同一對象,最好使用WeakMap來存儲拷貝表,那麼之後的每次拷貝只需從拷貝表中取出值即可,由於是淺拷貝所以時間較短(
注意:不過這種直接從WeakMap中取出的值屬於淺拷貝,使用同一個wm對象拷貝出來的都是淺拷貝,如果每個都需要深拷貝那麼只能每次重新創建WeakMap)三、原型與原型鏈
原型
- 只有對象類型才有原型概念
- 普通對象(即使用對象字面量或者Object構造器創建的對象)的原型為__proto__屬性,此屬性其實是個訪問器屬性,並不是真實存在的屬性,或者可以使用es6的Reflect.getPrototypeOf(obj)和Object.getPrototypeOf(obj)方法獲取對象的原型,其關係Reflect.getPrototypeOf({}) === Object.getPrototypeOf({}) === {}.__proto__
- 普通函數有2個屬性,一個是是__proto__(與普通對象類似),還有一個是函數專有的prototype屬性,因為函數有雙重身份,即可以是實例也可以是構造器,所以關係比較特殊
- 不是所有的對象都會有原型,比如對象原型Object.prototype的原型Object.prototype.__proto__就指向null,字典對象的原型也為null(把對象的__proto__設置為null,或者使用Object.create(null)創建一個沒有原型的字典對象,但是這個對象還是屬於對象類型),所以原始對象原型(Object.prototype)就是最原始的原型,其他對象類型都要繼承自它。
- 箭頭函數雖然屬於函數,由Function產生,但是沒有prototype屬性沒有構造器特性,所以也就沒有所謂的constructor,就不能作為構造器使用
原型鏈
這裡會詳細介紹原型、原型鏈、實例、構造器的關係 先看最原始的關係
由如上關係可以驗證console.log(
Function.prototype.__proto__.constructor.__proto__.constructor === Function) //true
- 所有函數都是由Function函數構造器實例化而來
- 所有實例的原型都指向構造它的構造器的prototype
- 每個構造器自身特有的方法就是靜態方法,原型上的方法可供所有繼承它或間接繼承它的實例使用
- 構造器也是函數,也是被Function實例化出來的,所以構造器的__proto__就是Function,但是構造器的prototype屬性指向的原型,是此構造器實例化出來的實例所指向的原型;簡單說構造器的prototype就是作為它的實例的原型
看看函數的原型鏈
- 在js中函數有多重身份,函數可以作為類就是構造器使用,定義靜態方法,作為普通函數調用,
- 只有由原始函數構造器(Function)實例化的函數才擁有直接使用函數原型(Function.prototype)上面的內置方法,創建函數只能通過原始函數構造器生成,
- 普通函數作為構造器使用(new)時相當於類(class)使用,類的prototype就是實例的原型,我們可以給原型添加屬性,給類添加屬性時就相當於給構造器添加靜態屬性
- 普通函數在創建實例的時候,會生成一個實例的原型,此原型指向Object.prototype即原始對象原型,也就是繼承對象原型,這麼一來實例也繼承了對象的原型,則實例也屬於對象類型
四、繼承與實現
繼承
- 所謂繼承一般說的是原型繼承,一個原型上面定義的方法一般都是基於其實例的用途來定義的,也就是說,原型的方法應該是實例經常用到的通用方法,而構造器方法一般是特定情況下可能會用到的方法,可按需調用,原型方法只能供其實例來使用
- 繼承可以讓原型鏈豐富,根據需求定製不同的原型鏈,不會存在內存浪費的情況,原型只會保留一份,用到的時候調用就行,還能節省空間
- 可以看出原型一般是一些共有的特性,實例是特有的特性,繼承的越多越具體,原型鏈的最頂端是最抽象的,越底端越具體,這樣一來我們可以根據需求在恰當位置繼承來實現個性化的定製屬性,統一而又有多樣化
繼承的實現
- 通過es6的extends關鍵字來繼承原型
- 手動實現原型繼承
<code>function
foo
(v
) {this
.v = v }function
boo
(v
) {this
.vv = v } boo.prototype.__proto__ = foo.prototypeconst
b =new
boo(3
) ------------------------------------------------function
foo
(v
) {this
.v = v }
function
boo
(v
) {this
.vv = v } boo.prototype =Object
.create(foo.prototype, {constructor
: {value
: boo,enumerable
:false
,writable
:true
,configurable
:true
} })const
b =new
boo(3
) -----------------------------------------------function
foo
(v
) {this
.v = v }function
boo
(v
) {this
.vv = v }function
o
() {} o.prototype = foo.prototype boo.prototype =new
o() boo.prototype.constructor = booconst
b =new
boo(3
) ------------------------------------------------ 如果實現類似extends的繼承還需加上 boo.__proto__ = foo 複製代碼/<code>
實現構造器原型的繼承,無非就是父構造器原型賦值給子構造器原型的原型,還有需要保證子構造器原型不能含有父構造器的屬性
五、實現class與extends
實現class
- es6加入的class其實是為了開發者方便創建類,與其他語言在寫法上儘量一致,但是js原生並沒有類這個東西,為了實現類的效果,可以通過js的構造器來實現,class使用new關鍵字生成實例,構造器也是通過new來實例化,那麼可以推斷class本質也是個構造器
- 手動實現class
<code>const
Class = (function
() {function
Constructor
(name
) {this
.name = name } Constructor.prototype.getName =function
name
(name
) {console
.log('原型方法getName:'
+this
.name); } Constructor.prototype.age ='原型屬性age'
Constructor.log =function
log
() {console
.log('我是構造器的靜態方法log'
); } Constructor.isWho ='構造器靜態屬性isWho'
return
Constructor })()const
i =new
Class('我是實例'
) 複製代碼/<code>
實現class語法糖,只需封裝一層函數。
- 返回的Constructor就是實例的構造器,其prototype是個空白的對象這是由於Function造成的
- new後面調用的函數必須是一個構造器函數,用於構造實例,此構造器的this指向實例
- 構造器內部需要實現依照傳入的參數設置實例的屬性
- 定義Class時需要實現原型屬性和靜態屬性的掛載
以上只實現class的定義,接下來要實現能夠兼容繼承的寫法
實現extends
- 繼承需要滿足原型的繼承
- 還需要滿足可調用父類構造器
<code>const
Parent = (function
() {function
Constructor
(age
) {this
.age = age } Constructor.prototype.getName =function
() {console
.log(this
.name); }return
Constructor })()const
Class = (function
(_Parent = null
) {if
(_Parent) { Constructor.prototype =Object
.create(_Parent.prototype, {constructor
: {value
: Constructor,enumerable
:false
,writable
:true
,configurable
:true
} }) Constructor.__proto__ = _Parent }function
Constructor
(name, age
) { _Parent ? _Parent.call(this
, age) :this
this
.name = name } Constructor.prototype.getAge =function
() {console
.log(this
.age); }return
Constructor })(Parent) 複製代碼/<code>
- 實現原型繼承,可以使用之前的繼承寫法,注意class形式的繼承,會把父類設為子類的__proto__
- 在構造函數內判斷是否有父類,如果有就要調用父類的構造函數,把當前的this傳入,這樣才能生成父類構造器中定義的屬性,這才算是真正的繼承。繼承不單繼承原型還能實現繼承父類構造器中定義的屬性
- 對於原型方法和靜態方法也是類似定義,注意定義的方法如果用到this需要使用function關鍵字定義函數,不可使用匿名函數,否則this無法指向調用對象本身
六、作用域、執行上下文與閉包
作用域與作用域鏈
作用域
- 所有未定義的變量直接賦值會自動聲明為全局作用域的變量(隱式全局變量可以用delete刪除,var定義的則不行)
<code>a=1
var
b=2
console
.log(a,b)delete
adelete
bconsole
.log(b,a) 複製代碼/<code>
- window對象的所有屬性擁有全局作用域
- 內層作用域可以訪問外層作用域,反之不行
- var聲明的變量,在除了函數作用域之外,在其他塊語句中不會創建獨立作用域
- let和const聲明的變量存在塊語句作用域,且不會變量提升
- 同作用域下不能重複使用let、const聲明同名變量,var可以,後者覆蓋前者
- for循環的條件語句的作用域與其循環體的作用域不同,條件語句塊屬於循環體的父級作用域
<code>for
(let
i =0
; i5
; i++) {let
i =5
} --------------------------------------------for
(let
i =0
; i5
; i=x) {let
x =5
} 複製代碼/<code>
作用域鏈
- 作用域鏈也就是所謂的變量查找的範圍
- 在當前作用域引用變量時,如果沒有此變量,則會一路往父級作用域查找此變量,直到全局作用域,如果都沒有,在非嚴格情況下會自動聲明,所以是undefined,在嚴格條件下則會報錯
- 變量的查找路徑依據的是在創建這個作用域的地方向上查找,並非是在執行時的作用域,如下 b變量的值為2。可以看出當執行到需要b變量時,當前作用域下並沒有b,所以要到定義這個b變量的靜態作用域中尋找,即創建時候的作用域鏈上查找b的值
<code>b =1
function
a
() {const
b =2
function
s
() {
console
.log(b); }return
s }const
s = a()var
b =3
s() 複製代碼/<code>
- 作用域在腳本解析階段就已經規定好了,所以與執行階段無關,且無法改變
執行上下文
- 執行上下文在運行時確定,隨時可能改變
- 調用棧中存放多個執行上下文,按照後進先出的規則進行創建和銷燬,最底部的執行上下文,也就是棧低的執行上下文為全局上下文,最早被壓入棧中,其上下文中的this指向window,嚴格模式下為undefined
- 創建執行上下文時,會綁定當前this,確定詞法環境,存儲當前環境下函數聲明內容,變量let與const綁定但未關聯任何值,確認變量環境時,綁定var的初始值為undefined
- 在var聲明之前,調用var聲明的變量時值為undefined,因為創建了執行上下文,var聲明的變量已經綁定初始undefined,而在let和const聲明之前調用其聲明的變量時,由於只綁定在了執行上下文中,但並未初始任何值,所以在聲明之前調用則會拋出引用錯誤(即TDZ暫時性死區),這也就是函數聲明與var聲明在執行上下文中的提升
這裡瞭解一下函數、變量提升
<code>console
.dir(foo)function
foo
() {}var
foo =5
------------------------------var
foo =5
function
foo
() {}console
.dir(foo) 複製代碼/<code>
從以上代碼結果可以得出結論:
- 上面代碼塊能夠體現,在解析階段會將函數與變量提升,且函數的優先級比var聲明的變量高,因為打印的是函數聲明,如果var聲明的優先級高,那麼應該是undefined
- 從下面的代碼塊中可以看出foo在代碼執行的時候被賦值為5,而函數聲明在解析階段已經結束,在執行階段沒有效果
- 還有一點 個人認為在解析階段,函數聲明與變量聲明提升之後在代碼塊中的位置順序沒什麼關係
閉包
- 所謂閉包就是函數與其詞法環境(創建當前作用時的任何局部變量)的引用。閉包可以使內部函數訪問到外部函數的作用域,當函數被創建時即生成閉包
<code>function
fn1
() {var
name ='hi'
;function
fn2
() {console
.log(name); }return
fn2 } fn1()() 複製代碼/<code>
- 當你從函數內部返回一個內部函數時,返回的函數將會保留當前閉包,即當前詞法環境
- 閉包只會保留環境中任何變量的最後一個值,這是因為閉包所保存的是整個變量的對象
- 閉包的作用域鏈包含著它自己的作用域,以及包含它父級函數的作用域和全局作用域
- 當返回一個閉包時,保留此閉包下的所有被外部引用的對象
- 閉包之間是獨立的,在閉包環境下可以創建多個不同的閉包環境暴露給外部,從而實現不同的效果
<code>function
makeAdder
(x
) {return
function
(y
) {return
x + y; }; }var
add5 = makeAdder(5
);var
add10 = makeAdder(10
);console
.log(add5(2
));console
.log(add10(2
)); 複製代碼/<code>
- 暴露閉包的方式不止返回內部函數一種,還可以使用回調函數產生閉包環境,或者把內部函數賦值給其他外部對象使用
- 閉包在沒有被外部使用的情況下,隨執行結束銷燬,如何產生閉包並且保留閉包環境的關鍵就在於不讓其環境被垃圾回收系統自動清除,那麼就要使內部環境中的引用被外部保留,這樣才能保留閉包
- 閉包雖然方便我們操作和保留內部環境,但是閉包在處理速度和內存消耗方面對腳本性能具有負面影響,除非在特定的情況下使用
這裡看個有趣的東西
<code>function
foo
(){let
a={name
:'me'
}let
b={who
:'isMe'
}let
wm=new
WeakMap
()function
bar
(){console
.log(a) wm.set(b,1
)return
wm }return
bar }const
wm=foo()()console
.dir(wm) -------------------------------------------function
foo
(){let
a={name
:'me'
}let
wm=new
WeakMap
()function
bar
(){console
.log(a) wm.set(a,1
)return
wm }return
bar }const
wm=foo()()console
.dir(wm) 複製代碼/<code>
- 從上塊代碼中可以看出,bar被return到外部環境,所以其內部形成閉包,bar中使用到的變量(a,wm)都會被保留下來,但是最後打印wm的時候為空?這是因為外部並沒有引用到b對象,只是通過wm弱引用保存b的值,從wm為空可以看出,閉包內部的b被清除,所以wm也自動清除b的弱引用,可以論證之前所說,閉包只保留外部用到的變量
- 從下塊代碼能直接看出a就是閉包中的a,bar在外部執行時需要用到a與wm所以保留了下來
- 有人可能會不解,為什麼上塊代碼中的b也被wm.set(b,1)引用,但是最終就沒有呢,那是因為WeakMap中保留的是b的弱引用,可以理解為,wm中的b是依賴原函數中的b而存在,當wm被return時,閉包中的b,沒有被任何外部所依賴,而是別人依賴它。可以這麼理解 b牽著別人走,因為b沒有被外面人牽著走,所以b這個鏈子就被斷開,也影響到b牽的人一塊丟了
七、this
先看一張圖
- this的綁定在創建執行上下文時確定
- 大多數情況函數調用的方式決定this的值,this在執行時無法賦值
- this的值為當前執行的環境對象,非嚴格下總是指向一個對象,嚴格下可以是任意值
- 全局環境下this始終指向window,嚴格模式下函數的調用沒有明確調用對象的情況下,函數內部this指向undefined,非嚴格下指向window
- 箭頭函數的this永遠指向創建當前詞法環境時的this
- 作為構造函數時,函數中的this指向實例對象
- this的綁定只受最靠近調用它的成員的引用
- 執行上下文在被執行的時候才會創建,創建執行上下文時才會綁定this,所以this的指向永遠是在執行時確定
<code>function
foo
( ){console
.dir(this
) } foo() -----------------------------------------------function
foo
(){console
.dir(this
) } foo.call(5
) 複製代碼/<code>
嚴格與非嚴格模式下的this指向是不同的,非嚴格總是指向一個對象,嚴格模式可以為任意值
執行前
執行後
以上2圖可以使用chrome開發工具來進行查看程序執行時的相關數據,可以看到嚴格模式下簡單調用的函數內部的this指向undefined
普通函數中的this
直接調用
在沒有明確調用者情況下函數內部this指向window,嚴格模式下都為undefined,除非綁定函數的this指向,才會改變this
<code>function
foo
() {console
.dir(this
)function
boo
(){console
.dir(this
) } boo() } ----------------------------------------------const
obj = {foo
:function
foo
() {console
.dir(this
)function
boo
() {console
.dir(this
) }return
boo } }const
foo = obj.foo foo()() ----------------------------------------------const
obj = {foo
:function
foo
() {console
.dir(this
)function
boo
() {console
.dir(this
) }return
boo } }const
foo = obj.foo() foo() ----------------------------------------------function
foo
(func
) {console
.dir(this
) func() } foo(function
() {console
.dir(this
) }) 複製代碼/<code>
基於調用者以及不同調用方式
函數調用也就是在函數名後面加個(),表示調用,如果函數名前沒有加任何東西,那麼默認為簡單調用,在嚴格與非嚴格環境下,簡單調用的函數內部this指向undefined與window,但是全局環境下的this永遠為window
基於對象
當函數作為對象的方法調用時,不受函數定義方式或者位置影響
<code>const
obj = {foo
:function
() {console
.dir(this
)function
boo
() {console
.dir(this
) } boo()return
boo } }const
obj1 = {} obj1.boo = obj.foo obj1.boo() ----------------------------------------------const
obj = {foo
:function
() {console
.dir(this
)function
boo
() {console
.dir(this
) } boo()return
boo } }const
obj1 = {} obj1.boo = obj.foo() obj1.boo() ----------------------------------------------const
obj = {name
:'obj'
,obj1
: {name
:'obj1'
,foo
:function
() {console
.dir(this
.name) } } } obj.obj1.foo() 複製代碼/<code>
基於new關鍵字
<code>function
foo
() {console
.dir(this
)console
.log(this
instanceof
foo)console
.log(foo.prototype.isPrototypeOf(this
)) that =this
}var
thatconst
f =new
foo()console
.log(that === f) ----------------------------------------------function
foo
() {console
.dir(this
)function
boo
() {console
.dir(this
) } boo() }const
f =new
foo() 複製代碼/<code>
基於定時器與微任務
微任務中的簡單調用的函數this指向window嚴格下指向undefined,而
定時器中的回調函數不管在嚴格還是非嚴格環境下this永遠指向window,說明一點,調用window對象的方法時this指向window也就是全局對象,換句話說,簡單調用的函數如果屬於window本身自帶的方法那麼這個方法的this指向window<code>const
id = setInterval(function
() {console
.dir(this
) setTimeout(()
=> {console
.dir(this
) clearInterval(id) }); }) ----------------------------------------------new
Promise
(function
(resolve, reject
) {console
.dir(this
) resolve() }).then(function
(res
) {console
.dir(this
) }); ---------------------------------------------- (async
function
foo
() {function
boo
() {console
.dir(this
) }await
boo()console
.dir(this
) })() ----------------------------------------------function
foo
(){ setTimeout(function
(){console
.log(this
) }) } foo.call(5
) ----------------------------------------------const
obj = { foo(callback) { callback()console
.log(this
.foo === obj.foo)console
.log(this
=== obj) } } obj.foo(function
() {console
.log(this
) }) ----------------------------------------------const
obj = { foo(callback) {arguments
[0
]()console
.log(this
.foo === obj.foo)console
.log(this
=== obj) } } obj.foo(function
() {console
.log(this
) }) 複製代碼/<code>
箭頭函數中的this
es6引入的箭頭函數,是不具有this綁定,不過在其函數體中可以使用this,而這個this指向的是箭頭函數當前所處的詞法環境中的this對象,可以理解為,this在箭頭函數中是透明的,箭頭函數包不住this,所以函數內部與外部的this為同一值
- 判斷箭頭函數的this指向,我們可以把箭頭函數看成透明,其上下文中的this就是它的this
<code>//
可以看出箭頭函數中的this
就是其所在環境的this
,箭頭函數無法固定this
,由其環境決定 const foo =()
=> {console
.dir(this
)//
window
,嚴格下還是window
} foo() ----------------------------------------------//
可見對象中的this
指向window
,箭頭函數中的this
指向對象中的this
。由於只有創建執行上下文才會綁定this
指向,而除了全局上下文,只有函數作用域才會創建上下文環境從而綁定this
,創建對象不會綁定this
,所以還是全局this
const obj={ this:this
, foo:()
=>{console
.dir(this
)//
window
,嚴格下window
} }console
.dir(obj.this
)//
window
,嚴格下window
obj.foo() ---------------------------------------------//
對象方法內部嵌套箭頭函數,則此箭頭函數的this
屬於外部非箭頭函數this
。當調用obj.foo時foo函數創建的執行上下文中的this
綁定對象obj,而箭頭函數並不會綁定this
,所以其this
屬於foo下的this
,即對象obj const obj = { foo: function () {return
() => {console
.dir(this
)//
obj ,嚴格下 obj } } } obj.foo()() 複製代碼/<code>
如何改變函數的this指向
最簡單的方法通過apply、call、bind來給函數綁定this
- apply方法中第一個參數為被調用的函數中的this指向,傳入你想要綁定的this值即可,第二個參數為被調用函數的參數集合,通常是個數組
- call與apply方法基本一致,區別在於傳入參數形式不同,call傳入的參數為可變參數列表,參數按逐個傳入
- bind方法與以上不同的是不會直接調用函數,只是先綁定函數的this,到要使用的時候調用即可,此方法返回一個綁定this與參數之後的新函數,其傳入參數形式同call
- 通過變量保留指定this來達到固定this
<code>const
obj = {name
:'obj'
,foo
:function
() {let
_this =this
function
boo
() { _this.name ='OBJ'
console
.dir(obj.name) }return
boo } } obj.foo()() 複製代碼/<code>
八、apply、call、bind實現
這3者的實現其實差不多,bind實現可能會有點不一樣,都要實現this的改變
手動實現apply
- 思路就是想辦法使函數被傳入的thisArg調用,那麼函數的this就指向調用者
<code>Function
.prototype.Apply =function
(thisArg, args = Symbol.for(
'args'
)) {console
.dir(this
)const
fn =Symbol
('fn'
) thisArg[fn] =this
||window
args ===Symbol
.for('args'
) ? thisArg[fn]() : thisArg[fn](...args)delete
thisArg[fn] }var
name ='foo'
var
age =5
function
foo
(age,height
) {console
.log(this
.name)console
.log(age)console
.log(height) }const
obj = {name
:'obj'
,age
:3
} foo.Apply(obj,[obj.age,null
]) 複製代碼/<code>
手動實現call
基本思路同apply,就是傳參形式改變一下,這裡通過arguments獲取參數列表
<code>Function.prototype.Call = function (thisArg) { console.dir(this) //this為這個方法的調用者=>foo函數 const fn = Symbol('fn') //生成一個不重複的鍵 thisArg[fn] = this || window //把foo函數作為傳入this的一個方法 const args = Array.from(arguments).slice(1) args.length ? thisArg[fn
](...args
) : thisArg[fn
]() //調用這方法,傳參 delete thisArg[fn] //使用完刪除 } 複製代碼/<code>
手動實現bind
bind函數要能夠返回嚴格綁定this與參數後的函數,調用這個返回的函數時有可能還會傳入參數,那麼需要拼接參數
<code>Function
.prototype.Bind =function
(thisArg
) {const
fn =Symbol
('fn'
) thisArg[fn] =this
||window
const
f = thisArg[fn]delete
thisArg[fn]const
args =Array
.from(arguments
).slice(1
)return
function
() {const
arg = args.concat(...arguments) f(...arg) } }var
name ='foo'
var
age =5
var
height =4
function
foo
(age, height
) {console
.log(this
.name)console
.log(age)console
.log(height) }const
obj = {name
:'obj'
,age
:3
} foo.Bind(obj, obj.age)(2
) 複製代碼/<code>
總結
以上總結可能沒有什麼順序,但是每章節都是針對性的講解,零散的知識點較多,希望看完這篇文章能擴展你的知識面,也許某方面講的不是很詳細,如果感興趣可以找些針對性的文章進行深入瞭解。
部分內容並非原創,還是要感謝前輩的總結,如果本文影響到您的利益,那麼還請事先告知,在寫本文時的初衷就是想給更多學習前端的小夥伴拓展知識,夯實基礎,共同進步,也為了以後方便複習使用
總結不易,如需轉載請註明出處,感謝!
求點贊
如果本文對你有所幫助,就請點個贊支持一下吧,讓更多人看到,你的支持就是我堅持寫作下去的動力,如果喜歡我的文章,那麼還請關注後續的文章吧~ ψ(`∇´)ψ
未完結,有興趣的小夥們,請看下一篇【實踐篇】
推薦JavaScript經典實例學習資料文章
《前端開發規範:命名規範、html規範、css規範、js規範》
《100個原生JavaScript代碼片段知識點詳細彙總【實踐】 》
《手把手教你深入鞏固JavaScript知識體系【思維導圖】》
《一個合格的中級前端工程師需要掌握的 28 個 JavaScript 技巧》
《身份證號碼的正則表達式及驗證詳解(JavaScript,Regex)》
《127個常用的JS代碼片段,每段代碼花30秒就能看懂-【上】》
《深入淺出講解JS中this/apply/call/bind巧妙用法【實踐】》
《乾貨滿滿!如何優雅簡潔地實現時鐘翻牌器(支持JS/Vue/React)》
作者:*5102
轉發鏈接:
https://juejin.im/post/5e9f0bdce51d4546f5791989