閉包
閉包的定義:函式 A 返回了一個函式 B,並且函式 B 中使用了函式 A 的變數,函式 B 就被稱為閉包。
閉包是指有權訪問另一個函式作用域中變數的函式.
function A() {
let a = 1
function B() {
console.log(a)
}
return B
}
複製程式碼
為什麼函式 B 還能引用到函式 A 中的變數?
因為函式 A 中的變數這時候是儲存在堆上的。現在的 JS 引擎可以通過逃逸分析辨別出哪些變數需要儲存在堆上,哪些需要儲存在棧上。
函式生命週期圖示:
閉包作用:
- 讀取函式內部的變數, 內部函式也可以引用外層的引數和變數
- 這些變數的值始終保持在記憶體中, 不會被垃圾回收機制回收 原因:f1是f2的父函式,而f2被賦給了一個全域性變數,這導致f2始終在記憶體中,而f2的存在依賴於f1,因此f1也始終在記憶體中,不會在呼叫結束後,被垃圾回收機制(garbage collection)回收。
(目前瀏覽器引擎都基於V8,V8有gc回收機制,不用太擔心變數不會被回收)
- 可用於setTimeout/setInterval、回撥函式(callback)、事件控制程式碼(event handle)
- 利用閉包可以長久地儲存變數又避免全域性汙染
使用閉包的注意點:
-
由於閉包會使得函式中的變數都被儲存在記憶體中,記憶體消耗很大,所以
不能濫用閉包
,否則會造成網頁的效能問題,在IE中可能導致記憶體洩露。 解決方法: 在退出函式之前,將不使用的區域性變數全部刪除。 -
閉包會在父函式外部,改變父函式內部變數的值。 如果你把父函式當作物件使用,把閉包當作它的公用方法,把內部變數當作它的私有屬性,
不要隨便改變父函式內部變數的值
。
面試題:迴圈中使用閉包解決 var 定義函式的問題?
for ( var i=0; i<5; i++) {
setTimeout( function timer() {
console.log( i );
}, 1000 );
}
//setTimeout 是個非同步函式,所有會先把迴圈全部執行完畢,這時候 i 就是 5 了,所以會輸出6個 5。
複製程式碼
如果用箭頭表示其前後的兩次輸出之間有 1 秒的時間間隔,而逗號表示其前後的兩次輸出之間的時間間隔可以忽略,程式碼實際執行的結果該如何描述?
5 -> 5, 5, 5, 5, 5
//迴圈執行過程中,幾乎同時設定了 5 個定時器,一般情況下,這些定時器都會在 1 秒之後觸發,而迴圈完的輸出是立即執行的。
複製程式碼
setTimeout註冊的函式fn會交給瀏覽器的定時器模組來管理,延遲時間到了就將fn加入主程式執行佇列,如果佇列前面還有沒有執行完的程式碼,則又需要花一點時間等待才能執行到fn,所以實際的延遲時間會比設定的長。
setInterval並不管上一次fn的執行結果,而是每隔100ms就將fn放入主執行緒佇列,而兩次fn之間具體間隔和JS執行情況有關。
迴圈中使用閉包解決 var 定義函式的問題解決辦法:
- 使用閉包 直接使用匿名函式的立即執行模式。立即執行函式保證了每個函式中的i是當時迴圈到的i值,即使迴圈結束,i 值在迴圈中已經以副本形式確定下來了。
for (var i = 0; i < 5; i++) {
(function(j) { //j = i
setTimeout(function timer() {
console.log(j);
}, 1000);
})(i); //5 -> 0,1,2,3,4
}
複製程式碼
- 修改迴圈體
var output = function (i) {
setTimeout(function() {
console.log(new Date, i);
}, 1000);
};
for (var i = 0; i < 5; i++) {
output(i); // 這裡傳過去的 i 值被複制了
}
複製程式碼
- 使用 setTimeout 的第三個引數
setTimeout(function[, delay, param1, param2, ...])
param1, param2, ...作為前面回撥函式的附加引數。
for ( var i=1; i<=5; i++) {
setTimeout( function timer(j) {
console.log( j );
}, i*1000, i);
}
複製程式碼
- 使用
let
定義 i ,因為let
會建立一個塊級作用域,迴圈中不會共享一個 i 值。
for ( let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
複製程式碼
- (補充)程式碼執行時,立即輸出 0,之後每隔 1 秒依次輸出 1,2,3,4,迴圈結束後在大概第 5 秒的時候輸出 5。
zhuanlan.zhihu.com/p/25855075
Promise
非同步解決方案
const tasks = []; // 這裡存放非同步操作的 Promise
const output = (i) => new Promise((resolve) => {
setTimeout(() => {
console.log(new Date, i);
resolve();
}, 1000 * i);
});
// 生成全部的非同步操作
for (var i = 0; i < 5; i++) {
tasks.push(output(i));
}
// 非同步操作完成之後,輸出最後的 i
Promise.all(tasks).then(() => {
setTimeout(() => {
console.log(new Date, i);
}, 1000);
});
複製程式碼
如何使用 ES7 中的 async await
特性來讓這段程式碼變的更簡潔?
// 模擬其他語言中的 sleep,實際上可以是任何非同步操作
const sleep = (timeountMS) => new Promise((resolve) => {
setTimeout(resolve, timeountMS);
});
(async () => { // 宣告即執行的 async 函式表示式
for (var i = 0; i < 5; i++) {
await sleep(1000);
console.log(new Date, i);
}
await sleep(1000);
console.log(new Date, i);
})();
複製程式碼
深淺拷貝
- 值型別和引用型別:
值型別:String(字串),Number(數值),Boolean(布林值),Undefined,Null 引用型別:Array(陣列),Object(物件),Function(函式)
- 值型別的變數會儲存在棧記憶體中; 引用型別的變數名儲存在棧記憶體中,實際上是一個存放在棧記憶體的指標,這個指標指向堆記憶體中的地址,變數值存放在堆記憶體中。
棧(stack)為自動分配的記憶體空間,它由系統自動釋放;而堆(heap)則是動態分配的記憶體,大小不定也不會自動釋放。 記憶體中的棧區域存放變數以及指向堆區域儲存位置的指標,內容存放在堆中。
- 基本型別的比較是值的比較,引用型別的比較是引用的比較。
var aa = 1;
var bb = 1;
console.log(aa === bb);//true
//比較的時候最好使用嚴格等,因為 == 會進行型別轉換
var a = [1,2,3];
var b = [1,2,3];
console.log(a === b); // false
//雖然變數 a 和變數 b 都是表示一個內容為 1,2,3 的陣列,但是其在記憶體中的位置不一樣,也就是說變數 a 和變數 b 指向的不是同一個物件,所以他們是不相等的。
複製程式碼
- 賦值:
基本型別的賦值是傳值: 在記憶體中新開闢一段棧記憶體,然後再將值賦值到新的棧中。所以基本型別賦值的兩個變數是兩個獨立相互不影響的變數。
引用型別的賦值是傳址: 只是改變指標的指向,也就是說引用型別的賦值是物件儲存在棧中的地址的賦值,這樣的話兩個變數就指向同一個物件,因此兩者之間操作互相有影響。
淺拷貝:
淺拷貝: 重新在堆中建立記憶體,拷貝前後物件的基本資料型別互不影響。只拷貝一層,不能對物件中的子物件進行拷貝。
賦值和淺拷貝的區別:
var obj1 = { //原始資料
'name' : 'zhangsan',
'age' : '18',
'language' : [1,[2,3],[4,5]],
};
var obj2 = obj1; //賦值操作
var obj3 = shallowCopy(obj1); //淺拷貝
function shallowCopy(src) {
var dst = {};
for (var prop in src) {
if (src.hasOwnProperty(prop)) {
dst[prop] = src[prop];
}
}
return dst;
}
obj2.name = "lisi";
obj3.age = "20";
obj2.language[1] = ["二","三"];
obj3.language[2] = ["四","五"];
console.log(obj1);
//obj1 = {
// 'name' : 'lisi',
// 'age' : '18',
// 'language' : [1,["二","三"],["四","五"]],
//};
console.log(obj2);
//obj2 = {
// 'name' : 'lisi',
// 'age' : '18',
// 'language' : [1,["二","三"],["四","五"]],
//};
console.log(obj3);
//obj3 = {
// 'name' : 'zhangsan',
// 'age' : '20',
// 'language' : [1,["二","三"],["四","五"]],
//};
複製程式碼
改變 obj2 的 name 屬性和 obj3 的 age 屬性,可以看到,改變賦值得到的物件 obj2 同時也會改變原始值 obj1,而改變淺拷貝得到的的 obj3 則不會改變原始物件 obj1。
說明賦值得到的物件 obj2 只是將指標改變,其引用的仍然是同一個物件,而淺拷貝得到的的 obj3 則是重新建立了新物件。
改變了賦值得到的物件 obj2 和淺拷貝得到的 obj3 中的 language 屬性的第二個值和第三個值(language 是一個陣列,也就是引用型別)。 結果可見,無論是修改賦值得到的物件 obj2 和淺拷貝得到的 obj3 都會改變原始資料。
淺拷貝只複製一層物件的屬性,並不包括物件裡面的為引用型別的資料。所以就會出現改變淺拷貝得到的 obj3 中的引用型別時,會使原始資料得到改變。
總結:
淺拷貝實現方案:
- 通過
Object.assign(target,source1,source2,source3);
Object.assign 是ES6新新增的介面,用於將所有可列舉屬性的值從一個或多個源物件複製到目標物件。它將返回目標物件。
let a = {
age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1
複製程式碼
- 通過
Array.prototype.slice(begin, end)
slice() 方法返回一個新的陣列物件,這一物件是一個由 begin 和 end(不包括end)決定的原陣列的淺拷貝。
- 通過
Array.prototype.concat()
concat() 方法用於合併多個陣列,並返回一個新的陣列,和slice方法類似。 var new_array = old_array.concat(value1[, value2[, ...[, valueN]]])
- ES6中的
展開運算子(...)
通過展開運算子
(...)
, 以更加簡潔的形式將一個物件的可列舉屬性拷貝至另一個物件。 需要轉換編譯器才能將物件展開運算子應用在生產環境中, 如Babel
。
Babel
是一個廣泛使用的轉碼器,可以將ES6程式碼轉為ES5程式碼,從而在現有環境執行。
替代函式的apply方法: 不再需要apply方法,將陣列轉為函式的引數了
// ES5 的寫法
function f(x, y, z) {
// ...
}
var args = [0, 1, 2];
f.apply(null, args);
// ES6的寫法
let args = [0, 1, 2];
f(...args);
複製程式碼
使用展開運算子複製陣列/合併陣列/......
//複製陣列
const a1 = [1, 2];
const a2 = [...a1]; // 寫法一
const [...a2] = a1; // 寫法二
//合併陣列
const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];
arr1.concat(arr2, arr3); // ES5 的合併陣列
[...arr1, ...arr2, ...arr3] // ES6 的合併陣列
複製程式碼
深拷貝:
深拷貝: 對物件以及物件中的所有子物件進行遞迴拷貝,拷貝前後的兩個物件互不影響。
如何實現深拷貝:遞迴呼叫剛剛的淺拷貝,把所有屬於物件的屬性型別都遍歷賦給另一個物件。
JSON.parse(JSON.stringify(object))
:通常情況下,複雜資料都是可以序列化的,所以這個函式可以解決大部分問題,並且該函式是內建函式中處理深拷貝效能最快的。
JSON.stringify()
方法是將JavaScript值(物件或者陣列)轉換為JSON字串"{"a":1,"b":2}"。JSON.parse()
方法用來解析JSON字串,構造出JSON物件age:"23" name:"lisa"。
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
複製程式碼
侷限性:
- 忽略 undefined 和 symbol
- 不能序列化函式
- 不能解決迴圈引用的物件
在遇到函式、 undefined 或者 symbol 的時候,該物件不能正常的序列化, 此時可以使用 lodash 的深拷貝函式
。
如果你所需拷貝的物件含有內建型別並且不包含函式,可以使用 MessageChannel
。
function structuralClone(obj) {
return new Promise(resolve => {
const {port1, port2} = new MessageChannel();
port2.onmessage = ev => resolve(ev.data);
port1.postMessage(obj);
});
}
var obj = {a: 1, b: {
c: b
}}
// 注意該方法是非同步的
// 可以處理 undefined 和迴圈引用物件
(async () => {
const clone = await structuralClone(obj)
})()
複製程式碼
自己實現一個深拷貝:
//通過對需要拷貝的物件的屬性進行遞迴遍歷,如果物件的屬性不是基本型別時,就繼續遞迴,知道遍歷到物件屬性為基本型別,然後將屬性和屬性值賦給新物件.
function copy(obj) {
if (!obj || typeof obj !== 'object') {
return
}
var newObj = obj.constructor === Array ? [] : {}
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
if (typeof obj[key] === 'object' && obj[key]) {
newObj[key] = copy(obj[key])
} else {
newObj[key] = obj[key]
}
}
}
return newObj
}
var old = {a: 'old', b: {c: 'old'}}
var newObj = copy(old)
newObj.b.c = 'new'
console.log(old) // { a: 'old', b: { c: 'old' } }
console.log(newObj) // { a: 'old', b: { c: 'new' } }
複製程式碼
模組化
原始寫法:使用"立即執行函式"(Immediately-Invoked Function Expression,IIFE),可以達到不暴露私有成員的目的。
var module1 = (function(){
    var _count = 0;
    var m1 = function(){
      //...
    };
    var m2 = function(){
      //...
    };
    return {
      m1 : m1,
      m2 : m2
    };
  })();
複製程式碼
在有 Babel 的情況下,可以直接使用 ES6 的模組化。
// file a.js
export function a() {}
export function b() {}
// file b.js
export default function() {}
import {a, b} from './a.js'
import XXX from './b.js'
複製程式碼
CommonJS
CommonJs
由 BravoJS 提出,是 Node 獨有的規範,瀏覽器中使用就需要用Browserify
解析。
// a.js
module.exports = {
a: 1
}
// or
exports.a = 1
// 不能對 exports 直接賦值
// b.js
var module = require('./a.js')
module.a // -> log 1
複製程式碼
CommonJS 和 ES6 中的模組化的兩者區別:
- 前者支援動態匯入,也就是 require(${path}/xx.js),後者目前不支援,但是已有提案
- 前者是同步匯入,因為用於服務端,檔案都在本地,同步匯入即使卡住主執行緒影響也不大。而後者是非同步匯入,因為用於瀏覽器,需要下載檔案,如果也採用同步匯入會對渲染有很大影響
- 前者在匯出時都是值拷貝,就算匯出的值變了,匯入的值也不會改變,所以如果想更新值,必須重新匯入一次。但是後者採用實時繫結的方式,匯入匯出的值都指向同一個記憶體地址,所以匯入值會跟隨匯出值變化
- 後者會編譯成 require/exports 來執行的
侷限:
因為呼叫模組提供的方法需要等待模組載入完成,對於瀏覽器來說,模組都放在伺服器端,等待時間取決於網速的快慢,可能要等很長時間,瀏覽器會處於"假死"狀態。所以CommonJS
不適用於瀏覽器環境。
因此,瀏覽器端的模組,不能採用"同步載入"(synchronous),只能採用"非同步載入"(asynchronous)。這就是AMD
規範誕生的背景。
AMD
AMD(非同步模組定義,Asynchronous Module Definition) 是由 RequireJS 提出的。 CMD 是由 SeaJS 提出的。
AMD採用非同步方式載入模組,模組的載入不影響它後面語句的執行。所有依賴這個模組的語句,都定義在一個回撥函式中,等到載入完成之後,這個回撥函式才會執行。
// CommonJS
var math = require('math');
math.add(2, 3);
//AMD
//require()第一個引數[module],是一個陣列,裡面的成員就是要載入的模組;第二個引數callback,則是載入成功之後的回撥函式。
require(['math'], function (math) {
    math.add(2, 3);
  });
複製程式碼
目前,主要有兩個Javascript庫實現了AMD規範:require.js
和 curl.js
。
http://www.ruanyifeng.com/blog/2012/11/require_js.html
介紹require.js,進一步講解AMD的用法,以及如何將模組化程式設計投入實戰。
require.js的誕生,解決了兩個問題:
- 實現js檔案的非同步載入,避免網頁失去響應;
- 管理模組之間的依賴性,便於程式碼的編寫和維護。
//require.js的非同步載入
<script src="js/require.js" defer async="true" ></script>
//載入自己的程式碼檔案。data-main屬性的作用是,指定網頁程式的主模組
<script src="js/require.js" data-main="js/main"></script>
複製程式碼
主模組的寫法:
require(['moduleA', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){
    // some code here
  });
複製程式碼
require()函式接受兩個引數。第一個引數是一個陣列,表示所依賴的模組,上例就是['moduleA', 'moduleB', 'moduleC'],即主模組依賴這三個模組;第二個引數是一個回撥函式,當前面指定的模組都載入成功後,它將被呼叫。載入的模組會以引數形式傳入該函式,從而在回撥函式內部就可以使用這些模組。
防抖和節流
防抖
PS:防抖和節流的作用都是防止函式多次呼叫。
區別在於,假設一個使用者一直觸發這個函式,且每次觸發函式的間隔小於wait,防抖只會呼叫一次,而節流會每隔一定時間(引數wait)呼叫函式。
袖珍版防抖實現,只能在最後呼叫:
// func是使用者傳入需要防抖的函式
// wait是等待時間
const debounce = (func, wait = 50) => {
// 快取一個定時器id
let timer = 0
// 這裡返回的函式是每次使用者實際呼叫的防抖函式
// 如果已經設定過定時器了就清空上一次的定時器
// 開始一個新的定時器,延遲執行使用者傳入的方法
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
// 不難看出如果使用者呼叫該函式的間隔小於wait,上一次的時間還未到就被清除了,並不會執行函式
複製程式碼
這是一個簡單版的防抖,但是有缺陷,這個防抖只能在最後呼叫。一般的防抖會有immediate選項,表示是否立即呼叫。這兩者的區別,舉個栗子來說:
-
例如在搜尋引擎搜尋問題的時候,我們當然是希望使用者輸入完最後一個字才呼叫查詢介面,這個時候適用
延遲執行的防抖函式
,它總是在一連串(間隔小於wait的)函式觸發之後呼叫。 -
例如使用者給interviewMap點star的時候,我們希望使用者點第一下的時候就去呼叫介面,並且成功之後改變star按鈕的樣子,使用者就可以立馬得到反饋是否star成功了,這個情況適用
立即執行的防抖函式
,它總是在第一次呼叫,並且下一次呼叫必須與前一次呼叫的時間間隔大於wait才會觸發。
帶有立即執行的防抖函式:
// 這個是用來獲取當前時間戳的
function now() {
return +new Date()
}
/**
* 防抖函式,返回函式連續呼叫時,空閒時間必須大於或等於 wait,func 才會執行
*
* @param {function} func 回撥函式
* @param {number} wait 表示時間視窗的間隔
* @param {boolean} immediate 設定為ture時,是否立即呼叫函式
* @return {function} 返回客戶呼叫函式
*/
function debounce (func, wait = 50, immediate = true) {
let timer, context, args
// 延遲執行函式
const later = () => setTimeout(() => {
// 延遲函式執行完畢,清空快取的定時器序號
timer = null
// 延遲執行的情況下,函式會在延遲函式中執行
// 使用到之前快取的引數和上下文
if (!immediate) {
func.apply(context, args)
context = args = null
}
}, wait)
// 這裡返回的函式是每次實際呼叫的函式
return function(...params) {
// 如果沒有建立延遲執行函式(later),就建立一個
if (!timer) {
timer = later()
// 如果是立即執行,呼叫函式
// 否則快取引數和呼叫上下文
if (immediate) {
func.apply(this, params)
} else {
context = this
args = params
}
// 如果已有延遲執行函式(later),呼叫的時候清除原來的並重新設定一個
// 這樣做延遲函式會重新計時
} else {
clearTimeout(timer)
timer = later()
}
}
}
複製程式碼
節流
防抖動是將多次執行變為最後一次執行,節流是將多次執行變成每隔一段時間執行。
/**
* underscore 節流函式,返回函式連續呼叫時,func 執行頻率限定為 次 / wait
*
* @param {function} func 回撥函式
* @param {number} wait 表示時間視窗的間隔
* @param {object} options 如果想忽略開始函式的的呼叫,傳入{leading: false}。
* 如果想忽略結尾函式的呼叫,傳入{trailing: false}
* 兩者不能共存,否則函式不能執行
* @return {function} 返回客戶呼叫函式
*/
_.throttle = function(func, wait, options) {
var context, args, result;
var timeout = null;
// 之前的時間戳
var previous = 0;
// 如果 options 沒傳則設為空物件
if (!options) options = {};
// 定時器回撥函式
var later = function() {
// 如果設定了 leading,就將 previous 設為 0
// 用於下面函式的第一個 if 判斷
previous = options.leading === false ? 0 : _.now();
// 置空一是為了防止記憶體洩漏,二是為了下面的定時器判斷
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function() {
// 獲得當前時間戳
var now = _.now();
// 首次進入前者肯定為 true
// 如果需要第一次不執行函式
// 就將上次時間戳設為當前的
// 這樣在接下來計算 remaining 的值時會大於0
if (!previous && options.leading === false) previous = now;
// 計算剩餘時間
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果當前呼叫已經大於上次呼叫時間 + wait
// 或者使用者手動調了時間
// 如果設定了 trailing,只會進入這個條件
// 如果沒有設定 leading,那麼第一次會進入這個條件
// 還有一點,你可能會覺得開啟了定時器那麼應該不會進入這個 if 條件了
// 其實還是會進入的,因為定時器的延時
// 並不是準確的時間,很可能你設定了2秒
// 但是他需要2.2秒才觸發,這時候就會進入這個條件
if (remaining <= 0 || remaining > wait) {
// 如果存在定時器就清理掉否則會呼叫二次回撥
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
// 判斷是否設定了定時器和 trailing
// 沒有的話就開啟一個定時器
// 並且不能不能同時設定 leading 和 trailing
timeout = setTimeout(later, remaining);
}
return result;
};
};
複製程式碼