在這個金九銀十的日子中,爲你們奉上JS精選複習寶典一份,望各位看官笑納!javascript
function fn() {
console.log(this); // 1. {a: 100}
var arr = [1, 2, 3];
(function() {
console.log(this); // 2. Window
})();
// 普通 JS
arr.map(function(item) {
console.log(this); // 3. Window
return item + 1;
});
// 箭頭函數
let brr = arr.map(item => {
console.log("es6", this); // 4. {a: 100}
return item + 1;
});
}
fn.call({ a: 100 });
複製代碼
其實訣竅很簡單,常見的基本是 3 種狀況:es5 普通函數、es6 的箭頭函數以及經過bind改變過上下文返回的新函數。html
① es5 普通函數:java
函數被直接調用,上下文必定是window 函數做爲對象屬性被調用,例如:obj.foo(),上下文就是對象自己obj 經過new調用,this綁定在返回的實例上nginx
② es6 箭頭函數:es6
它自己沒有this,會沿着做用域向上尋找,直到 window。請看下面的這段代碼:編程
function run() {
const inner = () => {
return () => {
console.log(this.a);
};
};
inner()();
}
run.bind({ a: 1 })(); // Output: 1
複製代碼
③ bind 綁定上下文返回的新函數:就是被第一個 bind 綁定的上下文,並且 bind 對「箭頭函數」無效。請看下面的這段代碼:後端
function run() {
console.log(this.a);
}
run.bind({ a: 1 })(); // output: 1
// 屢次bind,上下文由第一個bind的上下文決定
run.bind({ a: 2 }).bind({ a: 1 })(); // output: 2
複製代碼
最後,再說說這幾種方法的優先級:new > bind > 對象調用 > 直接調用跨域
題目:JS 中的原始數據類型?數組
ECMAScript 中定義了 7 種原始類型:promise
Boolean
String
Number
Null
Undefined
Symbol(es6)
BigInt (Stage提案) 注意:原始類型不包含 Object 和 Function
題目:經常使用的判斷方法?
在進行判斷的時候有typeof、instanceof。對於數組的判斷,使用Array.isArray():
typeof:
typeof 基本均可以正確判斷數據類型
typeof null和typeof [1, 2, 3]均返回"object"
ES6 新增:typeof Symbol()返回"symbol"
instanceof:
專門用於實例和構造函數對應
function Obj(value) {
this.value = value;
}
let obj = new Obj("test");
console.log(obj instanceof Obj); // output: true
複製代碼
判斷是不是數組:[1, 2, 3] instanceof Array
Array.isArray():ES6 新增,用來判斷是不是'Array'。Array.isArray({})返回false。
事件冒泡和事件捕獲
事件流分爲:冒泡和捕獲,順序是先捕獲再冒泡。
事件冒泡:子元素的觸發事件會一直向父節點傳遞,一直到根結點中止。此過程當中,能夠在每一個節點捕捉到相關事件。能夠經過stopPropagation方法終止冒泡。
事件捕獲:和「事件冒泡」相反,從根節點開始執行,一直向子節點傳遞,直到目標節點。
addEventListener
給出了第三個參數同時支持冒泡與捕獲:默認是false
,事件冒泡;設置爲true
時,是事件捕獲。
<div id="app" style="width: 100vw; background: red;">
<span id="btn">點我</span>
</div>
<script> // 事件捕獲:先輸出 "外層click事件觸發"; 再輸出 "內層click事件觸發" var useCapture = true; var btn = document.getElementById("btn"); btn.addEventListener( "click", function() { console.log("內層click事件觸發"); }, useCapture ); var app = document.getElementById("app"); app.onclick = function() { console.log("外層click事件觸發"); }; </script>
複製代碼
DOM0 級 和 DOM2 級 DOM2 級:前面說的addEventListener,它定義了DOM事件流,捕獲 + 冒泡。
DOM0 級:
直接在 html 標籤內綁定on事件 在 JS 中綁定on系列事件 注意:如今通用DOM2級事件,優勢以下:
能夠綁定 / 卸載事件 支持事件流 冒泡 + 捕獲:至關於每一個節點同一個事件,至少 2 次處理機會 同一類事件,能夠綁定多個函數
方法一:綁定構造函數
缺點:不能繼承父類原型方法/屬性
function Animal() {
this.species = "動物";
}
function Cat() {
// 執行父類的構造方法, 上下文爲實例對象
Animal.apply(this, arguments);
}
/** * 測試代碼 */
var cat = new Cat();
console.log(cat.species); // output: 動物
複製代碼
方法二:原型鏈繼承
缺點:沒法向父類構造函數中傳遞參數;子類原型鏈上定義的方法有前後順序問題。 注意:js 中交換原型鏈,均須要修復prototype.constructor指向問題。
function Animal(species) {
this.species = species;
}
Animal.prototype.func = function() {
console.log("Animal");
};
function Cat() {}
/** * func方法是無效的, 由於後面原型鏈被從新指向了Animal實例 */
Cat.prototype.func = function() {
console.log("Cat");
};
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat; // 修復: 將Cat.prototype.constructor從新指向自己
/** * 測試代碼 */
var cat = new Cat();
cat.func(); // output: Animal
console.log(cat.species); // undefined
複製代碼
方法 3:組合繼承
結合綁定構造函數和原型鏈繼承 2 種方式,缺點是:調用了 2 次父類的構造函數。
function Animal(species) {
this.species = species;
}
Animal.prototype.func = function() {
console.log("Animal");
};
function Cat() {
Animal.apply(this, arguments);
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
/** * 測試代碼 */
var cat = new Cat("cat");
cat.func(); // output: Animal
console.log(cat.species); // output: cat
複製代碼
方法4: 寄生組合繼承 改進了組合繼承的缺點,只須要調用 1 次父類的構造函數。這是目前最推薦的繼承方式
/** * 寄生組合繼承的核心代碼 * @param {Function} sub 子類 * @param {Function} parent 父類 */
function inheritPrototype(sub, parent) {
// 拿到父類的原型
var prototype = Object.create(parent.prototype);
// 改變constructor指向
prototype.constructor = sub;
// 父類原型賦給子類
sub.prototype = prototype;
}
function Animal(species) {
this.species = species;
}
Animal.prototype.func = function() {
console.log("Animal");
};
function Cat() {
Animal.apply(this, arguments); // 只調用了1次構造函數
}
inheritPrototype(Cat, Animal);
/** * 測試代碼 */
var cat = new Cat("cat");
cat.func(); // output: Animal
console.log(cat.species); // output: cat
複製代碼
如何理解 JS 中的原型?
原型就是用來繼承屬性和方法的。
當試圖獲得一個對象的某個屬性時,若是這個對象自己沒有這個屬性,那麼會去它的__proto__(即它的構造函數的prototype)中尋找
如何理解 JS 中的原型鏈?
__proto__是每一個對象都有的屬性,由於prototype也是對象,因此他也具備__proto__屬性,__proto__將每一個對象之間串聯起來,造成了鏈條,這就叫作原型鏈。
如何理解 JS 的做用域和做用域鏈?
① 做用域
ES5 有」全局做用域「和」函數做用域「。ES6 的let和const使得 JS 用了」塊級做用域「。
② 做用域鏈
當前做用域沒有找到定義,繼續向父級做用域尋找,直至全局做用域。這種層級關係,就是做用域鏈
執行一個宏任務(棧中沒有就從事件隊列中獲取)
執行過程當中若是遇到微任務,就將它添加到微任務的任務隊列中
宏任務執行完畢後,當即執行當前微任務隊列中的全部微任務(依次執行)
當前宏任務執行完畢,開始檢查渲染,而後GUI線程接管渲染
渲染完畢後,JS線程繼續接管,開始下一個宏任務(從事件隊列中獲取)
JS 是單線程的,其上面的全部任務都是在兩個地方執行:執行棧和任務隊列。前者是存放同步任務;後者是異步任務有結果後,就在其中放入一個事件。 當執行棧的任務都執行完了(棧空),js 會讀取任務隊列,並將能夠執行的任務從任務隊列丟到執行棧中執行。 這個過程是循環進行的,因此稱做EventLoop
JS執行上下文分爲全局執行上下文和函數執行上下文
①全局執行上下文
解析 JS 時候,建立一個 全局執行上下文 環境。把代碼中即將執行的(內部函數的不算,由於你不知道函數什麼時候執行)變量、函數聲明都拿出來。未賦值的變量就是undefined。
下面這段代碼輸出:undefined;而不是拋出Error。由於在解析 JS 的時候,變量 a 已經存入了全局執行上下文中了。
console.log(a);
var a = 1;
複製代碼
②函數執行上下文
和全局執行上下文差很少,可是多了this和arguments和參數。
在 JS 中,this是關鍵字,它做爲內置變量,其值是在執行的時候肯定(不是定義的時候肯定)。
定義:外部函數調用以後其變量對象本應該被銷燬,但閉包的存在使咱們仍然能夠訪問外部函數的變量對象,這就是閉包的重要概念
如何解決內存泄漏?
解決方法是顯式對外暴露一個接口,專門用以清理變量:
function mockData() {
const mem = {};
return {
clear: () => (mem = null), // 顯式暴露清理接口
get: page => {
if (page in mem) {
return mem[page];
}
mem[page] = Math.random();
}
};
}
複製代碼
一句話版本:
(123456789).toLocaleString('en-US');//"123,456,789"
複製代碼
要點:若是一個函數做爲一個對象的屬性,那麼經過對象的.運算符調用此函數,this就是此對象
let obj = {
a: "a",
b: "b",
test: function(arg1, arg2) {
console.log(arg1, arg2);
// this.a 就是 a; this.b 就是 b
console.log(this.a, this.b);
}
};
obj.test(1, 2);
複製代碼
知道了實現關鍵,下面就是咱們模擬的call:
Function.prototype.call2 = function(context) {
if (typeof this !== "function") {
throw new TypeError("Error");
}
// 默認上下文是window
context = context || window;
// 保存默認的fn
const { fn } = context;
// 前面講的關鍵,將函數自己做爲對象context的屬性調用,自動綁定this
context.fn = this;
const args = [...arguments].slice(1);
const result = context.fn(...args);
// 恢復默認的fn
context.fn = fn;
return result;
};
// 如下是測試代碼
function test(arg1, arg2) {
console.log(arg1, arg2);
console.log(this.a, this.b);
}
test.call2(
{
a: "a",
b: "b"
},
1,
2
);
複製代碼
apply和call實現相似,只是傳入的參數形式是數組形式,而不是逗號分隔的參數序列。
所以,藉助 es6 提供的...運算符,就能夠很方便的實現數組和參數序列的轉化。
Function.prototype.apply2 = function(context) {
if (typeof this !== "function") {
throw new TypeError("Error");
}
context = context || window;
const { fn } = context;
context.fn = this;
let result;
if (Array.isArray(arguments[1])) {
// 經過...運算符將數組轉換爲用逗號分隔的參數序列
result = context.fn(...arguments[1]);
} else {
result = context.fn();
}
context.fn = fn;
return result;
};
/** * 如下是測試代碼 */
function test(arg1, arg2) {
console.log(arg1, arg2);
console.log(this.a, this.b);
}
test.apply2(
{
a: "a",
b: "b"
},
[1, 2]
);
複製代碼
function deepClone(src, target) {
const keys = Reflect.ownKeys(src);
let value = null;
for (let key of keys) {
value = src[key];
if (Array.isArray(value)) {
target[key] = cloneArr(value, []);
} else if (typeof value === "object") {
// 若是是對象並且不是數組, 那麼遞歸調用深拷貝
target[key] = deepClone(value, {});
} else {
target[key] = value;
}
}
return target;
}
複製代碼
實現思路:這裏涉及了「訂閱/發佈模式」的相關知識。參考addEventListener和removeEventListener的具體效果來實現便可。
// 數組置空:
// arr = []; arr.length = 0; arr.splice(0, arr.length)
class Event {
constructor() {
this._cache = {};
}
// 註冊事件:若是不存在此種type,建立相關數組
on(type, callback) {
this._cache[type] = this._cache[type] || [];
let fns = this._cache[type];
if (fns.indexOf(callback) === -1) {
fns.push(callback);
}
return this;
}
// 觸發事件:對於一個type中的全部事件函數,均進行觸發
trigger(type, ...data) {
let fns = this._cache[type];
if (Array.isArray(fns)) {
fns.forEach(fn => {
fn(...data);
});
}
return this;
}
// 刪除事件:刪除事件類型對應的array
off(type, callback) {
let fns = this._cache[type];
// 檢查是否存在type的事件綁定
if (Array.isArray(fns)) {
if (callback) {
// 卸載指定的回調函數
let index = fns.indexOf(callback);
if (index !== -1) {
fns.splice(index, 1);
}
} else {
// 所有清空
fns = [];
}
}
return this;
}
}
// 如下是測試函數
const event = new Event();
event
.on("test", a => {
console.log(a);
})
.trigger("test", "hello");
複製代碼
const warnDemo = ctx => {
const promise = new Promise(resolve => {
resolve(ctx);
console.log("After resolved, but Run"); // 依然會執行這個語句
});
return promise;
};
warnDemo("ctx").then(ctx => console.log(`This is ${ctx}`));
複製代碼
Promise 的狀態一旦改變,就永久保持該狀態,不會再變了。
Promise 對象的錯誤具備「冒泡」性質,會一直向後傳遞,直到被捕獲爲止。也就是說,錯誤老是會被下一個catch語句捕獲
Promise會吃掉內部的錯誤,並不影響外部代碼的運行。因此須要catch,以防丟掉錯誤信息。
async函數返回一個Promise對象,可使用then方法添加回調函數。
當函數執行的時候,一旦遇到await就會先返回,等到異步操做完成,再接着執行函數體內後面的語句。
這也是它最受歡迎的地方:能讓異步代碼寫起來像同步代碼,而且方便控制順序。
能夠利用它實現一個sleep函數阻塞進程:
function sleep(millisecond) {
return new Promise(resolve => {
setTimeout(() => resolve, millisecond);
});
}
/** * 如下是測試代碼 */
async function test() {
console.log("start");
await sleep(1000); // 睡眠1秒
console.log("end");
}
test(); // 執行測試函數
複製代碼
雖然方便,可是它也不能取代Promise,尤爲是咱們能夠很方便地用Promise.all()來實現併發,而async/await只能實現串行(用很差會產生性能問題哦)。
目前 js 社區有 4 種模塊管理規範:AMD、CMD、CommonJS 和 EsModule。 ES Module 是原生實現的模塊化方案,與 CommonJS 有如下幾個區別:
CommonJS 支持動態導入,EsModule目前不支持 CommonJS 是同步導入,由於用於服務端,文件都在本地,同步導入即便卡住主線程影響也不大。而EsModule是異步導入,由於用於瀏覽器,須要下載文件,若是也採用同步導入會對渲染有很大影響 commonJs 輸出的是值的淺拷貝,esModule 輸出值的引用 ES Module 會編譯成 require/exports 來執行的
根據HTML代碼生成DOM樹
根據CSS生成CSSDOM
將 DOM 樹和 CSSOM 整合成 RenderTree
根據 RenderTree 開始渲染和展現
遇到script標籤,會阻塞渲染
瀏覽器中常見的線程有:渲染線程、JS 引擎線程、HTTP 線程等等。
例如,當咱們打開一個 Ajax 請求的時候,就啓動了一個 HTTP 線程。
一樣地,咱們能夠用線程的只是解釋:爲何直接操做 DOM 會變慢,性能損耗更大?由於 JS 引擎線程和渲染線程是互斥的。而直接操做 DOM 就會涉及到兩個線程互斥之間的通訊,因此開銷更大。
畢竟 JS 是能夠修改 DOM 的,若是 JS 執行的時候 UI 也工做,就有可能致使不安全(不符合預期)的渲染結果。
DOMContentLoaded是在onload前進行的。
DOMContentLoaded事件在 DOM 樹構建完畢後被觸發,咱們能夠在這個階段使用 js 去訪問元素。
async和defer的腳本可能尚未執行。
圖片及其餘資源文件可能還在下載中。
load事件在頁面全部資源被加載完畢後觸發,一般咱們不會用到這個事件,由於咱們不須要等那麼久。
document.addEventListener("DOMContentLoaded", () => {
console.log("DOMContentLoaded");
});
window.addEventListener("load", () => {
console.log("load");
});
複製代碼
// 定義回調函數
const Response = data => {
console.log(data);
};
// 構造 <script> 標籤
let script = document.createElement("script");
script.src =
"http://xxx.com?callback=Response";
// 向document中添加 <script> 標籤,而且發送GET請求
document.body.appendChild(script);
複製代碼
AOP(面向切面編程)的主要做用是把一些跟核心業務邏輯模塊無關的功能抽離出來,這些跟業務邏輯無關的功能一般包括日誌統計、安全控制、異常處理等。把這些功能抽離出來以後, 再經過「動態織入」的方式摻入業務邏輯模塊中
AOP的好處:
能夠保持業務邏輯模塊的純淨和高內聚性;能夠很方便地複用日誌統計等功能模塊
/** * 織入執行前函數 * @param {*} fn */
Function.prototype.before = function(fn) {
let self = this; // 保持原函數
return function() { // 返回包括原函數和新函數的「代理」函數
fn.apply(this, arguments) // 執行新函數,修正this
return self.apply(this, arguments) //執行原函數
}
}
/** * 織入執行後函數 * @param {*} fn */
Function.prototype.after = function(fn) {
let self = this;
return function() {
let current = self.apply(this, arguments) //先保存原函數
fn.apply(this, arguments) // 先執行新函數
return current;
}
}
let func = function() {
console.log('aop')
}
func = func.before(function(){
console.log('aop before')
}).after(function(){
console.log('aop after')
})
func()
// aop before
// aop
// aop after
複製代碼
願你們在金九銀十這個季節能找到滿意的工做!