JavaScript不具有 sleep() 函數,該函數會導致代碼在恢復執行之前等待指定的時間段。如果需要JavaScript等待,該怎么做呢?
假設您想將三則消息記錄到Javascript控制臺,每條消息之間要延遲一秒鐘。JavaScript中沒有 sleep() 方法,所以你可以嘗試使用下一個最好的方法 setTimeout()。
不幸的是,setTimeout() 不能像你期望的那樣正常工作,這取決于你如何使用它。你可能已經在JavaScript循環中的某個點上試過了,看到 setTimeout() 似乎根本不起作用。
問題的產生是由于將 setTimeout() 誤解為 sleep() 函數,而實際上它是按照自己的一套規則工作的。
在本文中,我將解釋如何使用 setTimeout(),包括如何使用它來制作一個睡眠函數,使JavaScript暫停執行并在連續的代碼行之間等待。
瀏覽一下 setTimeout() 的文檔,它似乎需要一個 "延遲 "參數,以毫秒為單位。
回到原始問題,您嘗試調用 setTimeout(1000) 在兩次調用 console.log() 函數之間等待1秒。
不幸的是 setTimeout() 不能這樣工作:
setTimeout(1000)
console.log(1)
setTimeout(1000)
console.log(2)
setTimeout(1000)
console.log(3)
for (let i = 0; i <= 3; i++) {
setTimeout(1000)
console.log(`#${i}`)
}
這段代碼的結果完全沒有延遲,就像 setTimeout() 不存在一樣。
回顧文檔,你會發現問題在于實際上第一個參數應該是函數調用,而不是延遲。畢竟,setTimeout() 實際上不是 sleep() 方法。
你重寫代碼以將回調函數作為第一個參數并將必需的延遲作為第二個參數:
setTimeout(() => console.log(1), 1000)
setTimeout(() => console.log(2), 1000)
setTimeout(() => console.log(3), 1000)
for (let i = 0; i <= 3; i++) {
setTimeout(() => console.log(`#${i}`), 1000)
}
這樣一來,三個console.log的日志信息在經過1000ms(1秒)的單次延時后,會一起顯示,而不是每次重復調用之間延時1秒的理想效果。
在討論如何解決此問題之前,讓我們更詳細地研究一下 setTimeout() 函數。
檢查setTimeout ()
你可能已經注意到上面第二個代碼片段中使用了箭頭函數。這些是必需的,因為你需要將匿名回調函數傳遞給 setTimeout(),該函數將在超時后運行要執行的代碼。
在匿名函數中,你可以指定在超時時間后執行的任意代碼:
// 使用箭頭語法的匿名回調函數。
setTimeout(() => console.log("你好!"), 1000)
// 這等同于使用function關鍵字
setTimeout(function() { console.log("你好!") }, 1000)
理論上,你可以只傳遞函數作為第一個參數,回調函數的參數作為剩余的參數,但對我來說,這似乎從來沒有正確的工作:
// 應該能用,但不能用
setTimeout(console.log, 1000, "你好")
人們使用字符串解決此問題,但是不建議這樣做。從字符串執行JavaScript具有安全隱患,因為任何不當行為者都可以運行作為字符串注入的任意代碼。
// 應該沒用,但確實有用
setTimeout(`console.log("你好")`, 1000)
那么,為什么在我們的第一組代碼示例中 setTimeout() 失?。亢孟裎覀冊谡_使用它,每次都重復了1000ms的延遲。
原因是 setTimeout() 作為同步代碼執行,并且對 setTimeout() 的多次調用均同時運行。每次調用 setTimeout() 都會創建異步代碼,該代碼將在給定延遲后稍后執行。由于代碼段中的每個延遲都是相同的(1000毫秒),因此所有排隊的代碼將在1秒鐘的單個延遲后同時運行。
如前所述,setTimeout() 實際上不是 sleep() 函數,取而代之的是,它只是將異步代碼排入隊列以供以后執行。幸運的是,可以使用 setTimeout() 在JavaScript中創建自己的 sleep() 函數。
如何編寫sleep函數
通過Promises,async 和 await 的功能,您可以編寫一個 sleep() 函數,該函數將按預期運行。
但是,你只能從 async 函數中調用此自定義 sleep() 函數,并且需要將其與 await 關鍵字一起使用。
這段代碼演示了如何編寫一個 sleep() 函數:
const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay))
const repeatedGreetings = async () => {
await sleep(1000)
console.log(1)
await sleep(1000)
console.log(2)
await sleep(1000)
console.log(3)
}
repeatedGreetings()
此JavaScript sleep() 函數的功能與您預期的完全一樣,因為 await 導致代碼的同步執行暫停,直到Promise被解決為止。
一個簡單的選擇
另外,你可以在第一次調用 setTimeout() 時指定增加的超時時間。
以下代碼等效于上一個示例:
setTimeout(() => console.log(1), 1000)
setTimeout(() => console.log(2), 2000)
setTimeout(() => console.log(3), 3000)
使用增加超時是可行的,因為代碼是同時執行的,所以指定的回調函數將在同步代碼執行的1、2和3秒后執行。
它會循環運行嗎?
如你所料,以上兩種暫停JavaScript執行的選項都可以在循環中正常工作。讓我們看兩個簡單的例子。
這是使用自定義 sleep() 函數的代碼段:
const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay))
async function repeatGreetingsLoop() {
for (let i = 0; i <= 5; i++) {
await sleep(1000)
console.log(`Hello #${i}`)
}
}
repeatGreetingsLoop()
這是一個簡單的使用增加超時的代碼片段:
for (let i = 0; i <= 5; i++) {
setTimeout(() => console.log(`Hello #${i}`), 1000 * i)
}
我更喜歡后一種語法,特別是在循環中使用。
總結
JavaScript可能沒有 sleep() 或 wait() 函數,但是使用內置的 setTimeout() 函數很容易創建一個JavaScript,只要你謹慎使用它即可。
就其本身而言,setTimeout() 不能用作 sleep() 函數,但是你可以使用 async 和 await 創建自定義JavaScript sleep() 函數。
采用不同的方法,可以將交錯的(增加的)超時傳遞給 setTimeout() 來模擬 sleep() 函數。之所以可行,是因為所有對setTimeout() 的調用都是同步執行的,就像JavaScript通常一樣。
希望這可以幫助你在代碼中引入一些延遲——僅使用原始JavaScript,而無需外部庫或框架。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務
1. 隨機排列
在開發者,有時候我們需要對數組的順序進行重新的洗牌。 在 JS 中并沒有提供數組隨機排序的方法,這里提供一個隨機排序的方法:
function shuffle(arr) {
var i, j, temp;
for (i = arr.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i + 1));
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
return arr;
}
2. 唯一值
在開發者,我們經常需要過濾重復的值,這里提供幾種方式來過濾數組的重復值。
使用 Set 對象
使用 Set() 函數,此函數可與單個值數組一起使用。對于數組中嵌套的對象值而言,不是一個好的選擇。
const numArray = [1,2,3,4,2,3,4,5,1,1,2,3,3,4,5,6,7,8,2,4,6];
// 使用 Array.from 方法
Array.from(new Set(numArray));
// 使用展開方式
[...new Set(numArray)]
使用 Array.filter
使用 filter 方法,我們可以對元素是對象的進行過濾。
const data = [
{id: 1, name: 'Lemon'},
{id: 2, name: 'Mint'},
{id: 3, name: 'Mango'},
{id: 4, name: 'Apple'},
{id: 5, name: 'Lemon'},
{id: 6, name: 'Mint'},
{id: 7, name: 'Mango'},
{id: 8, name: 'Apple'},
]
function findUnique(data) {
return data.filter((value, index, array) => {
if (array.findIndex(item => item.name === value.name) === index) {
return value;
}
})
}
3. 使用 loadsh 的 lodash 方法
import {uniqBy} from 'lodash'
const data = [
{id: 1, name: 'Lemon'},
{id: 2, name: 'Mint'},
{id: 3, name: 'Mango'},
{id: 4, name: 'Apple'},
{id: 5, name: 'Lemon'},
{id: 6, name: 'Mint'},
{id: 7, name: 'Mango'},
{id: 8, name: 'Apple'},
]
function findUnique(data) {
return uniqBy(data, e => {
return e.name
})
}
3. 按屬性對 對象數組 進行排序
我們知道 JS 數組中的 sort 方法是按字典順序進行排序的,所以對于字符串類, 該方法是可以很好的正常工作,但對于數據元素是對象類型,就不太好使了,這里我們需要自定義一個排序方法。
在比較函數中,我們將根據以下條件返回值:
小于0:A 在 B 之前
大于0 :B 在 A 之前
等于0 :A 和 B 彼此保持不變
const data = [
{id: 1, name: 'Lemon', type: 'fruit'},
{id: 2, name: 'Mint', type: 'vegetable'},
{id: 3, name: 'Mango', type: 'grain'},
{id: 4, name: 'Apple', type: 'fruit'},
{id: 5, name: 'Lemon', type: 'vegetable'},
{id: 6, name: 'Mint', type: 'fruit'},
{id: 7, name: 'Mango', type: 'fruit'},
{id: 8, name: 'Apple', type: 'grain'},
]
function compare(a, b) {
// Use toLowerCase() to ignore character casing
const typeA = a.type.toLowerCase();
const typeB = b.type.toLowerCase();
let comparison = 0;
if (typeA > typeB) {
comparison = 1;
} else if (typeA < typeB) {
comparison = -1;
}
return comparison;
}
data.sort(compare)
4. 把數組轉成以指定符號分隔的字符串
JS 中有個方法可以做到這一點,就是使用數組中的 .join() 方法,我們可以傳入指定的符號來做數組進行分隔。
const data = ['Mango', 'Apple', 'Banana', 'Peach']
data.join(',');
// return "Mango,Apple,Banana,Peach"
5. 從數組中選擇一個元素
對于此任務,我們有多種方式,一種是使用 forEach 組合 if-else 的方式 ,另一種可以使用filter 方法,但是使用forEach 和filter的缺點是:
在forEach中,我們要額外的遍歷其它不需要元素,并且還要使用 if 語句來提取所需的值。
在filter 方法中,我們有一個簡單的比較操作,但是它將返回的是一個數組,而是我們想要是根據給定條件從數組中獲得單個對象。
為了解決這個問題,我們可以使用 find函數從數組中找到確切的元素并返回該對象,這里我們不需要使用if-else語句來檢查元素是否滿足條件。
const data = [
{id: 1, name: 'Lemon'},
{id: 2, name: 'Mint'},
{id: 3, name: 'Mango'},
{id: 4, name: 'Apple'}
]
const value = data.find(item => item.name === 'Apple')
// value = {id: 4, name: 'Apple'}
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務
1. Vue 無法檢測實例被創建時不存在于 data 中的 property
原因:由于 Vue 會在初始化實例時對 property 執行 getter/setter 轉化,所以 property 必須在 data 對象上存在才能讓 Vue 將它轉換為響應式的。
場景:
var vm = new Vue({
data:{},
// 頁面不會變化
template: '<div>{{message}}</div>'
})
vm.message = 'Hello!' // `vm.message` 不是響應式的
解決辦法:
var vm = new Vue({
data: {
// 聲明 a、b 為一個空值字符串
message: '',
},
template: '<div>{{ message }}</div>'
})
vm.message = 'Hello!'
2. Vue 無法檢測對象 property 的添加或移除
原因:官方 - 由于 JavaScript(ES5) 的限制,Vue.js 不能檢測到對象屬性的添加或刪除。因為 Vue.js 在初始化實例時將屬性轉為 getter/setter,所以屬性必須在 data 對象上才能讓 Vue.js 轉換它,才能讓它是響應的。
場景:
var vm = new Vue({
data:{
obj: {
id: 001
}
},
// 頁面不會變化
template: '<div>{{ obj.message }}</div>'
})
vm.obj.message = 'hello' // 不是響應式的
delete vm.obj.id // 不是響應式的
解決辦法:
// 動態添加 - Vue.set
Vue.set(vm.obj, propertyName, newValue)
// 動態添加 - vm.$set
vm.$set(vm.obj, propertyName, newValue)
// 動態添加多個
// 代替 Object.assign(this.obj, { a: 1, b: 2 })
this.obj = Object.assign({}, this.obj, { a: 1, b: 2 })
// 動態移除 - Vue.delete
Vue.delete(vm.obj, propertyName)
// 動態移除 - vm.$delete
vm.$delete(vm.obj, propertyName)
3. Vue 不能檢測通過數組索引直接修改一個數組項
原因:官方 - 由于 JavaScript 的限制,Vue 不能檢測數組和對象的變化;尤雨溪 - 性能代價和獲得用戶體驗不成正比。
場景:
var vm = new Vue({
data: {
items: ['a', 'b', 'c']
}
})
vm.items[1] = 'x' // 不是響應性的
解決辦法:
// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// vm.$set
vm.$set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)
拓展:Object.defineProperty() 可以監測數組的變化
Object.defineProperty() 可以監測數組的變化。但對數組新增一個屬性(index)不會監測到數據變化,因為無法監測到新增數組的下標(index),刪除一個屬性(index)也是。
場景:
var arr = [1, 2, 3, 4]
arr.forEach(function(item, index) {
Object.defineProperty(arr, index, {
set: function(value) {
console.log('觸發 setter')
item = value
},
get: function() {
console.log('觸發 getter')
return item
}
})
})
arr[1] = '123' // 觸發 setter
arr[1] // 觸發 getter 返回值為 "123"
arr[5] = 5 // 不會觸發 setter 和 getter
4. Vue 不能監測直接修改數組長度的變化
原因:官方 - 由于 JavaScript 的限制,Vue 不能檢測數組和對象的變化;尤雨溪 - 性能代價和獲得用戶體驗不成正比。
場景:
var vm = new Vue({
data: {
items: ['a', 'b', 'c']
}
})
vm.items.length = 2 // 不是響應性的
解決辦法:
vm.items.splice(newLength)
5. 在異步更新執行之前操作 DOM 數據不會變化
原因:Vue 在更新 DOM 時是異步執行的。只要偵聽到數據變化,Vue 將開啟一個隊列,并緩沖在同一事件循環中發生的所有數據變更。如果同一個 watcher 被多次觸發,只會被推入到隊列中一次。這種在緩沖時去除重復數據對于避免不必要的計算和 DOM 操作是非常重要的。然后,在下一個的事件循環“tick”中,Vue 刷新隊列并執行實際 (已去重的) 工作。Vue 在內部對異步隊列嘗試使用原生的 Promise.then、MutationObserver 和 setImmediate,如果執行環境不支持,則會采用 setTimeout(fn, 0) 代替。
場景:
<div id="example">{{message}}</div>
var vm = new Vue({
el: '#example',
data: {
message: '123'
}
})
vm.message = 'new message' // 更改數據
vm.$el.textContent === 'new message' // false
vm.$el.style.color = 'red' // 頁面沒有變化
解決辦法:
var vm = new Vue({
el: '#example',
data: {
message: '123'
}
})
vm.message = 'new message' // 更改數據
//使用 Vue.nextTick(callback) callback 將在 DOM 更新完成后被調用
Vue.nextTick(function () {
vm.$el.textContent === 'new message' // true
vm.$el.style.color = 'red' // 文字顏色變成紅色
})
拓展:異步更新帶來的數據響應的誤解
<!-- 頁面顯示:我更新啦! -->
<div id="example">{{message.text}}</div>
var vm = new Vue({
el: '#example',
data: {
message: {},
}
})
vm.$nextTick(function () {
this.message = {}
this.message.text = '我更新啦!'
})
上段代碼中,我們在 data 對象中聲明了一個 message 空對象,然后在下次 DOM 更新循環結束之后觸發的異步回調中,執行了如下兩段代碼:
this.message = {};
this.message.text = '我更新啦!'
到這里,模版更新了,頁面最后會顯示 我更新啦!。
模板更新了,應該具有響應式特性,如果這么想那么你就已經走入了誤區。
一開始我們在 data 對象中只是聲明了一個 message 空對象,并不具有 text 屬性,所以該 text 屬性是不具有響應式特性的。
但模板切切實實已經更新了,這又是怎么回事呢?
那是因為 Vue.js 的 DOM 更新是異步的,即當 setter 操作發生后,指令并不會立馬更新,指令的更新操作會有一個延遲,當指令更新真正執行的時候,此時 text 屬性已經賦值,所以指令更新模板時得到的是新值。
模板中每個指令/數據綁定都有一個對應的 watcher 對象,在計算過程中它把屬性記錄為依賴。之后當依賴的 setter 被調用時,會觸發 watcher 重新計算 ,也就會導致它的關聯指令更新 DOM。
具體流程如下所示:
執行 this.message = {}; 時, setter 被調用。
Vue.js 追蹤到 message 依賴的 setter 被調用后,會觸發 watcher 重新計算。
this.message.text = '我更新啦!'; 對 text 屬性進行賦值。
異步回調邏輯執行結束之后,就會導致它的關聯指令更新 DOM,指令更新開始執行。
所以真正的觸發模版更新的操作是 this.message = {};這一句引起的,因為觸發了 setter,所以單看上述例子,具有響應式特性的數據只有 message 這一層,它的動態添加的屬性是不具備的。
對應上述第二點 - Vue 無法檢測對象 property 的添加或移除
6. 循環嵌套層級太深,視圖不更新?
看到網上有些人說數據更新的層級太深,導致數據不更新或者更新緩慢從而導致試圖不更新?
由于我沒有遇到過這種情況,在我試圖重現這種場景的情況下,發現并沒有上述情況的發生,所以對于這一點不進行過多描述(如果有人在真實場景下遇到這種情況留個言吧)。
針對上述情況有人給出的解決方案是使用強制更新:
如果你發現你自己需要在 Vue 中做一次強制更新,99.9% 的情況,是你在某個地方做錯了事。
vm.$forceUpdate()
7. 拓展:路由參數變化時,頁面不更新(數據不更新)
拓展一個因為路由參數變化,而導致頁面不更新的問題,頁面不更新本質上就是數據沒有更新。
原因:路由視圖組件引用了相同組件時,當路由參會變化時,會導致該組件無法更新,也就是我們常說中的頁面無法更新的問題。
場景:
<div id="app">
<ul>
<li><router-link to="/home/foo">To Foo</router-link></li>
<li><router-link to="/home/baz">To Baz</router-link></li>
<li><router-link to="/home/bar">To Bar</router-link></li>
</ul>
<router-view></router-view>
</div>
const Home = {
template: `<div>{{message}}</div>`,
data() {
return {
message: this.$route.params.name
}
}
}
const router = new VueRouter({
mode:'history',
routes: [
{path: '/home', component: Home },
{path: '/home/:name', component: Home }
]
})
new Vue({
el: '#app',
router
})
上段代碼中,我們在路由構建選項 routes 中配置了一個動態路由 '/home/:name',它們共用一個路由組件 Home,這代表他們復用 RouterView 。
當進行路由切換時,頁面只會渲染第一次路由匹配到的參數,之后再進行路由切換時,message 是沒有變化的。
解決辦法:
解決的辦法有很多種,這里只列舉我常用到幾種方法。
通過 watch 監聽 $route 的變化。
const Home = {
template: `<div>{{message}}</div>`,
data() {
return {
message: this.$route.params.name
}
},
watch: {
'$route': function() {
this.message = this.$route.params.name
}
}
}
...
new Vue({
el: '#app',
router
})
給 <router-view> 綁定 key 屬性,這樣 Vue 就會認為這是不同的 <router-view>。
弊端:如果從 /home 跳轉到 /user 等其他路由下,我們是不用擔心組件更新問題的,所以這個時候 key 屬性是多余的。
<div id="app">
...
<router-view :key="key"></router-view>
</div>
前言
前面幾篇我們就 Redux 展開了幾篇文章,這次我們來實現 react-thunk,就不是叫實現 redux-thunk 了,直接上源碼,因為源碼就11行。如果對 Redux 中間件還不理解的,可以看我寫的 Redux 文章。
實現一個迷你Redux(基礎版)
實現一個Redux(完善版)
淺談React的Context API
帶你實現 react-redux
為什么要用 redux-thunk
在使用 Redux 過程,通過 dispatch 方法派發一個 action 對象。當我們使用 redux-thunk 后,可以 dispatch 一個 function。redux-thunk會自動調用這個 function,并且傳遞 dispatch, getState 方法作為參數。這樣一來,我們就能在這個 function 里面處理異步邏輯,處理復雜邏輯,這是原來 Redux 做不到的,因為原來就只能 dispatch 一個簡單對象。
用法
redux-thunk 作為 redux 的中間件,主要用來處理異步請求,比如:
export function fetchData() {
return (dispatch, getState) => {
// to do ...
axios.get('https://jsonplaceholder.typicode.com/todos/1').then(res => {
console.log(res)
})
}
}
redux-thunk 源碼
redux-thunk 的源碼比較簡潔,實際就11行。前幾篇我們說到 redux 的中間件形式,
本質上是對 store.dispatch 方法進行了增強改造,基本是類似這種形式:
const middleware = (store) => next => action => {}
在這里就不詳細解釋了,可以看 實現一個Redux(完善版)
先給個縮水版的實現:
const thunk = ({ getState, dispatch }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState)
}
return next(action)
}
export default thunk
原理:即當 action 為 function 的時候,就調用這個 function (傳入 dispatch, getState)并返回;如果不是,就直接傳給下一個中間件。
完整源碼如下:
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
// 如果action是一個function,就返回action(dispatch, getState, extraArgument),否則返回next(action)。
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument)
}
// next為之前傳入的store.dispatch,即改寫前的dispatch
return next(action)
}
}
const thunk = createThunkMiddleware()
// 給thunk設置一個變量withExtraArgument,并且將createThunkMiddleware整個函數賦給它
thunk.withExtraArgument = createThunkMiddleware
export default thunk
我們發現其實還多了 extraArgument 傳入,這個是自定義參數,如下用法:
const api = "https://jsonplaceholder.typicode.com/todos/1";
const whatever = 10;
const store = createStore(
reducer,
applyMiddleware(thunk.withExtraArgument({ api, whatever })),
);
// later
function fetchData() {
return (dispatch, getState, { api, whatever }) => {
// you can use api and something else here
};
}
總結
同 redux-thunk 非常流行的庫 redux-saga 一樣,都是在 redux 中做異步請求等副作用。Redux 相關的系列文章就暫時寫到這部分為止,下次會寫其他系列。
一、前言
前端的模塊化規范包括 commonJS、AMD、CMD 和 ES6。其中 AMD 和 CMD 可以說是過渡期的產物,目前較為常見的是commonJS 和 ES6。在 TS 中這兩種模塊化方案的混用,往往會出現一些意想不到的問題。
二、import * as
考慮到兼容性,我們一般會將代碼編譯為 es5 標準,于是 tsconfig.json 會有以下配置:
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
}
}
代碼編譯后最終會以 commonJS 的形式輸出。
使用 React 的時候,這種寫法 import React from "react" 會收到一個莫名其妙的報錯:
Module "react" has no default export
這時候你只能把代碼改成這樣:import * as React from "react"。
究其原因,React 是以 commonJS 的規范導出的,而 import React from "react" 這種寫法會去找 React 模塊中的 exports.default,而 React 并沒有導出這個屬性,于是就報了如上錯誤。而 import * as React 的寫法會取 module.exports 中的值,這樣使用起來就不會有任何問題。我們來看看 React 模塊導出的代碼到底是怎樣的(精簡過):
...
var React = {
Children: {
map: mapChildren,
forEach: forEachChildren,
count: countChildren,
toArray: toArray,
only: onlyChild
},
createRef: createRef,
Component: Component,
PureComponent: PureComponent,
...
}
module.exports = React;
可以看到,React 導出的是一個對象,自然也不會有 default 屬性。
二、esModuleInterop
為了兼容這種這種情況,TS 提供了配置項 esModuleInterop 和 allowSyntheticDefaultImports,加上后就不會有報錯了:
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true
}
}
其中 allowSyntheticDefaultImports 這個字段的作用只是在靜態類型檢查時,把 import 沒有 exports.default 的報錯忽略掉。
而 esModuleInterop 會真正的在編譯的過程中生成兼容代碼,使模塊能正確的導入。還是開始的代碼:
import React from "react";
現在 TS 編譯后是這樣的:
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
var react_1 = __importDefault(require("react"));
編譯器幫我們生成了一個新的對象,將模塊賦值給它的 default 屬性,運行時就不會報錯了。
三、Tree Shaking
如果把 TS 按照 ES6 規范編譯,就不需要加上 esModuleInterop,只需要 allowSyntheticDefaultImports,防止靜態類型檢查時報錯。
{
"compilerOptions": {
"module": "es6",
"target": "es6",
"allowSyntheticDefaultImports": true
}
}
什么情況下我們會考慮導出成 ES6 規范呢?多數情況是為了使用 webpack 的 tree shaking 特性,因為它只對 ES6 的代碼生效。
順便再發散一下,講講 babel-plugin-component。
import { Button, Select } from 'element-ui'
上面的代碼經過編譯后,是下面這樣的:
var a = require('element-ui');
var Button = a.Button;
var Select = a.Select;
var a = require('element-ui') 會引入整個組件庫,即使只用了其中的 2 個組件。
babel-plugin-component 的作用是將代碼做如下轉換:
// 轉換前
import { Button, Select } from 'element-ui'
// 轉換后
import Button from 'element-ui/lib/button'
import Select from 'element-ui/lib/select'
最終編譯出來是這個樣子,只會加載用到的組件:
var Button = require('element-ui/lib/button');
var Select = require('element-ui/lib/select');
四、總結
本文講解了 TypeScript 是如何導入不同模塊標準打包的代碼的。無論你導入的是 commonJS 還是 ES6 的代碼,萬無一失的方式是把 esModuleInterop 和 allowSyntheticDefaultImports 都配置上。
初始化
使用 https://github.com/XYShaoKang... 作為基礎模板
gatsby new gatsby-project-config https://github.com/XYShaoKang/gatsby-hello-world
Prettier 配置
安裝 VSCode 擴展
按 Ctrl + P (MAC 下: Cmd + P) 輸入以下命令,按回車安裝
ext install esbenp.prettier-vscode
安裝依賴
yarn add -D prettier
Prettier 配置文件.prettierrc.js
// .prettierrc.js
module.exports = {
trailingComma: 'es5',
tabWidth: 2,
semi: false,
singleQuote: true,
endOfLine: 'lf',
printWidth: 50,
arrowParens: 'avoid',
}
ESLint 配置
安裝 VSCode 擴展
按 Ctrl + P (MAC 下: Cmd + P) 輸入以下命令,按回車安裝
ext install dbaeumer.vscode-eslint
安裝 ESLint 依賴
yarn add -D eslint babel-eslint eslint-config-google eslint-plugin-react eslint-plugin-filenames
ESLint 配置文件.eslintrc.js
使用官方倉庫的配置,之后在根據需要修改
// https://github.com/gatsbyjs/gatsby/blob/master/.eslintrc.js
// .eslintrc.js
module.exports = {
parser: 'babel-eslint',
extends: [
'google',
'eslint:recommended',
'plugin:react/recommended',
],
plugins: ['react', 'filenames'],
parserOptions: {
ecmaVersion: 2016,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
env: {
browser: true,
es6: true,
node: true,
jest: true,
},
globals: {
before: true,
after: true,
spyOn: true,
__PATH_PREFIX__: true,
__BASE_PATH__: true,
__ASSET_PREFIX__: true,
},
rules: {
'arrow-body-style': [
'error',
'as-needed',
{ requireReturnForObjectLiteral: true },
],
'no-unused-expressions': [
'error',
{
allowTaggedTemplates: true,
},
],
'consistent-return': ['error'],
'filenames/match-regex': [
'error',
'^[a-z-\\d\\.]+$',
true,
],
'no-console': 'off',
'no-inner-declarations': 'off',
quotes: ['error', 'backtick'],
'react/display-name': 'off',
'react/jsx-key': 'warn',
'react/no-unescaped-entities': 'off',
'react/prop-types': 'off',
'require-jsdoc': 'off',
'valid-jsdoc': 'off',
},
settings: {
react: {
version: '16.4.2',
},
},
}
解決 Prettier ESLint 規則沖突
推薦配置
安裝依賴
yarn add -D eslint-config-prettier eslint-plugin-prettier
在.eslintrc.js中的extends添加'plugin:prettier/recommended'
module.exports = {
extends: ['plugin:prettier/recommended'],
}
VSCode 中 Prettier 和 ESLint 協作
方式一:使用 ESLint 擴展來格式化代碼
配置.vscode/settings.json
// .vscode/settings.json
{
"eslint.format.enable": true,
"[javascript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[javascriptreact]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
}
}
ESLint 擴展會默認忽略.開頭的文件,比如.eslintrc.js
如果需要格式化.開頭的文件,可以在.eslintignore中添加一個否定忽略來啟用對應文件的格式化功能.
!.eslintrc.js
或者直接使用!.*,這樣可以開啟所有點文件的格式化功能
方式二:使用 Prettier 擴展來格式化代碼
在版prettier-vscode@v5.0.0中已經刪除了直接對linter的集成,所以版沒法像之前那樣,通過prettier-eslint來集成ESLint的修復了(一定要這樣用的話,可以通過降級到prettier-vscode@4來使用了).如果要使用Prettier來格式化的話,就只能按照官方指南中的說的集成方法,讓Prettier來處理格式,通過配置在保存時使用ESlint自動修復代碼.只是這樣必須要保存文件時,才能觸發ESLint的修復了.
配置 VSCode 使用 Prettier 來格式化 js 和 jsx 文件
在項目中新建文件.vscode/settings.json
// .vscode/settings.json
{
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}
說實話這個體驗很糟糕,之前直接一鍵格式化代碼并且修復 ESLint 錯誤,可以對比格式化之前和格式化之后的代碼,如果感覺不對可以直接撤銷更改就好了.現在必須要通過保存,才能觸發修復 ESlint 錯誤.而在開發過程中,通過監聽文件改變來觸發熱加載或者重新編譯是很常見的操作.這樣之后每次想要去修復 ESLint 錯誤,還是只是想看看修復錯誤之后的樣子,都必須要去觸發熱加載或重新編譯,每次操作的成本就太高了.
我更推薦第一種方式使用 ESLint 擴展來對代碼進行格式化.
調試 Gatsby 配置
調試構建過程
添加配置文件.vscode/launch.json
// .vscode/launch.json
{
// 使用 IntelliSense 了解相關屬性。
// 懸停以查看現有屬性的描述。
// 欲了解更多信息,請訪問: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Gatsby develop",
"type": "node",
"request": "launch",
"protocol": "inspector",
"program": "${workspaceRoot}/node_modules/gatsby/dist/bin/gatsby",
"args": ["develop"],
"stopOnEntry": false,
"runtimeArgs": ["--nolazy"],
"sourceMaps": false,
"outputCapture": "std"
}
]
}
的gatsby@2.22.*版本中調試不能進到斷點,解決辦法是降級到2.21.*,yarn add gatsby@2.21.40,等待官方修復再使用版本的
調試客戶端
需要安裝 Debugger for Chrome 擴展
ext install msjsdiag.debugger-for-chrome
添加配置文件.vscode/launch.json
// .vscode/launch.json
{
// 使用 IntelliSense 了解相關屬性。
// 懸停以查看現有屬性的描述。
// 欲了解更多信息,請訪問: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Gatsby Client Debug",
"url": "http://localhost:8000",
"webRoot": "${workspaceFolder}"
}
]
}
先啟動 Gatsby,yarn develop,然后按 F5 開始調試.
收集了一些工作中常用的工具。
如果你有好用的工具或者有意思的工具網站,要留言哦!
React是Facebook開發的一款JS庫,那么Facebook為什么要建造React呢,主要為了解決什么問題,通過這個又是如何解決的?
從這幾個問題出發我就在網上搜查了一下,有這樣的解釋。
Facebook認為MVC無法滿足他們的擴展需求,由于他們非常巨大的代碼庫和龐大的組織,使得MVC很快變得非常復復雜,每當需要添加一項新的功能或特性時,系統的復雜度就成級數增長,致使代碼變得脆弱和不可預測,結果導致他們的MVC正在土崩瓦解。認為MVC不適合大規模應用,當系統中有很多的模型和相應的視圖時,其復雜度就會迅速擴大,非常難以理解和調試,特別是模型和視圖間可能存在的雙向數據流動。
解決這個問題需要“以某種方式組織代碼,使其更加可預測”,這通過他們(Facebook)提出的Flux和React已經完成。
Flux
是一個系統架構,用于推進應用中的數據單向流動。React
是一個JavaScript框架,用于構建“可預期的”和“聲明式的”Web用戶界面,它已經使Facebook更快地開發Web應用
對于Flux,目前還沒怎么研究,不怎么懂,這里就先把Flux的圖放上來,有興趣或者了解的可以再分享下,這里主要說下React。
那么React是解決什么問題的,在官網可以找到這樣一句話:
We built React to solve one problem: building large applications with data that changes over time.
構建那些數據會隨時間改變的大型應用,做這些,React有兩個主要的特點:
另外在React官網上,通過《Why did we build React?》為什么我們要建造React的文檔中還可以了解到以下四點:
Virtual DOM 虛擬DOM
傳統的web應用,操作DOM一般是直接更新操作的,但是我們知道DOM更新通常是比較昂貴的。而React為了盡可能減少對DOM的操作,提供了一種不同的而又強大的方式來更新DOM,代替直接的DOM操作。就是Virtual DOM
,一個輕量級的虛擬的DOM,就是React抽象出來的一個對象,描述dom應該什么樣子的,應該如何呈現。通過這個Virtual DOM去更新真實的DOM,由這個Virtual DOM管理真實DOM的更新。
為什么通過這多一層的Virtual DOM操作就能更快呢? 這是因為React有個diff算法,更新Virtual DOM并不保證馬上影響真實的DOM,React會等到事件循環結束,然后利用這個diff算法,通過當前新的dom表述與之前的作比較,計算出最小的步驟更新真實的DOM。
component 的使用在 React 里極為重要, 因為 components 的存在讓計算 DOM diff 更。
State 和 Render
React是如何呈現真實的DOM,如何渲染組件,什么時候渲染,怎么同步更新的,這就需要簡單了解下State和Render了。state屬性包含定義組件所需要的一些數據,當數據發生變化時,將會調用Render重現渲染,這里只能通過提供的setState方法更新數據。
好了,說了這么多,下面看寫代碼吧,先看一個官網上提供的Hello World
的示例:
<!DOCTYPE html> <html> <head> <script src="http://fb.me/react-0.12.1.js"></script> <script src="http://fb.me/JSXTransformer-0.12.1.js"></script> </head> <body> <div id="example"></div> <script type="text/jsx"> React.render( <h1>Hello, world!</h1>,
document.getElementById('example')
); </script> </body> </html>
這個很簡單,瀏覽器訪問,可以看到Hello, world!
字樣。JSXTransformer.js
是支持解析JSX語法的,JSX是可以在Javascript中寫html代碼的一種語法。如果不喜歡,React也提供原生Javascript的方法。
再來看下另外一個例子:
<html> <head> <title>Hello React</title> <script src="http://fb.me/react-0.12.1.js"></script> <script src="http://fb.me/JSXTransformer-0.12.1.js"></script> <script src="http://code.jquery.com/jquery-1.10.0.min.js"></script> <script src="http://cdnjs.cloudflare.com/ajax/libs/showdown/0.3.1/showdown.min.js"></script> <style> #content{ width: 800px; margin: 0 auto; padding: 5px 10px; background-color:#eee; } .commentBox h1{ background-color: #bbb; } .commentList{ border: 1px solid yellow; padding:10px; } .commentList .comment{ border: 1px solid #bbb; padding-left: 10px; margin-bottom:10px; } .commentList .commentAuthor{ font-size: 20px; } .commentForm{ margin-top: 20px; border: 1px solid red; padding:10px; } .commentForm textarea{ width:100%; height:50px; margin:10px 0 10px 2px; } </style> </head> <body> <div id="content"></div> <script type="text/jsx"> var staticData = [ {author: "張飛", text: "我在寫一條評論~!"}, {author: "關羽", text: "2貨,都知道你在寫的是一條評論。。"}, {author: "劉備", text: "哎,咋跟這倆逗逼結拜了!"} ]; var converter = new Showdown.converter();//markdown /** 組件結構: <CommentBox> <CommentList> <Comment /> </CommentList> <CommentForm /> </CommentBox> */ //評論內容組件 var Comment = React.createClass({ render: function (){ var rawMarkup = converter.makeHtml(this.props.children.toString()); return ( <div className="comment"> <h2 className="commentAuthor"> {this.props.author}: </h2> <span dangerouslySetInnerHTML={{__html: rawMarkup}} /> </div> ); } }); //評論列表組件 var CommentList = React.createClass({ render: function (){ var commentNodes = this.props.data.map(function (comment){ return ( <Comment author={comment.author}> {comment.text} </Comment> ); }); return ( <div className="commentList"> {commentNodes} </div> ); } }); //評論表單組件 var CommentForm = React.createClass({ handleSubmit: function (e){ e.preventDefault(); var author = this.refs.author.getDOMNode().value.trim(); var text = this.refs.text.getDOMNode().value.trim(); if(!author || !text){ return; } this.props.onCommentSubmit({author: author, text: text}); this.refs.author.getDOMNode().value = ''; this.refs.text.getDOMNode().value = ''; return; }, render: function (){ return ( <form className="commentForm" onSubmit={this.handleSubmit}> <input type="text" placeholder="Your name" ref="author" /><br/> <textarea type="text" placeholder="Say something..." ref="text" ></textarea><br/> <input type="submit" value="Post" /> </form> ); } }); //評論塊組件 var CommentBox = React.createClass({ loadCommentsFromServer: function (){ this.setState({data: staticData}); /* 方便起見,這里就不走服務端了,可以自己嘗試 $.ajax({ url: this.props.url + "?_t=" + new Date().valueOf(), dataType: 'json', success: function (data){ this.setState({data: data}); }.bind(this), error: function (xhr, status, err){ console.error(this.props.url, status, err.toString()); }.bind(this) }); */ }, handleCommentSubmit: function (comment){ //TODO: submit to the server and refresh the list var comments = this.state.data; var newComments = comments.concat([comment]); //這里也不向后端提交了 staticData = newComments; this.setState({data: newComments}); }, //初始化 相當于構造函數 getInitialState: function (){ return {data: []}; }, //組件添加的時候運行 componentDidMount: function (){ this.loadCommentsFromServer(); this.interval = setInterval(this.loadCommentsFromServer, this.props.pollInterval); }, //組件刪除的時候運行 componentWillUnmount: function() { clearInterval(this.interval); }, //調用setState或者父級組件重新渲染不同的props時才會重新調用 render: function (){ return ( <div className="commentBox"> <h1>Comments</h1> <CommentList data={this.state.data}/> <CommentForm onCommentSubmit={this.handleCommentSubmit} /> </div> ); } }); //當前目錄需要有comments.json文件 //這里定義屬性,如url、pollInterval,包含在props屬性中 React.render( <CommentBox url="comments.json" pollInterval="2000" />, document.getElementById("content") ); </script> </body> </html>
乍一看挺多,主要看腳本部分就可以了。方便起見,這里都沒有走后端。定義了一個全局的變量staticData
,可權當是走服務端,通過瀏覽器的控制臺改變staticData
的值,查看下效果,提交一條評論,查看下staticData的值的變化。
國外應用的較多,facebook、Yahoo、Reddit等。在github可以看到一個列表Sites-Using-React,國內的話,查了查,貌似比較少,目前知道的有一個杭州大搜車。大多技術要在國內應用起來一般是較慢的,不過React確實感覺比較特殊,特別是UI的組件化和Virtual DOM的思想,我個人比較看好,有興趣繼續研究研究。
和其他一些js框架相比,React怎樣,比如Backbone、Angular等。
閉包是一個讓初級JavaScript
使用者既熟悉又陌生的一個概念。因為閉包在我們書寫JavaScript
代碼時,隨處可見,但是我們又不知道哪里用了閉包。
關于閉包的定義,網上(書上)的解釋總是千奇百怪,我們也只能“取其精華去其糟粕”去總結一下。
ECMAScript中,閉包指的是:
從實踐角度:一下才算是閉包:
閉包跟詞法作用域,作用域鏈,執行上下文這幾個JavaScript
中重要的概念都有關系,因此要想真的理解閉包,至少要對那幾個概念不陌生。
閉包的優點:
閉包的缺點:
我們來一步一步引出閉包。
自執行函數也叫立即調用函數(IIFE),是一個在定義時就執行的函數。
var a=1;
(function() { console.log(a)
})()
上述代碼是一個最簡單的自執行函數。
在ES6之前,是沒有塊級作用域的,只有全局作用域和函數作用域,因此自執行函數還能在ES6之前實現塊級作用域。
// ES6 塊級作用域 var a = 1; if(true) { let a=111; console.log(a); // 111 } console.log(a); // 1
這里 if{} 中用let聲明了一個 a。這個 a 就具有塊級作用域,在這個 {} 中訪問 a ,永遠訪問的都是 let 聲明的a,跟全局作用域中的a沒有關系。如果我們把 let 換成 var ,就會污染全局變量 a 。
如果用自執行函數來實現:
var a = 1;
(function() { if(true) { var a=111; console.log(a); // 111 }
})() console.log(a); // 1
為什么要在這里要引入自執行函數的概念呢?因為通常我們會用自執行函數來創建閉包,實現一定的效果。
來看一個基本上面試提問題:
for(var i=0;i<5;i++) {
setTimeout(function() { console.log(i);
},1000)
}
在理想狀態下我們期望輸出的是 0 ,1 ,2 ,3 ,4。但是實際上輸出的是5 ,5 ,5 ,5 ,5。為什么是這樣呢?其實這里不僅僅涉及到作用域,作用域鏈還涉及到Event Loop、微任務、宏任務。但是在這里不講這些。
下面我們先解釋它為什么會輸出 5個5,然后再用自執行函數來修改它,以達到我們預期的結果。
提示:for 循環中,每一次的都聲明一個同名變量,下一個變量的值為上一次循環執行完同名變量的值。
首先用var聲明變量 for 是不會產生塊級作用域的,所以在 () 中聲明的 i 為全局變量。相當于:
// 偽代碼 var i; for(i=0;i<5;i++) {
setTimeout(function() { console.log(i);
},1000)
}
setTimeout中的第一個參數為一個全局的匿名函數。相當于:
// 偽代碼 var i; var f = function() { console.log(i);
} for(i=0;i<5;i++) {
setTimeout(f,1000)
}
由于setTimeout是在1秒之后執行的,這個時候for循環已經執行完畢,此時的全局變量 i 已經變成了 5 。1秒后5個setTimeout中的匿名函數會同時執行,也就是5個 f 函數執行。這個時候 f 函數使用的變量 i 根據作用域鏈的查找規則找到了全局作用域中的 i 。因此會輸出 5 個5。
那我們怎樣來修改它呢?
for(var i=0;i<5;i++) {
(function (){ setTimeout(function() { console.log(i);
},1000)
})();
}
上述例子會輸出我們期望的值嗎?答案是否。為什么呢?我們雖然把 setTimeout 包裹在一個匿名函數中了,但是當setTimeout中匿名函數執行時,首先去匿名函數中查找 i 的值,找不到還是會找到全局作用域中,最終 i 的值仍然是全局變量中的 i ,仍然為 5個5.
那我們把外層的匿名函數中聲明一個變量 j 讓setTimeout中的匿名函數訪問這個 j 不就找不到全局變量中的變量了嗎。
for(var i=0;i<5;i++) {
(function (){ var j = i;
setTimeout(function() { console.log(j);
},1000)
})();
}
這個時候才達到了我們預期的結果:0 1 2 3 4。
我們來優化一下:
for(var i=0;i<5;i++) {
(function (i){ setTimeout(function() { console.log(i);
},1000)
})(i);
}
*思路2:用 let 聲明變量,產生塊級作用域。
for(let i=0;i<5;i++) {
setTimeout(function() { console.log(i);
},1000)
}
這時for循環5次,產生 5 個塊級作用域,也會聲明 5 個具有塊級作用域的變量 i ,因此setTimeout中的匿名函數每次執行時,訪問的 i 都是當前塊級作用域中的變量 i 。
什么是理論中的閉包?就是看似像閉包,其實并不是閉包。它只是類似于閉包。
function foo() { var a=2; function bar() { console.log(a); // 2 }
bar();
}
foo();
上述代碼根據最上面我們對閉包的定義,它并不完全是閉包,雖然是一個函數可以訪問另一個函數中的變量,但是被嵌套的函數是在當前詞法作用域中被調用的。
我們怎樣把上述代碼foo 函數中的bar函數,在它所在的詞法作用域外執行呢?
下面的代碼就清晰的展示了閉包:
function foo() { var a=2; function bar() { console.log(a);
} return bar;
} var baz=foo();
baz(); // 2 —— 朋友,這就是閉包的效果。
上述代碼中 bar 被當做 foo函數返回值。foo函數執行后把返回值也就是 bar函數 賦值給了全局變量 baz。當 baz 執行時,實際上也就是 bar 函數的執行。我們知道 foo 函數在執行后,foo 的內部作用域會被銷毀,因為引擎有垃圾回收期來釋放不再使用的內存空間。所以在bar函數執行時,實際上foo函數內部的作用域已經不存在了,理應來說 bar函數 內部再訪問 a 變量時是找不到的。但是閉包的神奇之處就在這里。由于 bar 是在 foo 作用域中被聲明的,所以 bar函數 會一直保存著對 foo 作用域的引用。這時就形成了閉包。
我們先看個例子:
var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope;
} return f;
} var foo = checkscope();
foo();
我們用偽代碼來解釋JavaScript
引擎在執行上述代碼時的步驟:
JavaScript
引擎遇到可執行代碼時,就會進入一個執行上下文(環境)
但是我們想一個問題,checkscope函數執行完畢,它的執行上下文從棧中彈出,也就是銷毀了不存在了,f 函數還能訪問包裹函數的作用域中的變量(scope)嗎?答案是可以。
理由是在第6步,我們說過當checkscope 執行函數執行完畢時,它的執行上下文會從棧中彈出,此時活動對象也會被回收,按理說當 f 在訪問checkscope的活動對象時是訪問不到的。
其實這里還有個概念,叫做作用域鏈:當 checkscope 函數被創建時,會創建對應的作用域鏈,里面值存放著包裹它的作用域對應執行上下文的變量對象,在這里只是全局執行上下文的變量對象,當checkscope執行時,此時的作用域鏈變化了 ,里面存放的是變量對象(活動對象)的集合,最頂端是當前函數的執行上下文的活動對象。端是全局執行上下文的變量對象。類似于:
checkscope.scopeChain = [
checkscope.AO
global.VO
]
當checkscope執行碰到了 f 函數的創建,因此 f 函數也會創建對應的作用域鏈,默認以包裹它的函數執行時對應的作用域鏈為基礎。因此此時 f 函數創建時的作用域鏈如下:
checkscope.scopeChain = [
checkscope.AO
global.VO
]
當 f 函數執行時,此時的作用域鏈變化如下:
checkscope.scopeChain = [
f.AO
checkscope.AO
global.VO
]
當checkscope函數執行完畢,內部作用域會被回收,但是 f函數 的作用域鏈還是存在的,里面存放著 checkscope函數的活動對象,因此在f函數執行時會從作用域鏈中查找內部使用的 scope 標識符,從而在作用域鏈的第二位找到了,也就是在 checkscope.AO 找到了變量scope的值。
正是因為JavaScript
做到了這一點,因此才會有閉包的概念。還有人說閉包并不是為了擁有它采取設計它的,而是設計作用域鏈時的副作用產物。
閉包是JavaScript
中最難的點,也是平常面試中常問的問題,我們必須要真正的去理解它,如果只靠死記硬背是經不起考驗的。
this
this是我們在書寫代碼時最常用的關鍵詞之一,即使如此,它也是JavaScript最容易被最頭疼的關鍵詞。那么this到底是什么呢?
如果你了解執行上下文,那么你就會知道,其實this是執行上下文對象的一個屬性:
executionContext = {
scopeChain:[ ... ],
VO:{
...
},
this: ?
}
執行上下文中有三個重要的屬性,作用域鏈(scopeChain)、變量對象(VO)和this。
this是在進入執行上下文時確定的,也就是在函數執行時才確定,并且在運行期間不允許修改并且是永久不變的
在全局代碼中的this
在全局代碼中this 是不變的,this始終是全局對象本身。
var a = 10;
this.b = 20;
window.c = 30;
console.log(this.a);
console.log(b);
console.log(this.c);
console.log(this === window) // true
// 由于this就是全局對象window,所以上述 a ,b ,c 都相當于在全局對象上添加相應的屬性
如果我們在代碼運行期嘗試修改this的值,就會拋出錯誤:
this = { a : 1 } ; // Uncaught SyntaxError: Invalid left-hand side in assignment
console.log(this === window) // true
函數代碼中的this
在函數代碼中使用this,才是令我們最容易困惑的,這里我們主要是對函數代碼中的this進行分析。
我們在上面說過this的值是,進入當前執行上下文時確定的,也就是在函數執行時并且是執行前確定的。但是同一個函數,作用域中的this指向可能完全不同,但是不管怎樣,函數在運行時的this的指向是不變的,而且不能被賦值。
function foo() {
console.log(this);
}
foo(); // window
var obj={
a: 1,
bar: foo,
}
obj.bar(); // obj
函數中this的指向豐富的多,它可以是全局對象、當前對象、或者是任意對象,當然這取決于函數的調用方式。在JavaScript中函數的調用方式有一下幾種方式:作為函數調用、作為對象屬性調用、作為構造函數調用、使用apply或call調用。下面我們將按照這幾種調用方式一一討論this的含義。
作為函數調用
什么是作為函數調用:就是獨立的函數調用,不加任何修飾符。
function foo(){
console.log(this === window); // true
this.a = 1;
console.log(b); // 2
}
var b = 2;
foo();
console.log(a); // 1
上述代碼中this綁定到了全局對象window。this.a相當于在全局對象上添加一個屬性 a 。
在嚴格模式下,獨立函數調用,this的綁定不再是window,而是undefined。
function foo() {
"use strict";
console.log(this===window); // false
console.log(this===undefined); // true
}
foo();
這里要注意,如果函數調用在嚴格模式下,而內部代碼執行在非嚴格模式下,this 還是會默認綁定為 window。
function foo() {
console.log(this===window); // true
}
(function() {
"use strict";
foo();
})()
對于在函數內部的函數獨立調用 this 又指向了誰呢?
function foo() {
function bar() {
this.a=1;
console.log(this===window); // true
}
bar()
}
foo();
console.log(a); // 1
上述代碼中,在函數內部的函數獨立調用,此時this還是被綁定到了window。
總結:當函數作為獨立函數被調用時,內部this被默認綁定為(指向)全局對象window,但是在嚴格模式下會有區別,在嚴格模式下this被綁定為undefined。
作為對象屬性調用
var a=1;
var obj={
a: 2,
foo: function() {
console.log(this===obj); // true
console.log(this.a); // 2
}
}
obj.foo();
上述代碼中 foo屬性的值為一個函數。這里稱 foo 為 對象obj 的方法。foo的調用方式為 對象 . 方法 調用。此時 this 被綁定到當前調用方法的對象。在這里為 obj 對象。
再看一個例子:
var a=1;
var obj={
a: 2,
bar: {
a: 3,
foo: function() {
console.log(this===bar); // true
console.log(this.a); // 3
}
}
}
obj.bar.foo();
遵循上面說的規則 對象 . 屬性 。這里的對象為 obj.bar 。此時 foo 內部this被綁定到了 obj.bar 。 因此 this.a 即為 obj.bar.a 。
再來看一個例子:
var a=1;
var obj={
a: 2,
foo: function() {
console.log(this===obj); // false
console.log(this===window); // true
console.log(this.a); // 1
}
}
var baz=obj.foo;
baz();
這里 foo 函數雖然作為對象obj 的方法。但是它被賦值給變量 baz 。當baz調用時,相當于 foo 函數獨立調用,因此內部 this被綁定到 window。
使用apply或call調用
apply和call為函數原型上的方法。它可以更改函數內部this的指向。
var a=1;
function foo() {
console.log(this.a);
}
var obj1={
a: 2
}
var obj2={
a: 3
}
var obj3={
a: 4
}
var bar=foo.bind(obj1);
bar();// 2 this => obj1
foo(); // 1 this => window
foo.call(obj2); // 3 this => obj2
foo.call(obj3); // 4 this => obj3
當函數foo 作為獨立函數調用時,this被綁定到了全局對象window,當使用bind、call或者apply方法調用時,this 被分別綁定到了不同的對象。
作為構造函數調用
var a=1;
function Person() {
this.a=2; // this => p;
}
var p=new Person();
console.log(p.a); // 2
上述代碼中,構造函數 Person 內部的 this 被綁定為 Person的一個實例。
總結:
當我們要判斷當前函數內部的this綁定,可以依照下面的原則:
函數是否在是通過 new 操作符調用?如果是,this 綁定為新創建的對象
var bar = new foo(); // this => bar;
函數是否通過call或者apply調用?如果是,this 綁定為指定的對象
foo.call(obj1); // this => obj1;
foo.apply(obj2); // this => obj2;
函數是否通過 對象 . 方法調用?如果是,this 綁定為當前對象
obj.foo(); // this => obj;
函數是否獨立調用?如果是,this 綁定為全局對象。
foo(); // this => window
DOM事件處理函數中的this
1). 事件綁定
<button id="btn">點擊我</button>
// 事件綁定
function handleClick(e) {
console.log(this); // <button id="btn">點擊我</button>
}
document.getElementById('btn').addEventListener('click',handleClick,false); // <button id="btn">點擊我</button>
document.getElementById('btn').onclick= handleClick; // <button id="btn">點擊我</button>
根據上述代碼我們可以得出:當通過事件綁定來給DOM元素添加事件,事件將被綁定為當前DOM對象。
2).內聯事件
<button onclick="handleClick()" id="btn1">點擊我</button>
<button onclick="console.log(this)" id="btn2">點擊我</button>
function handleClick(e) {
console.log(this); // window
}
//第二個 button 打印的是 <button id="btn">點擊我</button>
我認為內聯事件可以這樣理解:
//偽代碼
<button onclick=function(){ handleClick() } id="btn1">點擊我</button>
<button onclick=function() { console.log(this) } id="btn2">點擊我</button>
這樣我們就能理解上述代碼中為什么內聯事件一個指向window,一個指向當前DOM元素。(當然瀏覽器處理內聯事件時并不是這樣的)
定時器中的this
定時器中的 this 指向哪里呢?
function foo() {
setTimeout(function() {
console.log(this); // window
},1000)
}
foo();
再來看一個例子
var name="chen";
var obj={
name: "erdong",
foo: function() {
console.log(this.name); // erdong
setTimeout(function() {
console.log(this.name); // chen
},1000)
}
}
obj.foo();
到這里我們可以看到,函數 foo 內部this指向為調用它的對象,即:obj 。定時器中的this指向為 window。那么有什么辦法讓定時器中的this跟包裹它的函數綁定為同一個對象呢?
1). 利用閉包:
var name="chen";
var obj={
name: "erdong",
foo: function() {
console.log(this.name) // erdong
var that=this;
setTimeout(function() {
// that => obj
console.log(that.name); // erdong
},1000)
}
}
obj.foo();
利用閉包的特性,函數內部的函數可以訪問含義訪問當前詞法作用域中的變量,此時定時器中的 that 即為包裹它的函數中的 this 綁定的對象。在下面我們會介紹利用 ES6的箭頭函數實現這一功能。
當然這里也可以適用bind來實現:
var name="chen";
var obj={
name: "erdong",
foo: function() {
console.log(this.name); // erdong
setTimeout(function() {
// this => obj
console.log(this.name); // erdong
}.bind(this),1000)
}
}
obj.foo();
被忽略的this
如果你把 null 或者 undefined 作為 this 的綁定對象傳入 call 、apply或者bind,這些值在調用時會被忽略,實例 this 被綁定為對應上述規則。
var a=1;
function foo() {
console.log(this.a); // 1 this => window
}
var obj={
a: 2
}
foo.call(null);
var a=1;
function foo() {
console.log(this.a); // 1 this => window
}
var obj={
a: 2
}
foo.apply(null);
var a=1;
function foo() {
console.log(this.a); // 1 this => window
}
var obj={
a: 2
}
var bar = foo.bind(null);
bar();
bind 也可以實現函數柯里化:
function foo(a,b) {
console.log(a,b); // 2 3
}
var bar=foo.bind(null,2);
bar(3);
更復雜的例子:
var foo={
bar: function() {
console.log(this);
}
};
foo.bar(); // foo
(foo.bar)(); // foo
(foo.bar=foo.bar)(); // window
(false||foo.bar)(); // window
(foo.bar,foo.bar)(); // window
上述代碼中:
foo.bar()為對象的方法調用,因此 this 綁定為 foo 對象。
(foo.bar)() 前一個() 中的內容不計算,因此還是 foo.bar()
(foo.bar=foo.bar)() 前一個 () 中的內容計算后為 function() { console.log(this); } 所以這里為匿名函數自執行,因此 this 綁定為 全局對象 window
后面兩個實例同上。
這樣理解會比較好:
(foo.bar=foo.bar) 括號中的表達式執行為 先計算,再賦值,再返回值。
(false||foo.bar)() 括號中的表達式執行為 判斷前者是否為 true ,若為true,不計算后者,若為false,計算后者并返回后者的值。
(foo.bar,foo.bar) 括號中的表達式之行為分別計算 “,” 操作符兩邊,然后返回 “,” 操作符后面的值。
箭頭函數中的this
箭頭函數時ES6新增的語法。
有兩個作用:
更簡潔的函數
本身不綁定this
代碼格式為:
// 普通函數
function foo(a){
// ......
}
//箭頭函數
var foo = a => {
// ......
}
//如果沒有參數或者參數為多個
var foo = (a,b,c,d) => {
// ......
}
我們在使用普通函數之前對于函數的this綁定,需要根據這個函數如何被調用來確定其內部this的綁定對象。而且常常因為調用鏈的數量或者是找不到其真正的調用者對 this 的指向模糊不清。在箭頭函數出現后其內部的 this 指向不需要再依靠調用的方式來確定。
箭頭函數有幾個特點(與普通函數的區別)
箭頭函數不綁定 this 。它只會從作用域鏈的上一層繼承 this。
箭頭函數不綁定arguments,使用reset參數來獲取實參的數量。
箭頭函數是匿名函數,不能作為構造函數。
箭頭函數沒有prototype屬性。
不能使用 yield 關鍵字,因此箭頭函數不能作為函數生成器。
這里我們只討論箭頭函數中的this綁定。
用一個例子來對比普通函數與箭頭函數中的this綁定:
var obj={
foo: function() {
console.log(this); // obj
},
bar: () => {
console.log(this); // window
}
}
obj.foo();
obj.bar();
上述代碼中,同樣是通過對象 . 方法調用一個函數,但是函數內部this綁定確是不同,只因一個數普通函數一個是箭頭函數。
用一句話來總結箭頭函數中的this綁定:
個人上面說的它會從作用域鏈的上一層繼承 this ,說法并不是很正確。作用域中存放的是這個函數當前執行上下文與所有父級執行上下文的變量對象的集合。因此在作用域鏈中并不存在 this 。應該說是作用域鏈上一層對應的執行上下文中繼承 this 。
箭頭函數中的this繼承于作用域鏈上一層對應的執行上下文中的this
var obj={
foo: function() {
console.log(this); // obj
},
bar: () => {
console.log(this); // window
}
}
obj.bar();
上述代碼中obj.bar執行時的作用域鏈為:
scopeChain = [
obj.bar.AO,
global.VO
]
根據上面的規則,此時bar函數中的this指向為全局執行上下文中的this,即:window。
再來看一個例子:
var obj={
foo: function() {
console.log(this); // obj
var bar=() => {
console.log(this); // obj
}
bar();
}
}
obj.foo();
在普通函數中,bar 執行時內部this被綁定為全局對象,因為它是作為獨立函數調用。但是在箭頭函數中呢,它卻綁定為 obj 。跟父級函數中的 this 綁定為同一對象。
此時它的作用域鏈為:
scopeChain = [
bar.AO,
obj.foo.AO,
global.VO
]
這個時候我們就差不多知道了箭頭函數中的this綁定。
繼續看例子:
var obj={
foo: () => {
console.log(this); // window
var bar=() => {
console.log(this); // window
}
bar();
}
}
obj.foo();
這個時候怎么又指向了window了呢?
我們還看當 bar 執行時的作用域鏈:
scopeChain = [
bar.AO,
obj.foo.AO,
global.VO
]
當我們找bar函數中的this綁定時,就會去找foo函數中的this綁定。因為它是繼承于它的。這時 foo 函數也是箭頭函數,此時foo中的this綁定為window而不是調用它的obj對象。因此 bar函數中的this綁定也為全局對象window。
我們在回頭看上面關于定時器中的this的例子:
var name="chen";
var obj={
name: "erdong",
foo: function() {
console.log(this.name); // erdong
setTimeout(function() {
console.log(this); // chen
},1000)
}
}
obj.foo();
這時我們就可以很簡單的讓定時器中的this與foo中的this綁定為同一對象:
var name="chen";
var obj={
name: "erdong",
foo: function() {
// this => obj
console.log(this.name); // erdong
setTimeout(() => {
// this => foo中的this => obj
console.log(this.name); // erdong
},1000)
}
}
obj.foo();
藍藍設計的小編 http://www.syprn.cn