天天看點

【直接收藏】前端JavaScript面試100問(下)

作者:陸榮濤

61、常見的 HTTP 請求有哪些 ? 他們的差別是什麼 ?

常見的有5種,分别是GET、HEAD, POST、PUT、 DELETE

  • GET:它是最常見的方法,用于擷取資源,常用于向伺服器查詢某些資訊。打開網頁一般都是用GET方法,因為要從 Web 伺服器擷取資訊
  • HEAD:類似于 GET請求,隻不過傳回的響應中沒有具體的内容,用于擷取報頭。
  • POST:向指定資源送出資料進行處理請求(例如送出表單或者上傳檔案), 資料被包含在請求體中。POST請求可能會導緻新的資源的建立和/或對已有資源的修改。
  • PUT:從用戶端向伺服器傳送的資料取代指定文檔的内容。
  • DELETE:請求伺服器删除指定的頁面。

最常見的HTTP請求方法是GET 和 POST。GET一般用于擷取/查詢資源資訊,而POST一般用于更新資源資訊。GET和POST的差別:

  • GET送出的資料會放在?之後,以問号(?)分割URL 和傳輸資料,參數之間以&相連
  • GET送出的資料大小有限制(因為浏覽器對URL的長度有限制), 而POST 方法送出的資料大小沒有限制。
  • GET方式送出資料會帶來安全問題,比如一個登入頁面通過GET方式送出資料時,使用者名和密碼将出現在URL上,如果頁面可以被緩存或者其他人可以通路這台機器,就可以從曆史記錄獲得該使用者的賬号和密碼。

62、 JS 的資料類型有哪些 ? 如何判斷資料類型 ?他們的優缺點是什麼?

  • typeof 用來檢測資料類型的運算符
  • 檢測的不管是數組還是正則都傳回的是"object",是以typeof不能判斷一個值是否為數組
  • instanceof/constructor。檢測某一個執行個體是否屬于某一個類使用instanceof/constructor可以檢測數組和正則
  • 用instanceof檢測的時候,隻要目前的這個類在執行個體的原型鍊上(可以通過原型鍊__proto__找到它),檢測出來的結果都是true。

    基本資料類型的值是不能用instanceof來檢測的

    在類的原型繼承中,instanceof檢測出來的結果其實是不準确的

  • Object.prototype.toString.call(value) ->找到Object原型上的toString方法,讓方法執行,并且讓方法中的this變為value(value->就是我們要檢測資料類型的值)。檢測的類型比較多,也比較精準。

63、 symbol 你是怎麼了解的 ?

Symbol 是 ES6 新推出的一種基本類型,它表示獨一無二的值

它可以選擇接受一個字元串作為參數或者不傳,但是相同參數的兩個Symbol值不相等

//不傳參數
   const s1 = Symbol();
   const s2 = Symbol();
   console.log(s1 === s2); // false

   // 傳入參數
   const s3 = Symbol('debug');
   const s4 = Symbol('debug');
   console.log(s3 === s4); // false           

可以通過typeof判斷是否為Symbol類型

console.log(typeof s1); // symbol           

Symbol.for():用于将描述相同的Symbol變量指向同一個Symbol值

let a1 = Symbol.for('a');
let a2 = Symbol.for('a');
a1 === a2  // true
typeof a1  // "symbol"
typeof a2  // "symbol"

let a3= Symbol("a");
a1 === a3      // false           

Symbol.keyFor():用來檢測該字元串參數作為名稱的 Symbol值是否已被登記,傳回一個已登記的 Symbol 類型值的key

let a1 = Symbol.for("a");
Symbol.keyFor(a1);    // "a"

let a2 = Symbol("a");
Symbol.keyFor(a2);    // undefined           

description:用來傳回Symbol資料的描述:

// Symbol()定義的資料
let a = Symbol("acc");
a.description  // "acc"
Symbol.keyFor(a);  // undefined

// Symbol.for()定義的資料
let a1 = Symbol.for("acc");
a1.description  // "acc"
Symbol.keyFor(a1);  // "acc"

// 未指定描述的資料
let a2 = Symbol();
a2.description  // undefined           
  • 使用場景一:對象添加屬性
let n = Symbol('N');
let obj = {
    name: "hello world",
    age: 11,
    [n]: 100
};           
  • 使用場景二:給對象添加私有屬性
const speak = Symbol();
class Person {
    [speak]() {
        console.log(123)
    }
}
let person = new Person()
console.log(person[speak]())           

64、數組常用方法有那些

數組的常用方法 這樣的面試題 算是非常基礎的面試題 面試官的目的 也不會隻是單純的讓你背誦出 數組的所有方法

這裡的關鍵點 是 常用 這兩個字 面試官的 目的是 通過 這個問題 看你平時在項目中 對于 數組函數的應用和了解 然後判斷出 你平時在項目中對于數組的應用 然後推測出你真實的技術水準

這裡建議的回答方式是 通過一個 自己用的最多的數組函數方法 深入展開的說一說 在 實際項目中的應用

例如談到 數組單元删除 數組,splice() 除了要說 函數的用法之外 還要談到 具體的項目中 删除數組單元之後 數組坍塌的影響 以及如何處理

concat() 連接配接兩個或更多的數組,并傳回結果。

join() 把數組的所有元素放入一個字元串。元素通過指定的分隔符進行分隔。

pop() 删除并傳回數組的最後一個元素。

shift() 删除并傳回數組的第一個元素

push() 向數組的末尾添加一個或更多元素,并傳回新的長度。

unshift() 向數組的開頭添加一個或更多元素,并傳回新的長度。

reverse() 颠倒數組中元素的順序。

slice() 從某個已有的數組傳回標明的元素

sort() 對數組的元素進行排序

splice() 删除元素,并向數組添加新元素。

toSource() 傳回該對象的源代碼。

toString() 把數組轉換為字元串,并傳回結果。

toLocaleString() 把數組轉換為本地數組,并傳回結果。

valueOf() 傳回數組對象的原始值

65、JavaScript如何存儲cookie

基本文法是 document.cookie = '鍵名=鍵值;expires=時間對象;path=路徑' ;

時效 如果不設定 預設是 seeion 會話時效路徑 如果不設定 預設是 目前檔案所在檔案夾

設定時效 要 設定一個時間對象 時間對象的時間戳 就是 時效期要注意計算 目前時區 和 世界标磚時間的時間差

路徑一般設定為根目錄 也就是 '/'

66、柯理化函數

所謂的柯裡化函數 指的是 把接受多個參數的函數變換成接受一個單一參數的函數 并且傳回接受餘下的參數而且傳回結果的新函數

// 普通的add函數
function add(x, y) {
    return x + y
}

// Currying後
function curryingAdd(x) {
    return function (y) {
        return x + y
    }
}

add(1, 2)           // 3
curryingAdd(1)(2)   // 3           

優點:

1, 參數複用

例如 一個函數 有兩個參數 但是第一個參數會被反複使用 每次都需要輸入 一個重複的參數

使用柯裡化函數之後 隻需要 輸入一個參數就可以了

2, 提前确認

提前定義好一個參數 也就 決定了整個函數程式的執行方向 避免每次都執行判斷比較等

缺點:

隻能提前定義一個參數 如果想要提前定義多個參數 這樣的文法是不支援

柯裡化函數執行效能上的問題:

存取arguments對象通常要比存取命名參數要慢一點

一些老版本的浏覽器在arguments.length的實作上是相當慢的

使用 函數.apply() 和 函數.call() 通常比直接調用 fn() 稍微慢點

建立大量嵌套作用域和閉包函數會帶來花銷,無論是在記憶體還是速度上

67、對象周遊方法

JavaScript中 對象的周遊方法

for...in

基本文法是 for( 變量 in 對象 ){ 循環體程式 }

這裡要注意的是

1, 變量中存儲的鍵名 通過鍵名擷取對象中存儲的鍵值

因為是變量 點文法取值 不支援解析變量 要使用 對象[鍵名] 擷取鍵值

2, 循環變量 定義 let 和 var 定義 執行效果是不同的

Object.keys( 對象 )

傳回一個數組 是 目前對象 所有鍵名組成的數組

之後再循環周遊這個數組 再執行操作

Object.value( 對象 )

傳回一個數組 是 目前對象 所有鍵值組成的數組

之後再循環周遊這個數組 再執行操作

68、數組扁平化

數組扁平化

所謂的數組扁平化就是将多元數組轉化為一維數組一般數組扁平化,數組中存儲的多元資料都是數組 不會是對象或者函數

最常用的方法 就是 數組.toString() 将數組轉化為字元串 結果是 擷取數組中的每一個單元的資料 組成一個字元串 使用逗号間隔 再 以逗号為間隔 将字元串 轉化為數組

function fun1( arr ){
   let str = arr.toString();
   return str.split(',');
}           

還可以使用 數組.some() 方法 判斷數組中是不是還存在數組 在使用 展開運算符 指派

function fun1(arr) {
    while (arr.some(item => Array.isArray(item))) {
        arr = [].concat(...arr);
    }
    return arr;
}           

另外 ES6 文法中 新增的 flat函數也可以實作數組的扁平化參數是固定的

const arr = 原始數組.flat( Infinity );           

69、typeof 原理

利用 typeof 是根據傳回值的結果來判斷資料類型

具體傳回值 一共是 number, string, object, boolean, function, undefined

其中 數組 null 對象 的傳回值 都是 object這樣的話具體的資料類型就不能區分的非常明确 在實際項目中 就不能準确的區分

如果想要具體的 區分 資料類型 需要使用 Object.prototype.toString.call() 方法 傳回值是

object String 字元串

object Number 數值類型

object Boolean 布爾類型

object Undefined undefined類型

object Null null類型

object Function 函數類型

object Array 數組類型

70、介紹類型轉化

JavaScript 因為是 弱類型計算機語言 存儲資料時 對變量儲存的資料類型沒有設定是以一個變量中可以存儲任意類型的資料 在程式的執行過程中 就會遇到需要資料類型轉化的情況

自動轉化

自動轉化為字元串

資料 直接轉化為 對應的字元串

自動轉化為數值類型 轉化為 對應的數值 1 true 0 false null "" ''

符合數字規範的字元串

轉化為 NaN

不符合數字規範的字元串

undefined

自動轉化為數值類型

false:

0 0.0000 '' NaN null undefined

true:

其他情況都轉化為 true

強制轉化

強制轉化為布爾類型

Boolean( 變量 / 表達式 )

轉化原則 和 自動轉化原則完全相同

false : 0 0.000 '' null NaN undefined

true : 其他情況都轉化為true

強制轉化為字元串類型

String( 變量 / 表達式 );

轉化原則 和 自動轉化原則完全相同

不會改變 變量中存儲的原始資料

變量.toString( 進制 ) ;

轉化原則 和 自動轉化原則完全相同

不會改變 變量中存儲的原始資料

如果是 整數類型 可以 設定 轉化的進制

變量 存儲 null 或者 undefined不支援

強制轉化為數值類型

Number()

轉化原則 和 自動轉化完全相同

不會改變 變量中存儲的原始内容

parseInt()

從 左側起 擷取符合整數文法規範的内容部分

如果 左起第一個字元就不符合整數文法規範

執行結果是 NaN

parseFloat()

從 左側起 擷取符合浮點數文法規範的内容部分

如果 左起第一個字元就不符合浮點數文法規範

執行結果是 NaN

71、執行上下文

執行上下文:指目前執行環境中的變量、函數聲明,參數(arguments),作用域鍊,this等資訊。分為全局執行上下文、函數執行上下文,其差別在于全局執行上下文隻有一個,函數執行上下文在每次調用函數時候會建立一個新的函數執行上下文。

變量對象是與執行上下文相關的資料作用域,存儲了上下文中定義的變量和函數聲明。

變量對象式一個抽象的概念,在不同的上下文中,表示不同的對象:

全局執行上下文的變量對象全局執行上下文中,變量對象就是全局對象。在頂層js代碼中,this指向全局對象,全局變量會作為該對象的屬性來被查詢。在浏覽器中,window就是全局對象。函數執行上下文的變量對象函數上下文中,變量對象VO就是活動對象AO。初始化時,帶有arguments屬性。函數代碼分成兩個階段執行進入執行上下文時,此時變量對象包括形參函數聲明,會替換已有變量對象變量聲明,不會替換形參和函數函數執行

執行上下文棧的作用是用來跟蹤代碼的,由于JS是單線程的,每次隻能做一件事情,其他的事情會放在指定的上下文棧中排隊等待執行。

JS解釋器在初始化代碼的時候,首先會建立一個新的全局執行上下文到執行上下文棧頂中,然後随着每次函數的調用都會建立一個新的執行上下文放入到棧頂中,随着函數執行完畢後被執行上下文棧頂彈出,直到回到全局的執行上下文中。

首先建立了全局執行上下文,目前全局執行上下文處于活躍狀态。全局代碼中有2個函數 getName 和 getYear,然後調用 getName 函數,JS引擎停止執行全局執行上下文,建立了新的函數執行上下文,且把該函數上下文放入執行上下文棧頂。getName 函數裡又調用了 getYear 函數,此時暫停了 getName 的執行上下文,建立了 getYear 函數的新執行上下文,且把該函數執行上下文放入執行上下文棧頂。當 getYear 函數執行完後,其執行上下文從棧頂出棧,回到了 getName 執行上下文中繼續執行。當 getName 執行完後,其執行上下文從棧頂出棧,回到了全局執行上下文中。

72、閉包的問題和優化

閉包:是指有權通路另外一個函數作用域中的變量的函數。建立閉包的常見方式就是在一個函數内部建立另外一個函數。

作用:

1、可以讀取函數内部的變量2、相當于劃出了一塊私有作用域,避免資料污染;3、讓變量始終儲存在記憶體中

閉包有三個特性:

1.函數嵌套函數

2.函數内部可以引用外部的參數和變量

3.參數和變量不會被垃圾回收機制回收

閉包的問題

閉包會産生不銷毀的上下文,會導緻棧/堆記憶體消耗過大,有時候也會導緻記憶體洩漏等,影響頁面的運作性能,是以在真實項目中,要合理應用閉包!

閉包的優化

原始代碼

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };

  this.getMessage = function() {
    return this.message;
  };
}           

優化代碼

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype.getName = function() {
  return this.name;
};
MyObject.prototype.getMessage = function() {
  return this.message;
};           

73、浏覽器和Node事件循環的差別

一、全局環境下this的指向

在node中this指向global而在浏覽器中this指向window,這就是為什麼underscore中一上來就定義了一 root;

而且在浏覽器中的window下封裝了不少的API 比如 alert 、document、location、history 等等還有很多。我門就不能在node環境中xxx();或window.xxx();了。因為這些API是浏覽器級别的封裝,存javascript中是沒有的。當然node中也提供了不少node特有的API。

二、js引擎

在浏覽器中不同的浏覽器廠商提供了不同的浏覽器核心,浏覽器依賴這些核心解釋折我們編寫的js。但是考慮到不同核心的少量差異,我們需要對應相容性好在有一些優秀的庫幫助我們處理這個問題比如jquery、underscore等等。

  nodejs是基于Chromes JavaScript runtime,也就是說,實際上它是對GoogleV8引擎(應用于Google Chrome浏覽器)進行了封裝。V8引 擎執行Javascript的速度非常快,性能非常好。

NodeJS并不是提供簡單的封裝,然後提供API調用,如果是這樣的話那麼它就不會有現在這麼火了。Node對一些特殊用例進行了優化,提供了替代的API,使得V8在非浏覽器環境下運作得更好。例如,在伺服器環境中,處理二進制資料通常是必不可少的,但Javascript對此支援不足,是以,V8.Node增加了Buffer類,友善并且高效地 處理二進制資料。是以,Node不僅僅簡單的使用了V8,還對其進行了優化,使其在各環境下更加給力。

三、DOM操作

浏覽器中的js大多數情況下是在直接或間接(一些虛拟DOM的庫和架構)的操作DOM。因為浏覽器中的代碼主要是在表現層工作。但是node是一門服務端技術。沒有一個前台頁面,是以我門不會再node中操作DOM。

四、I/O讀寫

與浏覽器不同,我們需要像其他服務端技術一樣讀寫檔案,nodejs提供了比較友善的元件。而浏覽器(確定相容性的)想在頁面中直接打開一個本地的圖檔就麻煩了好多(别和我說這還不簡單,相對路徑。。。。。。試試就知道了要麼找個庫要麼二進制流,要麼上傳上去有了網絡位址在顯示。不然人家為什麼要搞一個js庫呢),而這一切node都用一個元件搞定了。

五、子產品加載

 javascript有個特點,就是原生沒提供包引用的API一次性把要加載的東西全執行一遍,這裡就要看各位閉包的功力了。所用東西都在一起,沒有分而治之,搞的特别沒有邏輯性和複用性。如果頁面簡單或網站當然我們可以通過一些AMD、CMD的js庫(比如requireJS 和 seaJS)搞定事實上很多大型網站都是這麼幹的。

  在nodeJS中提供了CMD的子產品加載的API,如果你用過seaJS,那麼應該上手很快。

node還提供了npm 這種包管理工具,能更有效友善的管理我們飲用的庫

74、移動端點選延遲

原因 :

為了确定使用者是要做單擊 還是輕按兩下 還是要做其他的操作 是以移動端 當你點選時 會有 300毫秒延遲 為了等待判斷使用者的下一步操作是什麼

解決方案1

禁用縮放

<meta name="viewport" content="user-scalable=no">
<meta name="viewport" content="initial-scale=1,maximum-scale=1">


當HTML文檔頭部包含以上meta标簽時 表明這個頁面是不可縮放的,那輕按兩下縮放的功能就沒有意義了,此時浏覽器可以禁用預設的輕按兩下縮放行為并且去掉300ms的點選延遲。
這個方案有一個缺點,就是必須通過完全禁用縮放來達到去掉點選延遲的目的,然而完全禁用縮放并不是我們的初衷,我們隻是想禁掉預設的輕按兩下縮放行為,這樣就不用等待300ms來判斷目前操作是否是輕按兩下。但是通常情況下,我們還是希望頁面能通過雙指縮放來進行縮放操作,比如放大一張圖檔,放大一段很小的文字。           

解決方案2 更改預設的視口寬度

<meta name="viewport" content="width=device-width">

一開始,為了讓桌面站點能在移動端浏覽器正常顯示,移動端浏覽器預設的視口寬度
并不等于裝置浏覽器視窗寬度,而是要比裝置浏覽器視窗寬度大,通常是980px。
我們可以通過以下标簽來設定視口寬度為裝置寬度。因為輕按兩下縮放主要是用來改善
桌面站點在移動端浏覽體驗的,而随着響應式設計的普及,很多站點都已經對移動
端坐過适配和優化了,這個時候就不需要輕按兩下縮放了,如果能夠識别出一個網站是
響應式的網站,那麼移動端浏覽器就可以自動禁掉預設的輕按兩下縮放行為并且去掉
300ms的點選延遲。如果設定了上述meta标簽,那浏覽器就可以認為該網站已經
對移動端做過了适配和優化,就無需輕按兩下縮放操作了。
這個方案相比方案一的好處在于,它沒有完全禁用縮放,而隻是禁用了浏覽器預設
的輕按兩下縮放行為,但使用者仍然可以通過雙指縮放操作來縮放頁面。           

解決方案3 CSS touch-action

跟300ms點選延遲相關的,是touch-action這個CSS屬性。這個屬性指定了相應元素上能夠觸發的使用者代理(也就是浏覽器)的預設行為。如果将該屬性值設定為touch-action: none,那麼表示在該元素上的操作不會觸發使用者代理的任何預設行為,就無需進行300ms的延遲判斷。


最後的最後 我們還可以使用一些 插件來解決這個問題 例如 FastClick 是 FT Labs 專門為解決移動端浏覽器 300 毫秒點選延遲問題所開發的一個輕量級的庫。FastClick的實作原理是在檢測到touchend事件的時候,會通過DOM自定義事件立即出發模拟一個click事件,并把浏覽器在300ms之後的click事件阻止掉。

安裝    npm install fastclick -S

使用    如何你是vue項目可以在main.js裡面直接引入,當然這樣是全局的,如果你需要某個頁面用到,那就單個頁面引入。

//引入
import fastClick from 'fastclick'
//初始化FastClick執行個體。在頁面的DOM文檔加載完成後
fastClick.attach(document.body)           

75、cookie屬性

cookie的常見屬性

鍵名 cookie鍵值對的鍵名

鍵值 cookie鍵值對的鍵值

expirescookie的時效 分為 session會話時效 時間時效 時間時效是伺服器時間也就是世界标準時間

path路徑 符合路徑的檔案才能通路cookie

httponly設定 為 true 了之後可以防止js程式通路 防止 xss攻擊 增加cookie的安全性

secure設定 為 true 了之後cookie隻能通過https協定發送 http協定是不能發送的 這樣也是為了增加cookie的安全性

76、反柯裡化

反柯裡化的作用是,當我們調用某個方法,不用考慮這個對象在被設計時,是否擁有這個方法,隻要這個方法适用于它,我們就可以對這個對象使用它

例如

Function.prototype.uncurring = function() {
  var self = this;
  return function() {
    var obj = Array.prototype.shift.call(arguments);
    return self.apply(obj, arguments);
  };
};           

我們先來看看上面這段代碼有什麼作用。

我們要把Array.prototype.push方法轉換成一個通用的push函數,隻需要這樣做:

var push = Array.prototype.push.uncurring();

//測試一下
(function() {
  push(arguments, 4);
  console.log(arguments); //[1, 2, 3, 4]
})(1, 2, 3)           

arguments本來是沒有push方法的,通常,我們都需要用Array.prototype.push.call來實作push方法,但現在,直接調用push函數,既簡潔又意圖明了。

我們不用考慮對象是否擁有這個方法,隻要它适用于這個方法,那就可以使用這個方法(類似于鴨子類型)。

我們來分析一下調用Array.prototype.push.uncurring()這句代碼時,發生了什麼事情:

Function.prototype.uncurring = function() {
  var self = this;  //self此時是Array.prototype.push

  return function() {
    var obj = Array.prototype.shift.call(arguments);
    //obj 是{
    //  "length": 1,
    //  "0": 1
    //}
    //arguments的第一個對象被截去(也就是調用push方法的對象),剩下[2]

    return self.apply(obj, arguments);
    //相當于Array.prototype.push.apply(obj, 2);

  };
};           

//測試一下

var push = Array.prototype.push.uncurring();
var obj = {
  "length": 1,
  "0" : 1
};

push(obj, 2);
console.log( obj ); //{0: 1,1: 2, length: 2 }           

看到這裡你應該對柯裡化和反柯裡化有了一個初步的認識了,但要熟練的運用在開發中,還需要我們更深入的去了解它們内在的含義。

77、千分位

這裡的需求 本質上是要 将 數字 轉化為 帶有千分位字元串 方法有很多

方法1 正規表達式

console.info( str.replace(/\d{1,3}(?=(\d{3})+$)/g,function(s){
  return s+','
}) )           

方法2 字元串替換

console.info( str.replace(/(\d{1,3})(?=(\d{3})+$)/g,function($1){
  return $1=$1+','
}) )           

方法3 數字轉數組 反轉後 添加 , 再反轉回來拼接為字元串

console.info( str.split("").reverse().join("").replace(/(\d{3})+?/g,function(s){
  return s+",";
}).replace(/,$/,"").split("").reverse().join("") )           

方法4 利用while循環拼接字元串每隔3個數字加一個分隔符,首尾不加

var result="",
  index = 0,
  len = str.length-1;
while(len>=0) {
  index%3===0&&index!==0 ? result+=","+str[len] : result+=str[len];
  len--;
  index++;
};
result=result.split("").reverse().join("");
console.info(result);           

方法5 利用while循環在數組裡push分隔符,首尾不加

// 利用while循環在數組裡push分隔符
var result="",
  index = 0,
  len = str.length,
  i = len-1,
  arr = str.split("");
while(len-index>0){
  len>=index&&len-index!==len && arr.splice(len-index,0,",");
  index+=3;
  i-=4;
};
console.log(arr.join(""));           

78、load和ready差別

document.ready:

是ready,表示文檔結構已經加載完成 不包含圖檔等非文字媒體檔案 隻要html标簽結構加載完畢就可以;

document.load:

是onload,訓示頁面包含圖檔等檔案在内的所有元素都加載完成。

1、概念

2、作用

document.ready:

在DOM加載完成後就可以可以對DOM進行操作。

一般情況一個頁面響應加載的順序是,域名解析-加載html-加載js和css-加載圖檔等其他資訊。

那麼Dom Ready應該在“加載js和css”和“加載圖檔等其他資訊”之間,就可以操作Dom了。

document.load:

在document文檔加載完成後就可以可以對DOM進行操作,document文檔包括了加載圖檔等其他資訊。

那麼Dom Load就是在頁面響應加載的順序中的“加載圖檔等其他資訊”之後,就可以操作Dom了。

3、加載順序

document.ready:

文檔加載的順序:域名解析-->加載HTML-->加載JavaScript和CSS-->加載圖檔等非文字媒體檔案。

隻要<img>标簽加載完成,不用等該圖檔加載完成,就可以設定圖檔的屬性或樣式等。

在原生JavaScript中沒有Dom ready的直接方法。

document.load:

文檔加載的順序:域名解析-->加載HTML-->加載JavaScript和CSS-->加載圖檔等非文字媒體檔案。

DOM load在加載圖檔等非文字媒體檔案之後,表示在document文檔加載完成後才可以對DOM進行操作,document文檔包括了加載圖檔等非文字媒體檔案。

例如,需要等該圖檔加載完成,才可以設定圖檔的屬性或樣式等。

在原生JavaScript中使用onload事件。

79、自定義事件

自定義事件,就是自己定義事件類型,自己定義事件處理函數。

我們平時操作dom時經常會用到onclick、onmousemove等浏覽器特定行為的事件類型。

封裝is自定義事件基本的構思:

var eventTarget = {
  addEvent: function(){
    //添加事件
  },
  fireEvent: function(){
    //觸發事件
  },
  removeEvent: function(){
    //移除事件
  }
};           

在js預設事件中事件類型以及對應的執行函數是一一對應的,但是自定義事件,需要一個映射表來建立兩者之間的聯系。

如: 這樣每個類型可以處理多個事件函數

handlers = {
      "type1":[
            "fun1",
            "fun2",
            // "..."
         ],
       "type2":[
            "fun1",
            "fun2"
             // "..."
         ]
         //"..."
}           

代碼實作:

function EventTarget(){
    //事件處理程式數組集合
    this.handlers={};
}

//自定義事件的原型對象
EventTarget.prototype={
    //設定原型構造函數鍊
    constructor:EventTarget,
    //注冊給定類型的事件處理程式
    //type->自定義事件類型,如click,handler->自定義事件回調函數
    addEvent:function(type,handler){
        //判斷事件處理函數中是否有該類型事件
        if(this.handlers[type]==undefined){
            this.handlers[type]=[];
        }
        this.handlers[type].push(handler);
    },

    //觸發事件
    //event為一個js對象,屬性中至少包含type屬性。
    fireEvent:function(event){
        //模拟真實事件的event
        if(!event.target){
            event.target=this;
        }
        //判斷是否存在該事件類型
        if(this.handlers[event.type] instanceof Array){
            var items=this.handlers[event.type];
            //在同一事件類型下可能存在多個事件處理函數,依次觸發
            //執行觸發
            items.forEach(function(item){
                item(event);
            })
        }
    },

    //删除事件
    removeEvent:function(type,handler){
        //判斷是否存在該事件類型
        if(this.handlers[type] instanceof Array){
            var items=this.handlers[type];
            //在同一事件類型下可能存在多個處理事件
            for(var i=0;i<items.length;i++){
                if(items[i]==handler){
                    //從該類型的事件數組中删除該事件
                    items.splice(i,1);
                    break;
                }
            }
        }
    }

}

//調用方法
function fun(){
    console.log('執行該方法');
}
function fun1(obj){
    console.log('run '+obj.min+'s');
}
var target=new EventTarget();
target.addEvent("run",fun);//添加事件
target.addEvent("run",fun1);//添加事件

target.fireEvent({type:"run",min:"30"});//執行該方法   123

target.removeEvent("run",fun);//移除事件

target.fireEvent({type:"run",min:"20"});//123           

為什麼要把方法添加到對象原型上?

在構造函數中加屬性,在原型中加方法。

将屬性和方法都寫在構造函數裡是沒有問題的,但是每次進行執行個體化的過程中,要重複建立功能不變的方法。

由于方法本質上是函數,其實也就是在堆記憶體中又建立了一個對象空間存放存儲函數,造成了不必要的資源浪費。

在本身添加會導緻每次對象執行個體化時代碼被複制,都需要申請一塊記憶體存放該方法。

寫一個EventEmitter類,包括on()、off()、once()、emit()方法once():為指定事件注冊一個單次監聽器,單次監聽器最多隻觸發一次,觸發後立即解除監聽器。

class EventEmitter{
            constructor(){
                this.handlers={};
            }
            on(type,fn){
                if(!this.handlers[type]){
                    this.handlers[type]=[];
                }
                this.handlers[type].push(fn);
                return this;
            }
            off(type,fn){
                let fns=this.handlers[type];
                for(let i=0;i<fns.length;i++){
                    if(fns[i]==fn){
                        fns.splice(i,1);
                        break;
                    }
                }
                return this;
            }
            emit(...args){
                let type=args[0];
                let params=[].slice.call(args,1);
                let fn=this.handlers[type];
                fn.forEach((item)=>{
                    item.apply(this,params);//執行函數
                })
                return this;
            }
            once(type,fn){
                let wrap=(...args)=>{
                    fn.apply(this,args);//執行事件後删除
                    this.off(type,wrap);
                }
                this.on(type,wrap);//再添加上去
                return this;
            }
        }
         let emitter=new EventEmitter();
    function fun1(){
        console.log('fun1');
    }
    function fun2(){
        console.log('fun2');
    }
    function fun3(){
        console.log('fun3');
    }
    emitter.on('TEST1',fun1).on('TEST2',fun2).emit('TEST1').once('TEST2',fun3);
    emitter.emit("TEST2");           

80、setTimeout實作setInterval

setTimeout() :在指定的毫秒數後調用函數或計算表達式,隻執行一次。

setInterval() :按照指定的周期(以毫秒計)來調用函數或計算表達式。方法會不停地調用函數,直到 clearInterval() 被調用或視窗被關閉。

思路是使用遞歸函數,不斷地去執行setTimeout進而達到setInterval的效果,看代碼

function mySetInterval(fn, millisec){
  function interval(){
    setTimeout(interval, millisec);
    fn();
  }
  setTimeout(interval, millisec)
}           

這個mySetInterval函數有一個叫做interval的内部函數,它通過setTimeout來自動被調用,在interval中有一個閉包,調用了回調函數并通過setTimeout再次調用了interval。

一個更好的實作我們再增加一個額外的參數用來标明代碼執行的次數

function mySetInterval(fn, millisec,count){
  function interval(){
    if(typeof count===‘undefined’||count-->0){
      setTimeout(interval, millisec);
      try{
        fn()
      }catch(e){
        count = 0;
        throw e.toString();
      }
    }
  }
  setTimeout(interval, millisec)
}           

81、避免回調地獄

使用 async await 配合 promise 是 解決回調地獄的終極方法

async/await特點

1, async/await更加語義化,async 是“異步”的簡寫,async function 用于申明一個 function 是異步的;await,可以認為是async wait的簡寫, 用于等待一個異步方法執行完成;

2, async/await是一個用同步思維解決異步問題的方案(等結果出來之後,代碼才會繼續往下執行)

3, 可以通過多層 async function 的同步寫法代替傳統的callback嵌套           

async function文法

1, 自動将正常函數轉換成Promise,傳回值也是一個Promise對象

2, 隻有async函數内部的異步操作執行完,才會執行then方法指定的回調函數

3, 異步函數内部可以使用await           

await文法

1, await 放置在Promise調用之前,await 強制後面點代碼等待,直到Promise對象resolve,得到resolve的值作為await表達式的運算結果

2. await隻能在async函數内部使用,用在普通函數裡就會報錯           

函數形式

function timeout(ms) {

  return new Promise((resolve, reject) => {

    setTimeout(() => {reject('error')}, ms);  //reject模拟出錯,傳回error

  });

}

async function asyncPrint(ms) {

  try {

     console.log('start');

     await timeout(ms);  //這裡傳回了錯誤

     console.log('end');  //是以這句代碼不會被執行了

  } catch(err) {

     console.log(err); //這裡捕捉到錯誤error

  }

}           

82、callee和caller的作用

caller傳回一個函數的引用,這個函數調用了目前的函數;callee放回正在執行的函數本身的引用,它是arguments的一個屬性

callercaller傳回一個函數的引用,這個函數調用了目前的函數。使用這個屬性要注意:1 這個屬性隻有當函數在執行時才有用2 如果在javascript程式中,函數是由頂層調用的,則傳回null

functionName.caller: functionName是目前正在執行的函數。
var a = function() {
    alert(a.caller);
}
var b = function() {
    a();
}
b();           

上面的代碼中,b調用了a,那麼a.caller傳回的是b的引用,結果如下:

var b = function() {
    a();
}           

如果直接調用a(即a在任何函數中被調用,也就是頂層調用),傳回null:

var a = function() {
    alert(a.caller);
}
var b = function() {
    a();
}
//b();
a();
輸出結果:
null           

callee

callee放回正在執行的函數本身的引用,它是arguments的一個屬性

使用callee時要注意:calleecallee放回正在執行的函數本身的引用,它是arguments的一個屬性使用callee時要注意:

1 這個屬性隻有在函數執行時才有效

2 它有一個length屬性,可以用來獲得形參的個數,是以可以用來比較形參和實參個數是否一緻,即比較arguments.length是否等于arguments.callee.length

3 它可以用來遞歸匿名函數。

var a = function() {
    alert(arguments.callee);
}
var b = function() {
    a();
}
b();


a在b中被調用,但是它傳回了a本身的引用,結果如下:

var a = function() {
    alert(arguments.callee);
}           

83、統計字元串中字母個數或統計最多的字母數

統計字母出現的次數

function count( str ){
    var obj={};
    for(var i=0;i<str.length; i++){
        if(obj[ str[i] ]==undefined){
            //對象初始化;如果key在對象中找不到,那麼會傳回undefined,反向思維
            obj[ str[i] ]= 1;
        } else{
            obj[ str[i] ]++;
        }
    }

    //取出各個字母和它的個數,作為一個新對象儲存在obj對象中
    return obj;

}

cosnle.log( count( "shhkfahkahsadhadskhdskdha" ) );           

統計字元出現次數最多的字母

function allProMax(obj){
    var mm="";
    for(var m in obj){
        if(mm==""){
            mm=new Object();
            mm[m]=obj[m];
        }else{
            for(var j in mm){
               if(mm[j]<obj[m]){
                   //清空原來的内容
                   mm=new Object();
                   //放入新的内容
                   mm[m]=obj[m];
               }
            }
        }
    }
    return mm ;
}

console.log( allProMax(count()) )           

84、面對對象和面向過程的差別

一、面向對象與面向過程的差別
面向過程就是分析出解決問題所需要的步驟,然後用函數把這些步驟一步一步實作,使用的時候一個一個依次調用就可以了;面向對象是把構成問題事務分解成各個對象,建立對象的目的不是為了完成一個步驟,而是為了描叙某個事物在整個解決問題的步驟中的行為。

可以拿生活中的執行個體來了解面向過程與面向對象,例如五子棋,面向過程的設計思路就是首先分析問題的步驟:1、開始遊戲,2、黑子先走,3、繪制畫面,4、判斷輸赢,5、輪到白子,6、繪制畫面,7、判斷輸赢,8、傳回步驟2,9、輸出最後結果。把上面每個步驟用不同的方法來實作。

如果是面向對象的設計思想來解決問題。面向對象的設計則是從另外的思路來解決問題。整個五子棋可以分為1、黑白雙方,這兩方的行為是一模一樣的,2、棋盤系統,負責繪制畫面,3、規則系統,負責判定諸如犯規、輸赢等。第一類對象(玩家對象)負責接受使用者輸入,并告知第二類對象(棋盤對象)棋子布局的變化,棋盤對象接收到了棋子的變化就要負責在螢幕上面顯示出這種變化,同時利用第三類對象(規則系統)來對棋局進行判定。

可以明顯地看出,面向對象是以功能來劃分問題,而不是步驟。同樣是繪制棋局,這樣的行為在面向過程的設計中分散在了多個步驟中,很可能出現不同的繪制版本,因為通常設計人員會考慮到實際情況進行各種各樣的簡化。而面向對象的設計中,繪圖隻可能在棋盤對象中出現,進而保證了繪圖的統一。

上述的内容是從網上查到的,覺得這個例子非常的生動形象,我就寫了下來,現在就應該了解了他倆的差別了吧,其實就是兩句話,面向對象就是高度實物抽象化、面向過程就是自頂向下的程式設計!

二、面向對象的特點
在了解其特點之前,咱們先談談對象,對象就是現實世界存在的任何事務都可以稱之為對象,有着自己獨特的個性

1, 概念 對 具有相同特性的一類事物的抽象描述

2, 組成 屬性 和 方法

3, 模闆 構造函數

4, 特點 封裝 繼承 多态

屬性用來描述具體某個對象的特征。比如小志身高180M,體重70KG,這裡身高、體重都是屬性。
面向對象的思想就是把一切都看成對象,而對象一般都由屬性+方法組成!

屬性屬于對象靜态的一面,用來形容對象的一些特性,方法屬于對象動态的一面,咱們舉一個例子,小明會跑,會說話,跑、說話這些行為就是對象的方法!是以為動态的一面, 我們把屬性和方法稱為這個對象的成員!

類:具有同種屬性的對象稱為類,是個抽象的概念。比如“人”就是一類,期中有一些人名,比如小明、小紅、小玲等等這些都是對象,類就相當于一個模具,他定義了它所包含的全體對象的公共特征和功能,對象就是類的一個執行個體化,小明就是人的一個執行個體化!我們在做程式的時候,經常要将一個變量執行個體化,就是這個原理!我們一般在做程式的時候一般都不用類名的,比如我們在叫小明的時候,不會喊“人,你幹嘛呢!”而是說的是“小明,你在幹嘛呢!”

面向對象有三大特性,分别是封裝性、繼承性和多态性,這裡小編不給予太多的解釋,因為在後邊的部落格會專門總結的!

三、面向過程與面向對象的優缺點
很多資料上全都是一群很難了解的理論知識,整的小編頭都大了,後來發現了一個比較好的文章,寫的真是太棒了,通俗易懂,想要不明白都難!

用面向過程的方法寫出來的程式是一份蛋炒飯,而用面向對象寫出來的程式是一份蓋澆飯。所謂蓋澆飯,北京叫蓋飯,東北叫燴飯,廣東叫碟頭飯,就是在一碗白米飯上面澆上一份蓋菜,你喜歡什麼菜,你就澆上什麼菜。我覺得這個比喻還是比較貼切的。

蛋炒飯制作的細節,我不太清楚,因為我沒當過廚師,也不會做飯,但最後的一道工序肯定是把米飯和雞蛋混在一起炒勻。蓋澆飯呢,則是把米飯和蓋菜分别做好,你如果要一份紅燒肉蓋飯呢,就給你澆一份紅燒肉;如果要一份青椒洋芋蓋澆飯,就給澆一份青椒洋芋絲。

蛋炒飯的好處就是入味均勻,吃起來香。如果恰巧你不愛吃雞蛋,隻愛吃青菜的話,那麼唯一的辦法就是全部倒掉,重新做一份青菜炒飯了。蓋澆飯就沒這麼多麻煩,你隻需要把上面的蓋菜撥掉,更換一份蓋菜就可以了。蓋澆飯的缺點是入味不均,可能沒有蛋炒飯那麼香。

到底是蛋炒飯好還是蓋澆飯好呢?其實這類問題都很難回答,非要比個上下高低的話,就必須設定一個場景,否則隻能說是各有所長。如果大家都不是美食家,沒那麼多講究,那麼從飯館角度來講的話,做蓋澆飯顯然比蛋炒飯更有優勢,他可以組合出來任意多的組合,而且不會浪費。

蓋澆飯的好處就是"菜"“飯"分離,進而提高了制作蓋澆飯的靈活性。飯不滿意就換飯,菜不滿意換菜。用軟體工程的專業術語就是"可維護性"比較好,“飯” 和"菜"的耦合度比較低。蛋炒飯将"蛋”“飯"攪和在一起,想換"蛋”"飯"中任何一種都很困難,耦合度很高,以至于"可維護性"比較差。軟體工程追求的目标之一就是可維護性,可維護性主要表現在3個方面:可了解性、可測試性和可修改性。面向對象的好處之一就是顯著的改善了軟體系統的可維護性。

我們最後簡單總結一下

面向過程

優點:性能比面向對象高,因為類調用時需要執行個體化,開銷比較大,比較消耗資源;比如單片機、嵌入式開發、 Linux/Unix等一般采用面向過程開發,性能是最重要的因素。
缺點:沒有面向對象易維護、易複用、易擴充


面向對象

優點:易維護、易複用、易擴充,由于面向對象有封裝、繼承、多态性的特性,可以設計出低耦合的系統,使系統 更加靈活、更加易于維護
缺點:性能比面向過程低           

85、eval

eval()是全局對象的一個函數屬性。

eval()的參數是一個字元串。如果字元串表示的是表達式,eval()會對表達式進行求值。如果參數表示一個或多個JavaScript語句, 那麼eval()就會執行這些語句。注意不要用eval()來執行一個四則運算表達式;因為 JavaScript 會自動為四則運算求值并不需要用eval來包裹。

這裡的四則運算是指數學上的運算,如:3 + 4 * 4 / 6。注意這裡面并沒有變量,隻是單純的數學運算,這樣的運算式并不需要調用eval來計算,直接在代碼中計算就可以。其實即便帶有變量,JavaScript也是可以直接計算的,但是如果你現在隻想聲明一個帶有變量的表達式,但是想稍後進行運算(你有可能在聲明這個帶有變量的運算式之後還有可能對裡面的變量進行修改),就可以使用eval。
如果要将算數表達式構造成為一個字元串,你可以用eval()在随後對其求值。比如,假如你有一個變量 x ,你可以通過一個字元串表達式來對涉及x的表達式延遲求值,将 “3 * x + 2”,存儲為變量,然後在你的腳本後面的一個地方調用eval()。

如果eval()的參數不是字元串,eval()将會将參數原封不動的傳回。在下面的例子中,字元串構造器被指定,eval()傳回了字元串對象而不是對字元串求值。

// 傳回了包含"2 + 2"的字元串對象
eval(new String("2 + 2"));

// returns 4
eval("2 + 2");


eval() 是一個危險的函數, 他執行的代碼擁有着執行者的權利。如果你用eval()運作的字元串代碼被惡意方(不懷好意的人)操控修改,您可能會利用最終在使用者機器上運作惡意方部署的惡意代碼,并導緻您失去您的網頁或者擴充程式的權限。更重要的是,第三方代碼可以看到某一個eval()被調用時的作用域,這也有可能導緻一些不同方式的攻擊。相似的Function就是不容易被攻擊的。

eval()的運作效率也普遍的比其他的替代方案慢,因為他會調用js解析器,即便現代的JS引擎中已經對此做了優化。

在常見的案例中我們都會找更安全或者更快的方案去替換他           

86、proxy

一、proxy

proxy在目标對象的外層搭建了一層攔截,外界對目标對象的某些操作,必須通過這層攔截

var proxy = new Proxy(target, handler);
new Proxy()表示生成一個Proxy執行個體,target參數表示所要攔截的目标對象,handler參數也是一個對象,用來定制攔截行為

var target = {
  name: 'poetries'
};
var logHandler = {
  get: function(target, key) {
    console.log(`${key} 被讀取`);
    return target[key];
  },
  set: function(target, key, value) {
    console.log(`${key} 被設定為 ${value}`);
    target[key] = value;
  }
}
var targetWithLog = new Proxy(target, logHandler);

targetWithLog.name; // 控制台輸出:name 被讀取
targetWithLog.name = 'others'; // 控制台輸出:name 被設定為 others

console.log(target.name); // 控制台輸出: others
targetWithLog 讀取屬性的值時,實際上執行的是 logHandler.get :在控制台輸出資訊,并且讀取被代理對象 target 的屬性。
在 targetWithLog 設定屬性值時,實際上執行的是 logHandler.set :在控制台輸出資訊,并且設定被代理對象 target 的屬性的值
// 由于攔截函數總是傳回35,是以通路任何屬性都得到35
var proxy = new Proxy({}, {
get: function(target, property) {
  return 35;
}
});

proxy.time // 35
proxy.name // 35
proxy.title // 35
Proxy 執行個體也可以作為其他對象的原型對象

var proxy = new Proxy({}, {
get: function(target, property) {
  return 35;
}
});

let obj = Object.create(proxy);
obj.time // 35           

proxy對象是obj對象的原型,obj對象本身并沒有time屬性,是以根據原型鍊,會在proxy對象上讀取該屬性,導緻被攔截

Proxy的作用

對于代理模式 Proxy 的作用主要展現在三個方面

攔截和監視外部對對象的通路降低函數或類的複雜度在複雜操作前對操作進行校驗或對所需資源進行管理

二、Proxy所能代理的範圍--handler

實際上 handler 本身就是ES6所新設計的一個對象.它的作用就是用來 自定義代理對象的各種可代理操作 。它本身一共有13中方法,每種方法都可以代理一種操作.其13種方法如下

// 在讀取代理對象的原型時觸發該操作,比如在執行 Object.getPrototypeOf(proxy) 時。
handler.getPrototypeOf()

// 在設定代理對象的原型時觸發該操作,比如在執行 Object.setPrototypeOf(proxy, null) 時。
handler.setPrototypeOf()


// 在判斷一個代理對象是否是可擴充時觸發該操作,比如在執行 Object.isExtensible(proxy) 時。
handler.isExtensible()


// 在讓一個代理對象不可擴充時觸發該操作,比如在執行 Object.preventExtensions(proxy) 時。
handler.preventExtensions()

// 在擷取代理對象某個屬性的屬性描述時觸發該操作,比如在執行 Object.getOwnPropertyDescriptor(proxy, "foo") 時。
handler.getOwnPropertyDescriptor()


// 在定義代理對象某個屬性時的屬性描述時觸發該操作,比如在執行 Object.defineProperty(proxy, "foo", {}) 時。
andler.defineProperty()


// 在判斷代理對象是否擁有某個屬性時觸發該操作,比如在執行 "foo" in proxy 時。
handler.has()

// 在讀取代理對象的某個屬性時觸發該操作,比如在執行 proxy.foo 時。
handler.get()


// 在給代理對象的某個屬性指派時觸發該操作,比如在執行 proxy.foo = 1 時。
handler.set()

// 在删除代理對象的某個屬性時觸發該操作,比如在執行 delete proxy.foo 時。
handler.deleteProperty()

// 在擷取代理對象的所有屬性鍵時觸發該操作,比如在執行 Object.getOwnPropertyNames(proxy) 時。
handler.ownKeys()

// 在調用一個目标對象為函數的代理對象時觸發該操作,比如在執行 proxy() 時。
handler.apply()


// 在給一個目标對象為構造函數的代理對象構造執行個體時觸發該操作,比如在執行new proxy() 時。
handler.construct()           

三、Proxy場景

3.1 實作私有變量

var target = {
   name: 'poetries',
   _age: 22
}

var logHandler = {
  get: function(target,key){
    if(key.startsWith('_')){
      console.log('私有變量age不能被通路')
      return false
    }
    return target[key];
  },
  set: function(target, key, value) {
     if(key.startsWith('_')){
      console.log('私有變量age不能被修改')
      return false
    }
     target[key] = value;
   }
}
var targetWithLog = new Proxy(target, logHandler);

// 私有變量age不能被通路
targetWithLog.name;

// 私有變量age不能被修改
targetWithLog.name = 'others';
在下面的代碼中,我們聲明了一個私有的 apiKey,便于 api 這個對象内部的方法調用,但不希望從外部也能夠通路 api._apiKey

var api = {
    _apiKey: '123abc456def',
    /* mock methods that use this._apiKey */
    getUsers: function(){},
    getUser: function(userId){},
    setUser: function(userId, config){}
};

// logs '123abc456def';
console.log("An apiKey we want to keep private", api._apiKey);

// get and mutate _apiKeys as desired
var apiKey = api._apiKey;
api._apiKey = '987654321';
很顯然,約定俗成是沒有束縛力的。使用 ES6 Proxy 我們就可以實作真實的私有變量了,下面針對不同的讀取方式示範兩個不同的私有化方法。第一種方法是使用 set / get 攔截讀寫請求并傳回 undefined:

let api = {
    _apiKey: '123abc456def',
    getUsers: function(){ },
    getUser: function(userId){ },
    setUser: function(userId, config){ }
};

const RESTRICTED = ['_apiKey'];
api = new Proxy(api, {
    get(target, key, proxy) {
        if(RESTRICTED.indexOf(key) > -1) {
            throw Error(`${key} is restricted. Please see api documentation for further info.`);
        }
        return Reflect.get(target, key, proxy);
    },
    set(target, key, value, proxy) {
        if(RESTRICTED.indexOf(key) > -1) {
            throw Error(`${key} is restricted. Please see api documentation for further info.`);
        }
        return Reflect.get(target, key, value, proxy);
    }
});

// 以下操作都會抛出錯誤
console.log(api._apiKey);
api._apiKey = '987654321';
第二種方法是使用 has 攔截 in 操作

var api = {
    _apiKey: '123abc456def',
    getUsers: function(){ },
    getUser: function(userId){ },
    setUser: function(userId, config){ }
};

const RESTRICTED = ['_apiKey'];
api = new Proxy(api, {
    has(target, key) {
        return (RESTRICTED.indexOf(key) > -1) ?
            false :
            Reflect.has(target, key);
    }
});

// these log false, and `for in` iterators will ignore _apiKey
console.log("_apiKey" in api);

for (var key in api) {
    if (api.hasOwnProperty(key) && key === "_apiKey") {
        console.log("This will never be logged because the proxy obscures _apiKey...")
    }
}           

3.2 抽離校驗子產品

讓我們從一個簡單的類型校驗開始做起,這個示例示範了如何使用 Proxy 保障資料類型的準确性

let numericDataStore = {
    count: 0,
    amount: 1234,
    total: 14
};

numericDataStore = new Proxy(numericDataStore, {
    set(target, key, value, proxy) {
        if (typeof value !== 'number') {
            throw Error("Properties in numericDataStore can only be numbers");
        }
        return Reflect.set(target, key, value, proxy);
    }
});

// 抛出錯誤,因為 "foo" 不是數值
numericDataStore.count = "foo";

// 指派成功
numericDataStore.count = 333;
如果要直接為對象的所有屬性開發一個校驗器可能很快就會讓代碼結構變得臃腫,使用 Proxy 則可以将校驗器從核心邏輯分離出來自成一體

function createValidator(target, validator) {
    return new Proxy(target, {
        _validator: validator,
        set(target, key, value, proxy) {
            if (target.hasOwnProperty(key)) {
                let validator = this._validator[key];
                if (!!validator(value)) {
                    return Reflect.set(target, key, value, proxy);
                } else {
                    throw Error(`Cannot set ${key} to ${value}. Invalid.`);
                }
            } else {
                throw Error(`${key} is not a valid property`)
            }
        }
    });
}

const personValidators = {
    name(val) {
        return typeof val === 'string';
    },
    age(val) {
        return typeof age === 'number' && val > 18;
    }
}
class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
        return createValidator(this, personValidators);
    }
}

const bill = new Person('Bill', 25);

// 以下操作都會報錯
bill.name = 0;
bill.age = 'Bill';
bill.age = 15;
通過校驗器和主邏輯的分離,你可以無限擴充 personValidators 校驗器的内容,而不會對相關的類或函數造成直接破壞。更複雜一點,我們還可以使用 Proxy 模拟類型檢查,檢查函數是否接收了類型和數量都正确的參數

let obj = {
    pickyMethodOne: function(obj, str, num) { /* ... */ },
    pickyMethodTwo: function(num, obj) { /*... */ }
};

const argTypes = {
    pickyMethodOne: ["object", "string", "number"],
    pickyMethodTwo: ["number", "object"]
};

obj = new Proxy(obj, {
    get: function(target, key, proxy) {
        var value = target[key];
        return function(...args) {
            var checkArgs = argChecker(key, args, argTypes[key]);
            return Reflect.apply(value, target, args);
        };
    }
});

function argChecker(name, args, checkers) {
    for (var idx = 0; idx < args.length; idx++) {
        var arg = args[idx];
        var type = checkers[idx];
        if (!arg || typeof arg !== type) {
            console.warn(`You are incorrectly implementing the signature of ${name}. Check param ${idx + 1}`);
        }
    }
}

obj.pickyMethodOne();
// > You are incorrectly implementing the signature of pickyMethodOne. Check param 1
// > You are incorrectly implementing the signature of pickyMethodOne. Check param 2
// > You are incorrectly implementing the signature of pickyMethodOne. Check param 3

obj.pickyMethodTwo("wopdopadoo", {});
// > You are incorrectly implementing the signature of pickyMethodTwo. Check param 1

// No warnings logged
obj.pickyMethodOne({}, "a little string", 123);
obj.pickyMethodOne(123, {});           

3.3 通路日志

對于那些調用頻繁、運作緩慢或占用執行環境資源較多的屬性或接口,開發者會希望記錄它們的使用情況或性能表現,這個時候就可以使用 Proxy 充當中間件的角色,輕而易舉實作日志功能

let api = {
    _apiKey: '123abc456def',
    getUsers: function() { /* ... */ },
    getUser: function(userId) { /* ... */ },
    setUser: function(userId, config) { /* ... */ }
};

function logMethodAsync(timestamp, method) {
    setTimeout(function() {
        console.log(`${timestamp} - Logging ${method} request asynchronously.`);
    }, 0)
}

api = new Proxy(api, {
    get: function(target, key, proxy) {
        var value = target[key];
        return function(...arguments) {
            logMethodAsync(new Date(), key);
            return Reflect.apply(value, target, arguments);
        };
    }
});

api.getUsers();           

3.4 預警和攔截

假設你不想讓其他開發者删除 noDelete 屬性,還想讓調用 oldMethod 的開發者了解到這個方法已經被廢棄了,或者告訴開發者不要修改 doNotChange 屬性,那麼就可以使用 Proxy 來實作

let dataStore = {
    noDelete: 1235,
    oldMethod: function() {/*...*/ },
    doNotChange: "tried and true"
};

const NODELETE = ['noDelete'];
const NOCHANGE = ['doNotChange'];
const DEPRECATED = ['oldMethod'];

dataStore = new Proxy(dataStore, {
    set(target, key, value, proxy) {
        if (NOCHANGE.includes(key)) {
            throw Error(`Error! ${key} is immutable.`);
        }
        return Reflect.set(target, key, value, proxy);
    },
    deleteProperty(target, key) {
        if (NODELETE.includes(key)) {
            throw Error(`Error! ${key} cannot be deleted.`);
        }
        return Reflect.deleteProperty(target, key);

    },
    get(target, key, proxy) {
        if (DEPRECATED.includes(key)) {
            console.warn(`Warning! ${key} is deprecated.`);
        }
        var val = target[key];

        return typeof val === 'function' ?
            function(...args) {
                Reflect.apply(target[key], target, args);
            } :
            val;
    }

});

// these will throw errors or log warnings, respectively
dataStore.doNotChange = "foo";
delete dataStore.noDelete;
dataStore.oldMethod();           

3.5 過濾操作

某些操作會非常占用資源,比如傳輸大檔案,這個時候如果檔案已經在分塊發送了,就不需要在對新的請求作出相應(非絕對),這個時候就可以使用 Proxy 對當請求進行特征檢測,并根據特征過濾出哪些是不需要響應的,哪些是需要響應的。下面的代碼簡單示範了過濾特征的方式,并不是完整代碼,相信大家會了解其中的妙處

let obj = {
    getGiantFile: function(fileId) {/*...*/ }
};

obj = new Proxy(obj, {
    get(target, key, proxy) {
        return function(...args) {
            const id = args[0];
            let isEnroute = checkEnroute(id);
            let isDownloading = checkStatus(id);
            let cached = getCached(id);

            if (isEnroute || isDownloading) {
                return false;
            }
            if (cached) {
                return cached;
            }
            return Reflect.apply(target[key], target, args);
        }
    }

});           

繼續閱讀