閉包的定義:函數 A 返回了一個函數 B,而且函數 B 中使用了函數 A 的變量,函數 B 就被稱爲閉包。javascript
閉包是指有權訪問另外一個函數做用域中變量的函數.html
function A() {
let a = 1
function B() {
console.log(a)
}
return B
}
複製代碼
爲何函數 B 還能引用到函數 A 中的變量?java
由於函數 A 中的變量這時候是存儲在堆上的。如今的 JS 引擎能夠經過逃逸分析辨別出哪些變量須要存儲在堆上,哪些須要存儲在棧上。面試
函數生命週期圖示:編程
閉包做用:數組
(目前瀏覽器引擎都基於V8,V8有gc回收機制,不用太擔憂變量不會被回收)瀏覽器
使用閉包的注意點:緩存
因爲閉包會使得函數中的變量都被保存在內存中,內存消耗很大,因此不能濫用閉包
,不然會形成網頁的性能問題,在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 定義函數的問題解決辦法:
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(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 );
}
複製代碼
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]]])
展開運算符(...)
經過展開運算符
(...)
, 以更加簡潔的形式將一個對象的可枚舉屬性拷貝至另外一個對象。 須要轉換編譯器才能將對象展開運算符應用在生產環境中, 如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 的時候,該對象不能正常的序列化, 此時可使用 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
由 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(異步模塊定義,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的誕生,解決了兩個問題:
//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;
};
};
複製代碼