摘要:如果你還沒讀過上篇上篇和中篇并無依賴關系,您可以讀過本文之后再閱讀上篇,可戳面試篇寒冬求職季之你必須要懂的原生上小姐姐花了近百個小時才完成這篇文章,篇幅較長,希望大家閱讀時多花點耐心,力求真正的掌握相關知識點。
一年前,也許你搞清楚閉包,this,原型鏈,就能獲得認可。但是現在,很顯然是不行了。本文梳理出了一些面試中有一定難度的高頻原生JS問題,部分知識點可能你之前從未關注過,或者看到了,卻沒有仔細研究,但是它們卻非常重要。
本文將以真實的面試題的形式來呈現知識點,大家在閱讀時,建議不要先看我的答案,而是自己先思考一番。盡管,本文所有的答案,都是我在翻閱各種資料,思考并驗證之后,才給出的(絕非復制粘貼而來)。但因水平有限,本人的答案未必是最優的,如果您有更好的答案,歡迎在 issue 中留言。
本文篇幅較長,但是滿滿的都是干貨!并且還埋伏了可愛的表情包,希望小伙伴們能夠堅持讀完。
寫文超級真誠的小姐姐祝愿大家都能找到心儀的工作。
如果你還沒讀過上篇【上篇和中篇并無依賴關系,您可以讀過本文之后再閱讀上篇】,可戳【面試篇】寒冬求職季之你必須要懂的原生JS(上)
小姐姐花了近百個小時才完成這篇文章,篇幅較長,希望大家閱讀時多花點耐心,力求真正的掌握相關知識點。
1.說一說JS異步發展史異步最早的解決方案是回調函數,如事件的回調,setInterval/setTimeout中的回調。但是回調函數有一個很常見的問題,就是回調地獄的問題(稍后會舉例說明);
為了解決回調地獄的問題,社區提出了Promise解決方案,ES6將其寫進了語言標準。Promise解決了回調地獄的問題,但是Promise也存在一些問題,如錯誤不能被try catch,而且使用Promise的鏈式調用,其實并沒有從根本上解決回調地獄的問題,只是換了一種寫法。
ES6中引入 Generator 函數,Generator是一種異步編程解決方案,Generator 函數是協程在 ES6 的實現,最大特點就是可以交出函數的執行權,Generator 函數可以看出是異步任務的容器,需要暫停的地方,都用yield語句注明。但是 Generator 使用起來較為復雜。
ES7又提出了新的異步解決方案:async/await,async是 Generator 函數的語法糖,async/await 使得異步代碼看起來像同步代碼,異步編程發展的目標就是讓異步邏輯的代碼看起來像同步一樣。
1.回調函數: callback
//node讀取文件
fs.readFile(xxx, "utf-8", function(err, data) {
//code
});
回調函數的使用場景(包括但不限于):
事件回調
Node API
setTimeout/setInterval中的回調函數
異步回調嵌套會導致代碼難以維護,并且不方便統一處理錯誤,不能try catch 和 回調地獄(如先讀取A文本內容,再根據A文本內容讀取B再根據B的內容讀取C...)。
fs.readFile(A, "utf-8", function(err, data) {
fs.readFile(B, "utf-8", function(err, data) {
fs.readFile(C, "utf-8", function(err, data) {
fs.readFile(D, "utf-8", function(err, data) {
//....
});
});
});
});
2.Promise
Promise 主要解決了回調地獄的問題,Promise 最早由社區提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise對象。
那么我們看看Promise是如何解決回調地獄問題的,仍然以上文的readFile為例。
function read(url) {
return new Promise((resolve, reject) => {
fs.readFile(url, "utf8", (err, data) => {
if(err) reject(err);
resolve(data);
});
});
}
read(A).then(data => {
return read(B);
}).then(data => {
return read(C);
}).then(data => {
return read(D);
}).catch(reason => {
console.log(reason);
});
想要運行代碼看效果,請戳(小姐姐使用的是VS的 Code Runner 執行代碼): github.com/YvetteLau/B…
思考一下在Promise之前,你是如何處理異步并發問題的,假設有這樣一個需求:讀取三個文件內容,都讀取成功后,輸出最終的結果。有了Promise之后,又如何處理呢?代碼可戳: github.com/YvetteLau/B…
注: 可以使用 bluebird 將接口 promise化;
引申: Promise有哪些優點和問題呢?
3.Generator
Generator 函數是 ES6 提供的一種異步編程解決方案,整個 Generator 函數就是一個封裝的異步任務,或者說是異步任務的容器。異步操作需要暫停的地方,都用 yield 語句注明。
Generator 函數一般配合 yield 或 Promise 使用。Generator函數返回的是迭代器。對生成器和迭代器不了解的同學,請自行補習下基礎。下面我們看一下 Generator 的簡單使用:
function* gen() {
let a = yield 111;
console.log(a);
let b = yield 222;
console.log(b);
let c = yield 333;
console.log(c);
let d = yield 444;
console.log(d);
}
let t = gen();
//next方法可以帶一個參數,該參數就會被當作上一個yield表達式的返回值
t.next(1); //第一次調用next函數時,傳遞的參數無效
t.next(2); //a輸出2;
t.next(3); //b輸出3;
t.next(4); //c輸出4;
t.next(5); //d輸出5;
為了讓大家更好的理解上面代碼是如何執行的,我畫了一張圖,分別對應每一次的next方法調用:
仍然以上文的readFile為例,使用 Generator + co庫來實現:
const fs = require("fs");
const co = require("co");
const bluebird = require("bluebird");
const readFile = bluebird.promisify(fs.readFile);
function* read() {
yield readFile(A, "utf-8");
yield readFile(B, "utf-8");
yield readFile(C, "utf-8");
//....
}
co(read()).then(data => {
//code
}).catch(err => {
//code
});
不使用co庫,如何實現?能否自己寫一個最簡的my_co?請戳: github.com/YvetteLau/B…
PS: 如果你還不太了解 Generator/yield,建議閱讀ES6相關文檔。
4.async/await
ES7中引入了 async/await 概念。async其實是一個語法糖,它的實現就是將Generator函數和自動執行器(co),包裝在一個函數中。
async/await 的優點是代碼清晰,不用像 Promise 寫很多 then 鏈,就可以處理回調地獄的問題。錯誤可以被try catch。
const fs = require("fs");
const bluebird = require("bluebird");
const readFile = bluebird.promisify(fs.readFile);
async function read() {
await readFile(A, "utf-8");
await readFile(B, "utf-8");
await readFile(C, "utf-8");
//code
}
read().then((data) => {
//code
}).catch(err => {
//code
});
可執行代碼,請戳:github.com/YvetteLau/B…
思考一下 async/await 如何處理異步并發問題的? github.com/YvetteLau/B…
如果你有更好的答案或想法,歡迎在這題目對應的github下留言:說一說JS異步發展史
async/await 就是 Generator 的語法糖,使得異步操作變得更加方便。來張圖對比一下:
async 函數就是將 Generator 函數的星號(*)替換成 async,將 yield 替換成await。
我們說 async 是 Generator 的語法糖,那么這個糖究竟甜在哪呢?
1)async函數內置執行器,函數調用之后,會自動執行,輸出最后結果。而Generator需要調用next或者配合co模塊使用。
2)更好的語義,async和await,比起星號和yield,語義更清楚了。async表示函數里有異步操作,await表示緊跟在后面的表達式需要等待結果。
3)更廣的適用性。co模塊約定,yield命令后面只能是 Thunk 函數或 Promise 對象,而async 函數的 await 命令后面,可以是 Promise 對象和原始類型的值。
4)返回值是Promise,async函數的返回值是 Promise 對象,Generator的返回值是 Iterator,Promise 對象使用起來更加方便。
async 函數的實現原理,就是將 Generator 函數和自動執行器,包裝在一個函數里。
具體代碼試下如下(和spawn的實現略有差異,個人覺得這樣寫更容易理解),如果你想知道如何一步步寫出 my_co ,可戳: github.com/YvetteLau/B…
function my_co(it) {
return new Promise((resolve, reject) => {
function next(data) {
try {
var { value, done } = it.next(data);
}catch(e){
return reject(e);
}
if (!done) {
//done為true,表示迭代完成
//value 不一定是 Promise,可能是一個普通值。使用 Promise.resolve 進行包裝。
Promise.resolve(value).then(val => {
next(val);
}, reject);
} else {
resolve(value);
}
}
next(); //執行一次next
});
}
function* test() {
yield new Promise((resolve, reject) => {
setTimeout(resolve, 100);
});
yield new Promise((resolve, reject) => {
// throw Error(1);
resolve(10)
});
yield 10;
return 1000;
}
my_co(test()).then(data => {
console.log(data); //輸出1000
}).catch((err) => {
console.log("err: ", err);
});
如果你有更好的答案或想法,歡迎在這題目對應的github下留言:談談對 async/await 的理解,async/await 的實現原理是什么");
await 命令后面的Promise對象,運行結果可能是 rejected,此時等同于 async 函數返回的 Promise 對象被reject。因此需要加上錯誤處理,可以給每個 await 后的 Promise 增加 catch 方法;也可以將 await 的代碼放在 try...catch 中。
多個await命令后面的異步操作,如果不存在繼發關系,最好讓它們同時觸發。
//下面兩種寫法都可以同時觸發
//法一
async function f1() {
await Promise.all([
new Promise((resolve) => {
setTimeout(resolve, 600);
}),
new Promise((resolve) => {
setTimeout(resolve, 600);
})
])
}
//法二
async function f2() {
let fn1 = new Promise((resolve) => {
setTimeout(resolve, 800);
});
let fn2 = new Promise((resolve) => {
setTimeout(resolve, 800);
})
await fn1;
await fn2;
}
await命令只能用在async函數之中,如果用在普通函數,會報錯。
async 函數可以保留運行堆棧。
/**
* 函數a內部運行了一個異步任務b()。當b()運行的時候,函數a()不會中斷,而是繼續執行。
* 等到b()運行結束,可能a()早就* 運行結束了,b()所在的上下文環境已經消失了。
* 如果b()或c()報錯,錯誤堆棧將不包括a()。
*/
function b() {
return new Promise((resolve, reject) => {
setTimeout(resolve, 200)
});
}
function c() {
throw Error(10);
}
const a = () => {
b().then(() => c());
};
a();
/**
* 改成async函數
*/
const m = async () => {
await b();
c();
};
m();
報錯信息如下,可以看出 async 函數可以保留運行堆棧。
如果你有更好的答案或想法,歡迎在這題目對應的github下留言:使用 async/await 需要注意什么?
在代碼實現前,我們需要先了解 Promise.race 的特點:
Promise.race返回的仍然是一個Promise. 它的狀態與第一個完成的Promise的狀態相同。它可以是完成( resolves),也可以是失敗(rejects),這要取決于第一個Promise是哪一種狀態。
如果傳入的參數是不可迭代的,那么將會拋出錯誤。
如果傳的參數數組是空,那么返回的 promise 將永遠等待。
如果迭代包含一個或多個非承諾值和/或已解決/拒絕的承諾,則 Promise.race 將解析為迭代中找到的第一個值。
Promise.race = function (promises) {
//promises 必須是一個可遍歷的數據結構,否則拋錯
return new Promise((resolve, reject) => {
if (typeof promises[Symbol.iterator] !== "function") {
//真實不是這個錯誤
Promise.reject("args is not iteratable!");
}
if (promises.length === 0) {
return;
} else {
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i]).then((data) => {
resolve(data);
return;
}, (err) => {
reject(err);
return;
});
}
}
});
}
測試代碼:
//一直在等待態
Promise.race([]).then((data) => {
console.log("success ", data);
}, (err) => {
console.log("err ", err);
});
//拋錯
Promise.race().then((data) => {
console.log("success ", data);
}, (err) => {
console.log("err ", err);
});
Promise.race([
new Promise((resolve, reject) => { setTimeout(() => { resolve(100) }, 1000) }),
new Promise((resolve, reject) => { setTimeout(() => { resolve(200) }, 200) }),
new Promise((resolve, reject) => { setTimeout(() => { reject(100) }, 100) })
]).then((data) => {
console.log(data);
}, (err) => {
console.log(err);
});
引申: Promise.all/Promise.reject/Promise.resolve/Promise.prototype.finally/Promise.prototype.catch 的實現原理,如果還不太會,戳:Promise源碼實現
如果你有更好的答案或想法,歡迎在這題目對應的github下留言:如何實現 Promise.race?
5.可遍歷數據結構的有什么特點?
一個對象如果要具備可被 for...of 循環調用的 Iterator 接口,就必須在其 Symbol.iterator 的屬性上部署遍歷器生成方法(或者原型鏈上的對象具有該方法)
PS: 遍歷器對象根本特征就是具有next方法。每次調用next方法,都會返回一個代表當前成員的信息對象,具有value和done兩個屬性。
//如為對象添加Iterator 接口;
let obj = {
name: "Yvette",
age: 18,
job: "engineer",
[Symbol.iterator]() {
const self = this;
const keys = Object.keys(self);
let index = 0;
return {
next() {
if (index < keys.length) {
return {
value: self[keys[index++]],
done: false
};
} else {
return { value: undefined, done: true };
}
}
};
}
};
for(let item of obj) {
console.log(item); //Yvette 18 engineer
}
使用 Generator 函數(遍歷器對象生成函數)簡寫 Symbol.iterator 方法,可以簡寫如下:
let obj = {
name: "Yvette",
age: 18,
job: "engineer",
* [Symbol.iterator] () {
const self = this;
const keys = Object.keys(self);
for (let index = 0;index < keys.length; index++) {
yield self[keys[index]];//yield表達式僅能使用在 Generator 函數中
}
}
};
原生具備 Iterator 接口的數據結構如下。
Array
Map
Set
String
TypedArray
函數的 arguments 對象
NodeList 對象
ES6 的數組、Set、Map 都部署了以下三個方法: entries() / keys() / values(),調用后都返回遍歷器對象。
如果你有更好的答案或想法,歡迎在這題目對應的github下留言:可遍歷數據結構的有什么特點?
在 requestAnimationFrame 之前,我們主要使用 setTimeout/setInterval 來編寫JS動畫。
編寫動畫的關鍵是循環間隔的設置,一方面,循環間隔足夠短,動畫效果才能顯得平滑流暢;另一方面,循環間隔還要足夠長,才能確保瀏覽器有能力渲染產生的變化。
大部分的電腦顯示器的刷新頻率是60HZ,也就是每秒鐘重繪60次。大多數瀏覽器都會對重繪操作加以限制,不超過顯示器的重繪頻率,因為即使超過那個頻率用戶體驗也不會提升。因此,最平滑動畫的最佳循環間隔是 1000ms / 60 ,約為16.7ms。
setTimeout/setInterval 有一個顯著的缺陷在于時間是不精確的,setTimeout/setInterval 只能保證延時或間隔不小于設定的時間。因為它們實際上只是把任務添加到了任務隊列中,但是如果前面的任務還沒有執行完成,它們必須要等待。
requestAnimationFrame 才有的是系統時間間隔,保持最佳繪制效率,不會因為間隔時間過短,造成過度繪制,增加開銷;也不會因為間隔時間太長,使用動畫卡頓不流暢,讓各種網頁動畫效果能夠有一個統一的刷新機制,從而節省系統資源,提高系統性能,改善視覺效果。
綜上所述,requestAnimationFrame 和 setTimeout/setInterval 在編寫動畫時相比,優點如下:
1.requestAnimationFrame 不需要設置時間,采用系統時間間隔,能達到最佳的動畫效果。
2.requestAnimationFrame 會把每一幀中的所有DOM操作集中起來,在一次重繪或回流中就完成。
3.當 requestAnimationFrame() 運行在后臺標簽頁或者隱藏的 里時,requestAnimationFrame() 會被暫停調用以提升性能和電池壽命(大多數瀏覽器中)。
requestAnimationFrame 使用(試試使用requestAnimationFrame寫一個移動的小球,從A移動到B初):
function step(timestamp) {
//code...
window.requestAnimationFrame(step);
}
window.requestAnimationFrame(step);
如果你有更好的答案或想法,歡迎在這題目對應的github下留言:requestAnimationFrame 和 setTimeout/setInterval 有什么區別?使用 requestAnimationFrame 有哪些好處?
類型轉換的規則三言兩語說不清,真想哇得一聲哭出來~
JS中類型轉換分為 強制類型轉換 和 隱式類型轉換 。
通過 Number()、parseInt()、parseFloat()、toString()、String()、Boolean(),進行強制類型轉換。
邏輯運算符(&&、 ||、 !)、運算符(+、-、*、/)、關系操作符(>、 <、 <= 、>=)、相等運算符(==)或者 if/while 的條件,可能會進行隱式類型轉換。
強制類型轉換
1.Number() 將任意類型的參數轉換為數值類型
規則如下:
如果是布爾值,true和false分別被轉換為1和0
如果是數字,返回自身
如果是 null,返回 0
如果是 undefined,返回 NAN
如果是字符串,遵循以下規則:
如果字符串中只包含數字(或者是 0X / 0x 開頭的十六進制數字字符串,允許包含正負號),則將其轉換為十進制
如果字符串中包含有效的浮點格式,將其轉換為浮點數值
如果是空字符串,將其轉換為0
如不是以上格式的字符串,均返回 NaN
如果是Symbol,拋出錯誤
如果是對象,則調用對象的 valueOf() 方法,然后依據前面的規則轉換返回的值。如果轉換的結果是 NaN ,則調用對象的 toString() 方法,再次依照前面的規則轉換返回的字符串值。
部分內置對象調用默認的 valueOf 的行為:
對象 | 返回值 |
---|---|
Array | 數組本身(對象類型) |
Boolean | 布爾值(原始類型) |
Date | 從 UTC 1970 年 1 月 1 日午夜開始計算,到所封裝的日期所經過的毫秒數 |
Function | 函數本身(對象類型) |
Number | 數字值(原始類型) |
Object | 對象本身(對象類型) |
String | 字符串值(原始類型) |
Number("0111"); //111
Number("0X11") //17
Number(null); //0
Number(""); //0
Number("1a"); //NaN
Number(-0X11);//-17
2.parseInt(param, radix)
如果第一個參數傳入的是字符串類型:
忽略字符串前面的空格,直至找到第一個非空字符,如果是空字符串,返回NaN
如果第一個字符不是數字符號或者正負號,返回NaN
如果第一個字符是數字/正負號,則繼續解析直至字符串解析完畢或者遇到一個非數字符號為止
如果第一個參數傳入的Number類型:
數字如果是0開頭,則將其當作八進制來解析(如果是一個八進制數);如果以0x開頭,則將其當作十六進制來解析
如果第一個參數是 null 或者是 undefined,或者是一個對象類型:
返回 NaN
如果第一個參數是數組: 1. 去數組的第一個元素,按照上面的規則進行解析
如果第一個參數是Symbol類型: 1. 拋出錯誤
如果指定radix參數,以radix為基數進行解析
parseInt("0111"); //111
parseInt(0111); //八進制數 73
parseInt("");//NaN
parseInt("0X11"); //17
parseInt("1a") //1
parseInt("a1"); //NaN
parseInt(["10aa","aaa"]);//10
parseInt([]);//NaN; parseInt(undefined);
parseFloat
規則和parseInt基本相同,接受一個Number類型或字符串,如果是字符串中,那么只有第一個小數點是有效的。
toString()
規則如下:
如果是Number類型,輸出數字字符串
如果是 null 或者是 undefined,拋錯
如果是數組,那么將數組展開輸出。空數組,返回""
如果是對象,返回 [object Object]
如果是Date, 返回日期的文字表示法
如果是函數,輸出對應的字符串(如下demo)
如果是Symbol,輸出Symbol字符串
let arry = [];
let obj = {a:1};
let sym = Symbol(100);
let date = new Date();
let fn = function() {console.log("穩住,我們能贏!")}
let str = "hello world";
console.log([].toString()); // ""
console.log([1, 2, 3, undefined, 5, 6].toString());//1,2,3,,5,6
console.log(arry.toString()); // 1,2,3
console.log(obj.toString()); // [object Object]
console.log(date.toString()); // Sun Apr 21 2019 16:11:39 GMT+0800 (CST)
console.log(fn.toString());// function () {console.log("穩住,我們能贏!")}
console.log(str.toString());// "hello world"
console.log(sym.toString());// Symbol(100)
console.log(undefined.toString());// 拋錯
console.log(null.toString());// 拋錯
String()
String() 的轉換規則與 toString() 基本一致,最大的一點不同在于 null 和 undefined,使用 String 進行轉換,null 和 undefined對應的是字符串 "null" 和 "undefined"
Boolean
除了 undefined、 null、 false、 ""、 0(包括 +0,-0)、 NaN 轉換出來是false,其它都是true.
隱式類型轉換
&& 、|| 、 ! 、 if/while 的條件判斷
需要將數據轉換成 Boolean 類型,轉換規則同 Boolean 強制類型轉換
運算符: + - * /
+ 號操作符,不僅可以用作數字相加,還可以用作字符串拼接。
僅當 + 號兩邊都是數字時,進行的是加法運算。如果兩邊都是字符串,直接拼接,無需進行隱式類型轉換。
除了上面的情況外,如果操作數是對象、數值或者布爾值,則調用toString()方法取得字符串值(toString轉換規則)。對于 undefined 和 null,分別調用String()顯式轉換為字符串,然后再進行拼接。
console.log({}+10); //[object Object]10
console.log([1, 2, 3, undefined, 5, 6] + 10);//1,2,3,,5,610
-、*、/ 操作符針對的是運算,如果操作值之一不是數值,則被隱式調用Number()函數進行轉換。如果其中有一個轉換除了為NaN,結果為NaN.
關系操作符: ==、>、< 、<=、>=
> , < ,<= ,>=
如果兩個操作值都是數值,則進行數值比較
如果兩個操作值都是字符串,則比較字符串對應的字符編碼值
如果有一方是Symbol類型,拋出錯誤
除了上述情況之外,都進行Number()進行類型轉換,然后再進行比較。
注: NaN是非常特殊的值,它不和任何類型的值相等,包括它自己,同時它與任何類型的值比較大小時都返回false。
console.log(10 > {});//返回false.
/**
*{}.valueOf ---> {}
*{}.toString() ---> "[object Object]" ---> NaN
*NaN 和 任何類型比大小,都返回 false
*/
相等操作符:==
如果類型相同,無需進行類型轉換。
如果其中一個操作值是 null 或者是 undefined,那么另一個操作符必須為 null 或者 undefined 時,才返回 true,否則都返回 false.
如果其中一個是 Symbol 類型,那么返回 false.
兩個操作值是否為 string 和 number,就會將字符串轉換為 number
如果一個操作值是 boolean,那么轉換成 number
如果一個操作值為 object 且另一方為 string、number 或者 symbol,是的話就會把 object 轉為原始類型再進行判斷(調用object的valueOf/toString方法進行轉換)
對象如何轉換成原始數據類型
如果部署了 [Symbol.toPrimitive] 接口,那么調用此接口,若返回的不是基礎數據類型,拋出錯誤。
如果沒有部署 [Symbol.toPrimitive] 接口,那么先返回 valueOf() 的值,若返回的不是基礎類型的值,再返回 toString() 的值,若返回的不是基礎類型的值, 則拋出異常。
//先調用 valueOf, 后調用 toString
let obj = {
[Symbol.toPrimitive]() {
return 200;
},
valueOf() {
return 300;
},
toString() {
return "Hello";
}
}
//如果 valueOf 返回的不是基本數據類型,則會調用 toString,
//如果 toString 返回的也不是基本數據類型,會拋出錯誤
console.log(obj + 200); //400
如果你有更好的答案或想法,歡迎在這題目對應的github下留言:JS 類型轉換的規則是什么?
HTML5則提出了 Web Worker 標準,表示js允許多線程,但是子線程完全受主線程控制并且不能操作dom,只有主線程可以操作dom,所以js本質上依然是單線程語言。
web worker就是在js單線程執行的基礎上開啟一個子線程,進行程序處理,而不影響主線程的執行,當子線程執行完之后再回到主線程上,在這個過程中不影響主線程的執行。子線程與主線程之間提供了數據交互的接口postMessage和onmessage,來進行數據發送和接收。
var worker = new Worker("./worker.js"); //創建一個子線程
worker.postMessage("Hello");
worker.onmessage = function (e) {
console.log(e.data); //Hi
worker.terminate(); //結束線程
};
//worker.js
onmessage = function (e) {
console.log(e.data); //Hello
postMessage("Hi"); //向主進程發送消息
};
僅是最簡示例代碼,項目中通常是將一些耗時較長的代碼,放在子線程中運行。
如果你有更好的答案或想法,歡迎在這題目對應的github下留言:簡述下對 webWorker 的理解
ES6模塊在編譯時,就能確定模塊的依賴關系,以及輸入和輸出的變量。
CommonJS 模塊,運行時加載。
ES6 模塊自動采用嚴格模式,無論模塊頭部是否寫了 "use strict";
require 可以做動態加載,import 語句做不到,import 語句必須位于頂層作用域中。
ES6 模塊中頂層的 this 指向 undefined,CommonJS 模塊的頂層 this 指向當前模塊。
CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。
CommonJS 模塊輸出的是值的拷貝,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。如:
//name.js
var name = "William";
setTimeout(() => name = "Yvette", 200);
module.exports = {
name
};
//index.js
const name = require("./name");
console.log(name); //William
setTimeout(() => console.log(name), 300); //William
對比 ES6 模塊看一下:
ES6 模塊的運行機制與 CommonJS 不一樣。JS 引擎對腳本靜態分析的時候,遇到模塊加載命令 import ,就會生成一個只讀引用。等到腳本真正執行時,再根據這個只讀引用,到被加載的那個模塊里面去取值。
//name.js
var name = "William";
setTimeout(() => name = "Yvette", 200);
export { name };
//index.js
import { name } from "./name";
console.log(name); //William
setTimeout(() => console.log(name), 300); //Yvette
如果你有更好的答案或想法,歡迎在這題目對應的github下留言:ES6模塊和CommonJS模塊的差異?
在說瀏覽器事件代理機制原理之前,我們首先了解一下事件流的概念,早期瀏覽器,IE采用的是事件冒泡事件流,而Netscape采用的則是事件捕獲。"DOM2級事件"把事件流分為三個階段,捕獲階段、目標階段、冒泡階段。現代瀏覽器也都遵循此規范。
那么事件代理是什么呢?
事件代理又稱為事件委托,在祖先級DOM元素綁定一個事件,當觸發子孫級DOM元素的事件時,利用事件冒泡的原理來觸發綁定在祖先級DOM的事件。因為事件會從目標元素一層層冒泡至document對象。
為什么要事件代理?
添加到頁面上的事件數量會影響頁面的運行性能,如果添加的事件過多,會導致網頁的性能下降。采用事件代理的方式,可以大大減少注冊事件的個數。
事件代理的當時,某個子孫元素是動態增加的,不需要再次對其進行事件綁定。
不用擔心某個注冊了事件的DOM元素被移除后,可能無法回收其事件處理程序,我們只要把事件處理程序委托給更高層級的元素,就可以避免此問題。
如將頁面中的所有click事件都代理到document上:
addEventListener 接受3個參數,分別是要處理的事件名、處理事件程序的函數和一個布爾值。布爾值默認為false。表示冒泡階段調用事件處理程序,若設置為true,表示在捕獲階段調用事件處理程序。
document.addEventListener("click", function (e) {
console.log(e.target);
/**
* 捕獲階段調用調用事件處理程序,eventPhase是 1;
* 處于目標,eventPhase是2
* 冒泡階段調用事件處理程序,eventPhase是 1;
*/
console.log(e.eventPhase);
});
如果你有更好的答案或想法,歡迎在這題目對應的github下留言:瀏覽器事件代理機制的原理是什么?
11.js如何自定義事件?
自定義 DOM 事件(不考慮IE9之前版本)
自定義事件有三種方法,一種是使用 new Event(), 另一種是 createEvent("CustomEvent") , 另一種是 new customEvent()
使用 new Event()
獲取不到 event.detail
let btn = document.querySelector("#btn");
let ev = new Event("alert", {
bubbles: true, //事件是否冒泡;默認值false
cancelable: true, //事件能否被取消;默認值false
composed: false
});
btn.addEventListener("alert", function (event) {
console.log(event.bubbles); //true
console.log(event.cancelable); //true
console.log(event.detail); //undefined
}, false);
btn.dispatchEvent(ev);
使用 createEvent("CustomEvent") (DOM3)
要創建自定義事件,可以調用 createEvent("CustomEvent"),返回的對象有 initCustomEvent 方法,接受以下四個參數:
type: 字符串,表示觸發的事件類型,如此處的"alert"
bubbles: 布爾值: 表示事件是否冒泡
cancelable: 布爾值,表示事件是否可以取消
detail: 任意值,保存在 event 對象的 detail 屬性中
let btn = document.querySelector("#btn");
let ev = btn.createEvent("CustomEvent");
ev.initCustomEvent("alert", true, true, "button");
btn.addEventListener("alert", function (event) {
console.log(event.bubbles); //true
console.log(event.cancelable);//true
console.log(event.detail); //button
}, false);
btn.dispatchEvent(ev);
使用 new customEvent() (DOM4)
使用起來比 createEvent("CustomEvent") 更加方便
var btn = document.querySelector("#btn");
/*
* 第一個參數是事件類型
* 第二個參數是一個對象
*/
var ev = new CustomEvent("alert", {
bubbles: "true",
cancelable: "true",
detail: "button"
});
btn.addEventListener("alert", function (event) {
console.log(event.bubbles); //true
console.log(event.cancelable);//true
console.log(event.detail); //button
}, false);
btn.dispatchEvent(ev);
自定義非 DOM 事件(觀察者模式)
EventTarget類型有一個多帶帶的屬性handlers,用于存儲事件處理程序(觀察者)。
addHandler() 用于注冊給定類型事件的事件處理程序;
fire() 用于觸發一個事件;
removeHandler() 用于注銷某個事件類型的事件處理程序。
function EventTarget(){
this.handlers = {};
}
EventTarget.prototype = {
constructor:EventTarget,
addHandler:function(type,handler){
if(typeof this.handlers[type] === "undefined"){
this.handlers[type] = [];
}
this.handlers[type].push(handler);
},
fire:function(event){
if(!event.target){
event.target = this;
}
if(this.handlers[event.type] instanceof Array){
const handlers = this.handlers[event.type];
handlers.forEach((handler)=>{
handler(event);
});
}
},
removeHandler:function(type,handler){
if(this.handlers[type] instanceof Array){
const handlers = this.handlers[type];
for(var i = 0,len = handlers.length; i < len; i++){
if(handlers[i] === handler){
break;
}
}
handlers.splice(i,1);
}
}
}
//使用
function handleMessage(event){
console.log(event.message);
}
//創建一個新對象
var target = new EventTarget();
//添加一個事件處理程序
target.addHandler("message", handleMessage);
//觸發事件
target.fire({type:"message", message:"Hi"}); //Hi
//刪除事件處理程序
target.removeHandler("message",handleMessage);
//再次觸發事件,沒有事件處理程序
target.fire({type:"message",message: "Hi"});
如果你有更好的答案或想法,歡迎在這題目對應的github下留言:js如何自定義事件?
知其然知其所以然,在說跨域方法之前,我們先了解下什么叫跨域,瀏覽器有同源策略,只有當“協議”、“域名”、“端口號”都相同時,才能稱之為是同源,其中有一個不同,即是跨域。
那么同源策略的作用是什么呢?同源策略限制了從同一個源加載的文檔或腳本如何與來自另一個源的資源進行交互。這是一個用于隔離潛在惡意文件的重要安全機制。
那么我們又為什么需要跨域呢?一是前端和服務器分開部署,接口請求需要跨域,二是我們可能會加載其它網站的頁面作為iframe內嵌。
跨域的方法有哪些?
常用的跨域方法
jsonp
盡管瀏覽器有同源策略,但是 標簽的 src 屬性不會被同源策略所約束,可以獲取任意服務器上的腳本并執行。jsonp 通過插入script標簽的方式來實現跨域,參數只能通過url傳入,僅能支持get請求。
實現原理:
Step1: 創建 callback 方法
Step2: 插入 script 標簽
Step3: 后臺接受到請求,解析前端傳過去的 callback 方法,返回該方法的調用,并且數據作為參數傳入該方法
Step4: 前端執行服務端返回的方法調用
下面代碼僅為說明 jsonp 原理,項目中請使用成熟的庫。分別看一下前端和服務端的簡單實現:
//前端代碼
function jsonp({url, params, cb}) {
return
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/7235.html
摘要:半路出家的前端程序員應該不在少數,我也是其中之一。年,馮馮同事兼師兄看我寫太費勁,跟我說對面樓在找,問我要不要學,說出來可能有點丟人,但是在那之前,我真得不知道什么是,什么是。 半路出家的前端程序員應該不在少數,我也是其中之一。 為何會走向前端 非計算機專業的我,畢業之后,就職于一家電力行業公司,做過設備調試、部門助理、測試,也寫過一段時間的QT,那三年的時間,最難過的不是工作忙不忙,...
摘要:如果你還沒讀過上篇上篇和中篇并無依賴關系,您可以讀過本文之后再閱讀上篇,可戳面試篇寒冬求職季之你必須要懂的原生上小姐姐花了近百個小時才完成這篇文章,篇幅較長,希望大家閱讀時多花點耐心,力求真正的掌握相關知識點。 互聯網寒冬之際,各大公司都縮減了HC,甚至是采取了裁員措施,在這樣的大環境之下,想要獲得一份更好的工作,必然需要付出更多的努力。 一年前,也許你搞清楚閉包,this,原型鏈,就...
摘要:循環可以使用的范圍包括數組和結構某些類似數組的對象對象,以及字符串。只能遍歷數組,不能中斷,返回值是修改后的數組。除了之外,等,也有同樣的問題。聲明一個只讀的常量。這在語法上,稱為暫時性死區。暫時性死區也意味著不再是一個百分百安全的操作。 互聯網寒冬之際,各大公司都縮減了HC,甚至是采取了裁員措施,在這樣的大環境之下,想要獲得一份更好的工作,必然需要付出更多的努力。 一年前,也許你搞清楚閉包...
摘要:只能遍歷數組,不能中斷,返回值是修改后的數組。這在語法上,稱為暫時性死區。作用域鏈無論是還是查詢,都會在當前的作用域開始查找,如果沒有找到,就會向上級作用域繼續查找目標標識符,每次上升一個作用域,一直到全局作用域為止。 互聯網寒冬之際,各大公司都縮減了HC,甚至是采取了裁員措施,在這樣的大環境之下,想要獲得一份更好的工作,必然需要付出更多的努力。 一年前,也許你搞清楚閉包,this,原...
閱讀 1207·2021-09-03 10:44
閱讀 603·2019-08-30 13:13
閱讀 2796·2019-08-30 13:11
閱讀 1967·2019-08-30 12:59
閱讀 1034·2019-08-29 15:32
閱讀 1595·2019-08-29 15:25
閱讀 987·2019-08-29 12:24
閱讀 1277·2019-08-27 10:58