整合營銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          JavaScript 使用誤區(qū)

          賦值運(yùn)算符應(yīng)用錯誤

          在 JavaScript 程序中如果你在 if 條件語句中使用賦值運(yùn)算符的等號 (=) 將會產(chǎn)生一個錯誤結(jié)果, 正確的方法是使用比較運(yùn)算符的兩個等號 (==)。

          if 條件語句返回 false (是我們預(yù)期的)因為 x 不等于 10:

          var x = 0;

          if (x == 10)

          if 條件語句返回 true (不是我們預(yù)期的)因為條件語句執(zhí)行為 x 賦值 10,10 為 true:

          var x = 0;

          if (x = 10)

          if 條件語句返回 false (不是我們預(yù)期的)因為條件語句執(zhí)行為 x 賦值 0,0 為 false:

          var x = 0;

          if (x = 0)

          賦值語句返回變量的值。

          比較運(yùn)算符常見錯誤

          在常規(guī)的比較中,數(shù)據(jù)類型是被忽略的,以下 if 條件語句返回 true:

          var x = 10;

          var y = "10";

          if (x == y)

          在嚴(yán)格的比較運(yùn)算中,=== 為恒等計算符,同時檢查表達(dá)式的值與類型,以下 if 條件語句返回 false:

          var x = 10;

          var y = "10";

          if (x === y)

          這種錯誤經(jīng)常會在 switch 語句中出現(xiàn),switch 語句會使用恒等計算符(===)進(jìn)行比較:

          以下實(shí)例會執(zhí)行 alert 彈窗:

          var x = 10;

          switch(x) {

          case 10: alert("Hello");

          }

          以下實(shí)例由于類型不一致不會執(zhí)行 alert 彈窗:

          var x = 10;

          switch(x) {

          case "10": alert("Hello");

          }

          加法與連接注意事項

          加法是兩個數(shù)字相加。

          連接是兩個字符串連接。

          JavaScript 的加法和連接都使用 + 運(yùn)算符。

          接下來我們可以通過實(shí)例查看兩個數(shù)字相加及數(shù)字與字符串連接的區(qū)別:

          var x = 10 + 5; // x 的結(jié)果為 15

          var x = 10 + "5"; // x 的結(jié)果為 "105"

          使用變量相加結(jié)果也不一致:

          var x = 10;

          var y = 5;

          var z = x + y; // z 的結(jié)果為 15

          var x = 10;

          var y = "5";

          var z = x + y; // z 的結(jié)果為 "105"

          浮點(diǎn)型數(shù)據(jù)使用注意事項

          JavaScript 中的所有數(shù)據(jù)都是以 64 位浮點(diǎn)型數(shù)據(jù)(float) 來存儲。

          所有的編程語言,包括 JavaScript,對浮點(diǎn)型數(shù)據(jù)的精確度都很難確定:

          var x = 0.1;

          var y = 0.2;

          var z = x + y // z 的結(jié)果為 0.3

          if (z == 0.3) // 返回 false

          我解決以上問題,可以用整數(shù)的乘除法來解決:

          實(shí)例

          var z = (x * 10 + y * 10) / 10; // z 的結(jié)果為 0.3

          JavaScript 字符串分行

          JavaScript 運(yùn)行我們在字符串中使用斷行語句:

          實(shí)例 1

          var x =

          "Hello World!";

          但是,在字符串中直接使用回車換行是會報錯的:

          實(shí)例 2

          var x = "Hello

          World!";

          我們可以在選擇開發(fā)工具或按下 F12 來查看錯誤信息:

          字符串?dāng)嘈行枰褂梅葱备?\),如下所示:

          實(shí)例 3

          var x = "Hello \

          World!";

          錯誤的使用分號

          以下實(shí)例中,由于分號使用錯誤,if 語句中的代碼塊將無法執(zhí)行:

          if (x == 19);

          {

          // code block

          }

          Return 語句使用注意事項

          JavaScript 默認(rèn)是在代碼的最后一行自動結(jié)束。

          以下兩個實(shí)例返回結(jié)果是一樣的(一個有分號一個沒有):

          實(shí)例 1

          function myFunction(a) {

          var power = 10

          return a * power

          }

          實(shí)例 2

          function myFunction(a) {

          var power = 10;

          return a * power;

          }

          JavaScript 也可以使用多行來結(jié)束一個語句。

          以下實(shí)例返回相同的結(jié)果:

          實(shí)例 3

          function myFunction(a) {

          var

          power = 10;

          return a * power;

          }

          但是,以下實(shí)例結(jié)果會返回 undefined

          實(shí)例 4

          function myFunction(a) {

          var

          power = 10;

          return

          a * power;

          }

          為什么會有這樣的結(jié)果呢?因為在 JavaScript 中,實(shí)例 4 的代碼與下面的代碼一致:

          function myFunction(a) {

          var

          power = 10;

          return; // 分號結(jié)束,返回 undefined

          a * power;}

          解析

          如果是一個不完整的語句,如下所示:

          var

          JavaScript 將嘗試讀取第二行的語句:

          power = 10;

          但是由于這樣的語句是完整的:

          return

          JavaScript 將自動關(guān)閉語句:

          return;

          在 JavaScript 中,分號是可選的 。

          由于 return 是一個完整的語句,所以 JavaScript 將關(guān)閉 return 語句。

          注意:不用對 return 語句進(jìn)行斷行。

          數(shù)組中使用名字來索引

          許多程序語言都允許使用名字來作為數(shù)組的索引。

          使用名字來作為索引的數(shù)組稱為關(guān)聯(lián)數(shù)組(或哈希)。

          JavaScript 不支持使用名字來索引數(shù)組,只允許使用數(shù)字索引。

          實(shí)例

          var person = [];

          person[0] = "John";

          person[1] = "Doe";

          person[2] = 46;

          var x = person.length; // person.length 返回 3

          var y = person[0]; // person[0] 返回 "John"

          在 JavaScript 中, 對象 使用 名字作為索引

          如果你使用名字作為索引,當(dāng)訪問數(shù)組時,JavaScript 會把數(shù)組重新定義為標(biāo)準(zhǔn)對象。

          執(zhí)行這樣操作后,數(shù)組的方法及屬性將不能再使用,否則會產(chǎn)生錯誤:

          實(shí)例

          var person = [];

          person["firstName"] = "John";

          person["lastName"] = "Doe";

          person["age"] = 46;

          var x = person.length; // person.length 返回 0

          var y = person[0]; // person[0] 返回 undefined

          定義數(shù)組元素,最后不能添加逗號

          錯誤的定義方式:

          points = [40, 100, 1, 5, 25, 10,];

          正確的定義方式:

          points = [40, 100, 1, 5, 25, 10];

          定義對象,最后不能添加逗號

          錯誤的定義方式:

          websites = {site:"菜鳥教程", url:"www.runoob.com", like:460,}

          正確的定義方式:

          websites = {site:"菜鳥教程", url:"www.runoob.com", like:460}

          Undefined 不是 Null

          在 JavaScript 中, null 用于對象, undefined 用于變量,屬性和方法。

          對象只有被定義才有可能為 null,否則為 undefined。

          如果我們想測試對象是否存在,在對象還沒定義時將會拋出一個錯誤。

          錯誤的使用方式:

          if (myObj !== null && typeof myObj !== "undefined")

          正確的方式是我們需要先使用 typeof 來檢測對象是否已定義:

          if (typeof myObj !== "undefined" && myObj !== null)

          程序塊作用域

          在每個代碼塊中 JavaScript 不會創(chuàng)建一個新的作用域,一般各個代碼塊的作用域都是全局的。

          以下代碼的的變量 i 返回 10,而不是 undefined:

          實(shí)例

          for (var i = 0; i < 10; i++) {

          // some code

          }

          return i;

          如您還有不明白的可以在下面與我留言或是與我探討QQ群308855039,我們一起飛!



          最近與部門老大一起面試了許多前端求職者,其中「想換個學(xué)習(xí)氛圍較好的人占多數(shù)」,但良好的學(xué)習(xí)氛圍也是需要一點(diǎn)點(diǎn)營造出來的。

          為此我們組建了我們團(tuán)隊內(nèi)部的“「現(xiàn)代 JavaScript 突擊隊」”,第一期學(xué)習(xí)內(nèi)容為《現(xiàn)代 JavaScript 教程》系列,幫助小組成員系統(tǒng)地進(jìn)行學(xué)習(xí)鞏固,并「讓大家養(yǎng)成系統(tǒng)性學(xué)習(xí)和輸出學(xué)習(xí)總結(jié)的學(xué)習(xí)方式」

          本文作為我輸出的第一部分學(xué)習(xí)總結(jié),希望作為一份自測清單,幫助大家鞏固知識,溫故知新。

          這里也下面分享我們學(xué)習(xí)小組的“押金制度”和“押金記錄表”

          “押金制度”和“押金記錄表”

          接下來開始分享自測清單的內(nèi)容。

          初中級前端 JavaScript 自測清單.png

          一、Hello World!

          1. 腳本引入方式

          JavaScript 腳本引入方式有兩種:

          • <script> 標(biāo)簽插入腳本;
          • <script> 標(biāo)簽 src 設(shè)置腳本地址。

          2. script 標(biāo)簽屬性

          <script> 標(biāo)簽有以下常用屬性:

          2.1 src

          src :指定外部腳本的URI, 如果設(shè)置了 src 特性,script 標(biāo)簽內(nèi)容將會被忽略;

          <script src="example-url.js"></script>

          2.2 type

          type :指定引用腳本的語言,屬性值為 MIME 類型,包括text/javascript, text/ecmascript, application/javascript, 和application/ecmascript。如果沒有定義這個屬性,腳本會被視作JavaScript。

          ES6 新增了屬性值 module ,代碼會被當(dāng)做 JavaScript 模塊。

          <script type="text/javascript"></script>

          2.3 async

          async 規(guī)定一旦腳本可用,則會異步執(zhí)行。 注意:async 屬性僅適用于外部腳本(「只有在使用 src 屬性時」)。 有多種執(zhí)行外部腳本的方法: 如果 async="async" :腳本相對于頁面的其余部分異步地執(zhí)行(當(dāng)頁面繼續(xù)進(jìn)行解析時,腳本將被執(zhí)行); 如果不使用 async 且 defer="defer" :腳本將在頁面完成解析時執(zhí)行; 如果既不使用 async 也不使用 defer :在瀏覽器繼續(xù)解析頁面之前,立即讀取并執(zhí)行腳本;

          <script async="async"></script>

          2.4 defer

          defer 屬性規(guī)定是否對腳本執(zhí)行進(jìn)行延遲,直到頁面加載為止。

          如果您的腳本不會改變文檔的內(nèi)容,可將 defer 屬性加入到 <script> 標(biāo)簽中,以便加快處理文檔的速度。因為瀏覽器知道它將能夠安全地讀取文檔的剩余部分而不用執(zhí)行腳本,它將推遲對腳本的解釋,直到文檔已經(jīng)顯示給用戶為止。

          <script defer="defer"></script>

          詳細(xì)介紹可以閱讀《MDN <script>章節(jié) 》。

          二、代碼結(jié)構(gòu)

          1. 語句

          語句是執(zhí)行行為(action)的語法結(jié)構(gòu)和命令。如: alert('Hello, world!') 這樣可以用來顯示消息的語句。

          2. 分號

          存在分行符時,多數(shù)情況下可以省略分號。但不全是,比如:

          alert(3 +
          1
          + 2);

          建議新人最好不要省略分號。

          3. 注釋

          「單行注釋以兩個正斜杠字符 // 開始。」

          // 注釋文本
          console.log("leo");

          「多行注釋以一個正斜杠和星號開始 “/*” 并以一個星號和正斜桿結(jié)束 “*/”。」

          /*
          這是多行注釋。
          第二行注釋。
          */
          console.log("leo");

          三、現(xiàn)代模式,"use strict"

          1. 作用

          JavaScript 的嚴(yán)格模式是使用受限制的 JavaScript 的一種方式,從而隱式地退出“草率模式”。

          "use strict" 指令將瀏覽器引擎轉(zhuǎn)換為“現(xiàn)代”模式,改變一些內(nèi)建特性的行為。

          2. 使用

          通過在腳本文件/函數(shù)開頭添加 "use strict"; 聲明,即可啟用嚴(yán)格模式。 全局開啟嚴(yán)格模式:

          // index.js
          "use strict";
          const v = "Hi!  I'm a strict mode script!";

          函數(shù)內(nèi)開啟嚴(yán)格模式:

          // index.js
          function strict() {
            'use strict';
            function nested() { 
              return "And so am I!"; 
            }
            return "Hi!  I'm a strict mode function!  " + nested();
          }

          3. 注意點(diǎn)

          1. "use strict" 需要定義在腳本最頂部(函數(shù)內(nèi)除外),否則嚴(yán)格模式可能無法啟用。
          2. 一旦進(jìn)入了嚴(yán)格模式,就無法關(guān)閉嚴(yán)格模式。

          4. 體驗

          啟用 "use strict" 后,為未定義元素賦值將拋出異常:

          "use strict";
          leo = 17; // Uncaught ReferenceError: leo is not defined

          啟用 "use strict" 后,試圖刪除不可刪除的屬性時會拋出異常:

          "use strict";
          delete Object.prototype; // Uncaught TypeError: Cannot delete property 'prototype' of function Object() { [native code] }

          詳細(xì)介紹可以閱讀《MDN 嚴(yán)格模式章節(jié) 》。

          四、變量

          1. 介紹

          變量是數(shù)據(jù)的“命名存儲”。

          2. 使用

          目前定義變量可以使用三種關(guān)鍵字:var / let / const。三者區(qū)別可以閱讀《let 和 const 命令》 。

          let name = "leo";
          let name = "leo", age, addr;
          let name = "leo", age = 27, addr = "fujian";

          3. 命名建議

          變量命名有 2 個限制:

          1. 變量名稱必須僅包含「字母,數(shù)字,符號」 $ 和 _。
          2. 首字符必須「非數(shù)字」。 變量命名還有一些建議:
          • 常量一般用全大寫,如 const PI = 3.141592 ;
          • 使用易讀的命名,比如 userName 或者 shoppingCart。

          4. 注意點(diǎn)

          • JavaScript 變量名稱區(qū)分大小寫,如變量 leo 與 Leo 是不同的;
          • JavaScript 變量名稱允許非英文字母,但不推薦,如 let 平安 = "leo" ;
          • 避免使用 a、b、c 這種縮寫。

          五、數(shù)據(jù)類型

          JavaScript 是一種「弱類型」或者說「動態(tài)語言」。這意味著你不用提前聲明變量的類型,在程序運(yùn)行過程中,類型會被自動確定。這也意味著你可以使用同一個變量保存不同類型的數(shù)據(jù):

          var foo = 42;    // foo is a Number now
          foo = "bar"; // foo is a String now
          foo = true;  // foo is a Boolean now

          詳細(xì)介紹可以閱讀《MDN JavaScript 數(shù)據(jù)類型和數(shù)據(jù)結(jié)構(gòu) 》。

          1. 八大數(shù)據(jù)類型

          前七種為基本數(shù)據(jù)類型,也稱為原始類型(值本身無法被改變),而 object 為復(fù)雜數(shù)據(jù)類型。 八大數(shù)據(jù)類型分別是:

          • number 用于任何類型的數(shù)字:整數(shù)或浮點(diǎn)數(shù),在 ±2 范圍內(nèi)的整數(shù)。
          • bigint 用于任意長度的整數(shù)。
          • string 用于字符串:一個字符串可以包含一個或多個字符,所以沒有單獨(dú)的單字符類型。
          • boolean 用于 true 和 false。
          • null 用于未知的值 —— 只有一個 null 值的獨(dú)立類型。
          • undefined 用于未定義的值 —— 只有一個 undefined 值的獨(dú)立類型。
          • symbol 用于唯一的標(biāo)識符。
          • object 用于更復(fù)雜的數(shù)據(jù)結(jié)構(gòu)。 「每個類型后面會詳細(xì)介紹。」

          2. 檢測數(shù)據(jù)類型

          通過 typeof 運(yùn)算符檢查:

          • 兩種形式:typeof x 或者 typeof(x)。
          • 以字符串的形式返回類型名稱,例如 "string"。
          • typeof null 會返回 "object" —— 這是 JavaScript 編程語言的一個錯誤,實(shí)際上它并不是一個 object。
          typeof "leo" // "string"
          typeof undefined    // "undefined"
          typeof 0     // "number"
          typeof NaN   // "number"
          typeof 10n   // "bigint"
          typeof true  // "boolean"
          typeof Symbol("id") // "symbol"
          typeof [1,2,3,4]    // "object"
          typeof Math  // "object"  (1) Math 是一個提供數(shù)學(xué)運(yùn)算的內(nèi)建 object。
          typeof null  // "object"  (2) JavaScript 語言的一個錯誤,null 不是一個 object。null 有自己的類型,它是一個特殊值。
          typeof alert // "function"  (3) alert 在 JavaScript 語言中是一個函數(shù)。

          六、類型轉(zhuǎn)換

          JavaScript 變量可以轉(zhuǎn)換為新變量或其他數(shù)據(jù)類型:

          • 通過使用 JavaScript 函數(shù)
          • 通過 JavaScript 自身自動轉(zhuǎn)換

          1. 字符串轉(zhuǎn)換

          通過全局方法 String() 將**其他類型數(shù)據(jù)(任何類型的數(shù)字,字母,布爾值,對象)**轉(zhuǎn)換為 String 類型:

          String(123);   // "123"
          // Number方法toString()/toExponential()/toFixed()/toPrecision() 也有同樣效果。
          String(false); // "false"
          // Boolean方法 toString() 也有同樣效果。
          String(new Date()); // "Sun Jun 07 2020 21:44:20 GMT+0800 (中國標(biāo)準(zhǔn)時間)"
          // Date方法 toString() 也有同樣效果。
          String(leo);

          2. 數(shù)值轉(zhuǎn)換

          通過以下幾種方式能將其他類型數(shù)據(jù)轉(zhuǎn)換為 Number 類型:

          • 一元運(yùn)算符 +
          const age = +"22"; // 22
          • Number 方法
          const age = Number("22"); // 22
          Number.parseFloat("22");  // 22
          Number.parseInt("22");  // 22
          • 其他方式轉(zhuǎn) Number 類型
          // 布爾值
          Number(false)     // 返回 0
          Number(true)      // 返回 1
          // 日期
          const date = new Date();
          Number(date);     // 返回 1591537858154
          date.getTime();   // 返回 1591537858154,效果一致。
          // 自動轉(zhuǎn)換
          5 + null    // 返回 5         null 轉(zhuǎn)換為 0
          "5" + null  // 返回"5null"   null 轉(zhuǎn)換為 "null"
          "5" + 1     // 返回 "51"      1 轉(zhuǎn)換為 "1" 
          "5" - 1     // 返回 4         "5" 轉(zhuǎn)換為 5

          3. 布爾值轉(zhuǎn)換

          轉(zhuǎn)換規(guī)則如下:

          • 直觀上為“空”的值(如 0、空字符串、null、undefined 和 NaN)將變?yōu)?false。
          • 其他值變成 true。
          Boolean(1); // true
          Boolean(0); // false
          Boolean("hello"); // true
          Boolean(""); // false
          Boolean("0"); // true
          Boolean(" "); // 空白, 也是 true (任何非空字符串是 true)

          4. 小結(jié)

          類型轉(zhuǎn)換

          七、運(yùn)算符

          1、運(yùn)算符概念

          常見運(yùn)算符如加法 + 、減法 - 、乘法 * 和除法 / ,舉一個例子,來介紹一些概念:

          let sum = 1 + 2;
          let age = +18;

          其中:

          • 加法運(yùn)算 1 + 2 中, 1 和 2 為 2 個運(yùn)算元,左運(yùn)算元 1 和右運(yùn)算元 2 ,即「運(yùn)算元就是運(yùn)算符作用的對象。」
          • 1 + 2 運(yùn)算式中包含 2 個運(yùn)算元,因此也稱該運(yùn)算式中的加號 + 為 「二元運(yùn)算符。」
          • 在 +18 中的加號 + 對應(yīng)只有一個運(yùn)算元,則它是 「一元運(yùn)算符」

          2、+ 號運(yùn)算符

          let msg = "hello " + "leo"; // "hello leo"
          let total = 10 + 20;  // 30
          let text1 = "1" + "2"; // "12"
          let text2 = "1" + 2;   // "12"
          let text3 = 1 + "2";   // "12"
          let text4 = 1 + 2 + "3";  // "33"
          let num = +text1; //  12 轉(zhuǎn)換為 Number 類型

          3、運(yùn)算符優(yōu)先級

          運(yùn)算符的優(yōu)先級決定了表達(dá)式中運(yùn)算執(zhí)行的先后順序,優(yōu)先級高的運(yùn)算符最先被執(zhí)行。 下面的表將所有運(yùn)算符按照優(yōu)先級的不同從高(20)到低(1)排列。

          優(yōu)先級 運(yùn)算類型 關(guān)聯(lián)性 運(yùn)算符 20 圓括號 n/a(不相關(guān)) ( … ) 19 成員訪問 從左到右 … . … 需計算的成員訪問 從左到右 … [ … ] new (帶參數(shù)列表) n/a new … ( … ) 函數(shù)調(diào)用 從左到右 … ( … ) 可選鏈(Optional chaining) 從左到右 ?. 18 new (無參數(shù)列表) 從右到左 new … 17 后置遞增(運(yùn)算符在后) n/a … ++ 后置遞減(運(yùn)算符在后) … -- 16 邏輯非 從右到左 ! … 按位非 ~ … 一元加法 + … 一元減法 - … 前置遞增 ++ … 前置遞減 -- … typeof typeof … void void … delete delete … await await … 15 冪 從右到左 … ** … 14 乘法 從左到右 … * … 除法 … / … 取模 … % … 13 加法 從左到右 … + … 減法 … - … 12 按位左移 從左到右 … << … 按位右移 … >> … 無符號右移 … >>> … 11 小于 從左到右 … < … 小于等于 … <= … 大于 … > … 大于等于 … >= … in … in … instanceof … instanceof … 10 等號 從左到右 … == … 非等號 … != … 全等號 … === … 非全等號 … !== … 9 按位與 從左到右 … & … 8 按位異或 從左到右 … ^ … 7 按位或 從左到右 … | … 6 邏輯與 從左到右 … && … 5 邏輯或 從左到右 … || … 4 條件運(yùn)算符 從右到左 … ? … : … 3 賦值 從右到左 … = … … += … … -= … … *= … … /= … … %= … … <<= … … >>= … … >>>= … … &= … … ^= … … |= … 2 yield 從右到左 yield … yield* yield* … 1 展開運(yùn)算符 n/a ... … 0 逗號 從左到右 … , …

          3 > 2 && 2 > 1
          // return true
          3 > 2 > 1
          // 返回 false,因為 3 > 2 是 true,并且 true > 1 is false
          // 加括號可以更清楚:(3 > 2) > 1

          八、值的比較

          1. 常見比較

          在 JS 中的值的比較與數(shù)學(xué)很類型:

          • 大于/小于/大于等于/小于等于: a>b / a<b / a>=b / a<=b ;
          • 判斷相等:
          // 使用 ==,非嚴(yán)格等于,不關(guān)心值類型
          // == 運(yùn)算符會對比較的操作數(shù)做隱式類型轉(zhuǎn)換,再比較
          '1' == 1; // true
          // 使用 ===,嚴(yán)格相等,關(guān)心值類型
          // 將數(shù)字值 -0 和 +0 視為相等,并認(rèn)為 Number.NaN 不等于 NaN。
          '1' === 1; // false

          (圖片來自:《MDN JavaScript 中的相等性判斷》)

          • 判斷不相等: 和判斷相等一樣,也有兩種: != / !== 。

          2. 相等性判斷(Object.is())

          另外 ES6 新增 Object.is 方法判斷兩個值是否相同,語法如下:

          Object.is(value1, value2);

          以下任意項成立則兩個值相同:

          • 兩個值都是 undefined
          • 兩個值都是 null
          • 兩個值都是 true 或者都是 false
          • 兩個值是由相同個數(shù)的字符按照相同的順序組成的字符串
          • 兩個值指向同一個對象
          • 兩個值都是數(shù)字并且 都是正零 +0都是負(fù)零 -0都是 NaN都是除零和 NaN 外的其它同一個數(shù)字 使用示例:
          Object.is('foo', 'foo');     // true
          Object.is(window, window);   // true
          Object.is('foo', 'bar');     // false
          Object.is([], []);           // false
          var foo = { a: 1 };
          var bar = { a: 1 };
          Object.is(foo, foo);         // true
          Object.is(foo, bar);         // false
          Object.is(null, null);       // true
          // 特例
          Object.is(0, -0);            // false
          Object.is(0, +0);            // true
          Object.is(-0, -0);           // true
          Object.is(NaN, 0/0);         // true

          兼容性 Polyfill 處理:

          if (!Object.is) {
            Object.is = function(x, y) {
              // SameValue algorithm
              if (x === y) { // Steps 1-5, 7-10
                // Steps 6.b-6.e: +0 != -0
                return x !== 0 || 1 / x === 1 / y;
              } else {
                // Step 6.a: NaN == NaN
                return x !== x && y !== y;
              }
            };
          }

          3. null 與 undefined 比較

          對于相等性判斷比較簡單:

          null == undefined;  // true
          null === undefined; // false

          對于其他比較,它們會先轉(zhuǎn)換位數(shù)字: null 轉(zhuǎn)換為 0 , undefied 轉(zhuǎn)換為 NaN 。

          null > 0;  // false 1
          null >= 0; // true  2
          null == 0; // false 3
          null < 1;  // true  4

          需要注意: null == 0; // false 這里是因為:undefined 和 null 在相等性檢查 == 中「不會進(jìn)行任何的類型轉(zhuǎn)換」,它們有自己獨(dú)立的比較規(guī)則,所以除了它們之間互等外,不會等于任何其他的值。

          undefined > 0;  // false  1
          undefined > 1;  // false  2
          undefined == 0; // false  3

          第 1、2 行都返回 false 是因為 undefined 在比較中被轉(zhuǎn)換為了 NaN,而 NaN 是一個特殊的數(shù)值型值,它與任何值進(jìn)行比較都會返回 false。 第 3 行返回 false 是因為這是一個相等性檢查,而 undefined 只與 null 相等,不會與其他值相等。

          九、alert / prompt / confirm

          1. alert

          顯示一個警告對話框,上面顯示有指定的文本內(nèi)容以及一個“確定”按鈕。 「注意:彈出模態(tài)框,并暫停腳本,直到用戶點(diǎn)擊“確定”按鈕。」

          // 語法
          window.alert(message);
          alert(message);
          // 示例
          alert('hello leo!');

          message是要顯示在對話框中的文本字符串,如果傳入其他類型的值,會轉(zhuǎn)換成字符串。

          2. prompt

          顯示一個對話框,對話框中包含一條文字信息,用來提示用戶輸入文字。 「注意:彈出模態(tài)框,并暫停腳本,直到用戶點(diǎn)擊“確定”按鈕。」 當(dāng)點(diǎn)擊確定返回文本,點(diǎn)擊取消或按下 Esc 鍵返回 null。 語法如下:

          let result = window.prompt(text, value);
          • result 用來存儲用戶輸入文字的字符串,或者是 null。
          • text 用來提示用戶輸入文字的字符串,如果沒有任何提示內(nèi)容,該參數(shù)可以省略不寫。
          • value 文本輸入框中的默認(rèn)值,該參數(shù)也可以省略不寫。不過在 Internet Explorer 7 和 8 中,省略該參數(shù)會導(dǎo)致輸入框中顯示默認(rèn)值"undefined"。

          3. confirm

          Window.confirm() 方法顯示一個具有一個可選消息和兩個按鈕(確定和取消)的模態(tài)對話框。 「注意:彈出模態(tài)框,并暫停腳本,直到用戶點(diǎn)擊“確定”按鈕。」 語法如下:

          let result = window.confirm(message);
          • message 是要在對話框中顯示的可選字符串。
          • result 是一個布爾值,表示是選擇確定還是取消 (true表示OK)。

          十、條件運(yùn)算符:if 和 '?'

          1. if 語句

          當(dāng) if 語句當(dāng)條件表達(dá)式,會將表達(dá)式轉(zhuǎn)換為布爾值,當(dāng)為 truthy 時執(zhí)行里面代碼。 轉(zhuǎn)換規(guī)則如:

          • 數(shù)字 0、空字符串 ""、null、undefined 和 NaN 都會被轉(zhuǎn)換成 false。因為他們被稱為 “falsy” 值。
          • 其他值被轉(zhuǎn)換為 true,所以它們被稱為 “truthy”。

          2. 三元運(yùn)算符

          「條件(三元)運(yùn)算符」是 JavaScript 僅有的使用三個操作數(shù)的運(yùn)算符。一個條件后面會跟一個問號(?),如果條件為 truthy ,則問號后面的表達(dá)式A將會執(zhí)行;表達(dá)式A后面跟著一個冒號(:),如果條件為 falsy ,則冒號后面的表達(dá)式B將會執(zhí)行。本運(yùn)算符經(jīng)常作為 [if](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/if...else) 語句的簡捷形式來使用。 語法:

          condition ? exprIfTrue : exprIfFalse
          • condition 計算結(jié)果用作條件的表達(dá)式。
          • exprIfTrue 如果表達(dá)式 condition 的計算結(jié)果是 truthy(它和 true 相等或者可以轉(zhuǎn)換成 true ),那么表達(dá)式 exprIfTrue 將會被求值。
          • exprIfFalse 如果表達(dá)式 condition 的計算結(jié)果是 falsy(它可以轉(zhuǎn)換成 false ),那么表達(dá)式 exprIfFalse 將會被執(zhí)行。 示例:
          let getUser = function(name){
           return name === 'leo' ? 'hello leo!' : 'unknow user';
          }
          // 可以簡寫如下:
          let getUser = name => name === 'leo' ? 'hello leo!' : 'unknow user';
          getUser('leo'); // "hello leo!"
          getUser('pingan'); // "unknow user"

          十一、邏輯運(yùn)算符

          詳細(xì)可以閱讀《MDN 邏輯運(yùn)算符》 。

          1. 運(yùn)算符介紹

          邏輯運(yùn)算符如下表所示 (其中_expr_可能是任何一種類型, 不一定是布爾值):

          運(yùn)算符 語法 說明 邏輯與,AND(&&) _expr1_ && _expr2_ 若 expr**1** 可轉(zhuǎn)換為 true,則返回 expr**2**;否則,返回 expr**1**。 邏輯或,OR(||) _expr1_ || _expr2_ 若 expr**1** 可轉(zhuǎn)換為 true,則返回 expr**1**;否則,返回 expr**2**。 邏輯非,NOT(!) !_expr_ 若 expr 可轉(zhuǎn)換為 true,則返回 false;否則,返回 true。

          如果一個值可以被轉(zhuǎn)換為 true,那么這個值就是所謂的 truthy,如果可以被轉(zhuǎn)換為 false,那么這個值就是所謂的 falsy。 會被轉(zhuǎn)換為 false 的表達(dá)式有:

          • null;
          • NaN;
          • 0;
          • 空字符串("" or '' or ````);
          • undefined。 盡管 && 和 || 運(yùn)算符能夠使用非布爾值的操作數(shù), 但它們依然可以被看作是布爾操作符,因為它們的返回值總是能夠被轉(zhuǎn)換為布爾值。如果要顯式地將它們的返回值(或者表達(dá)式)轉(zhuǎn)換為布爾值,請使用雙重非運(yùn)算符(即!!)或者Boolean構(gòu)造函數(shù)。 JavaScript 里有三個邏輯運(yùn)算符:||(或),&&(與),!(非)。

          2. 運(yùn)算符示例

          • 邏輯與(&&) 所有條件都為 true 才返回 true,否則為 false。
          a1 = true  && true      // t && t 返回 true
          a2 = true  && false     // t && f 返回 false
          a3 = false && true      // f && t 返回 false
          a4 = false && (3 == 4)  // f && f 返回 false
          a5 = "Cat" && "Dog"     // t && t 返回 "Dog"
          a6 = false && "Cat"     // f && t 返回 false
          a7 = "Cat" && false     // t && f 返回 false
          a8 = ''    && false     // f && f 返回 ""
          a9 = false && ''        // f && f 返回 false
          • 邏輯或( || ) 所有條件有一個為 true 則返回 true,否則為 false。
          o1 = true  || true      // t || t 返回 true
          o2 = false || true      // f || t 返回 true
          o3 = true  || false     // t || f 返回 true
          o4 = false || (3 == 4)  // f || f 返回 false
          o5 = "Cat" || "Dog"     // t || t 返回 "Cat"
          o6 = false || "Cat"     // f || t 返回 "Cat"
          o7 = "Cat" || false     // t || f 返回 "Cat"
          o8 = ''    || false     // f || f 返回 false
          o9 = false || ''        // f || f 返回 ""
          • 邏輯非( ! )
          n1 = !true              // !t 返回 false
          n2 = !false             // !f 返回 true
          n3 = !''                // !f 返回 true
          n4 = !'Cat'             // !t 返回 false
          • 雙重非運(yùn)( !! )
          n1 = !!true                   // !!truthy 返回 true
          n2 = !!{}                     // !!truthy 返回 true: 任何 對象都是 truthy 的…
          n3 = !!(new Boolean(false))   // …甚至 .valueOf() 返回 false 的布爾值對象也是!
          n4 = !!false                  // !!falsy 返回 false
          n5 = !!""                     // !!falsy 返回 false
          n6 = !!Boolean(false)         // !!falsy 返回 false

          3. 布爾值轉(zhuǎn)換規(guī)則

          • 將 && 轉(zhuǎn)換為 ||
          condi1 && confi2
          // 轉(zhuǎn)換為
          !(!condi1 || !condi2)
          • 將 || 轉(zhuǎn)換為 &&
          condi1 || condi2
          // 轉(zhuǎn)換為
          !(!condi1 && !condi2)

          4. 短路取值

          由于邏輯表達(dá)式的運(yùn)算順序是從左到右,也可以用以下規(guī)則進(jìn)行"短路"計算:

          • (some falsy expression) && (_expr)_ 短路計算的結(jié)果為假。
          • (some truthy expression) || _(expr)_ 短路計算的結(jié)果為真。 短路意味著上述表達(dá)式中的expr部分「不會被執(zhí)行」,因此expr的任何副作用都不會生效(舉個例子,如果expr是一次函數(shù)調(diào)用,這次調(diào)用就不會發(fā)生)。造成這種現(xiàn)象的原因是,整個表達(dá)式的值在第一個操作數(shù)被計算后已經(jīng)確定了。看一個例子:
          function A(){ console.log('called A'); return false; }
          function B(){ console.log('called B'); return true; }
          console.log( A() && B() );
          // logs "called A" due to the function call,
          // then logs false (which is the resulting value of the operator)
          console.log( B() || A() );
          // logs "called B" due to the function call,
          // then logs true (which is the resulting value of the operator)

          5. 注意

          與運(yùn)算 && 的優(yōu)先級比或運(yùn)算 || 要高。 所以代碼 a && b || c && d 完全跟 && 表達(dá)式加了括號一樣:(a && b) || (c && d)。

          十二、循環(huán):while 和 for

          1. while 循環(huán)

          詳細(xì)可以閱讀《MDN while》 。 「while 語句」可以在某個條件表達(dá)式為真的前提下,循環(huán)執(zhí)行指定的一段代碼,直到那個表達(dá)式不為真時結(jié)束循環(huán)。 如:

          var n = 0;
          var x = 0;
          while (n < 3) {
            n++;
            x += n;
          }

          當(dāng)循環(huán)體為單行時,可以不寫大括號:

          let i = 3;
          while(i) console.log(i --);

          2. do...while 循環(huán)

          詳細(xì)可以閱讀《MDN do...while》 。 do...while 語句創(chuàng)建一個執(zhí)行指定語句的循環(huán),直到condition值為 false。在執(zhí)行statement 后檢測condition,所以指定的statement至少執(zhí)行一次。 如:

          var result = '';
          var i = 0;
          do {
             i += 1;
             result += i + ' ';
          } while (i < 5);

          3. for 循環(huán)

          詳細(xì)可以閱讀《MDN for》 。 for 語句用于創(chuàng)建一個循環(huán),它包含了三個可選的表達(dá)式,這三個表達(dá)式被包圍在圓括號之中,使用分號分隔,后跟一個用于在循環(huán)中執(zhí)行的語句(通常是一個塊語句)。 語法如:

          for (begin; condition; step) {
            // ……循環(huán)體……
          }

          示例:

          for (let i = 0; i < 3; i++) {
            console.log(i);
          }

          描述:

          begin i = 0 進(jìn)入循環(huán)時執(zhí)行一次。 condition i < 3 在每次循環(huán)迭代之前檢查,如果為 false,停止循環(huán)。 body(循環(huán)體) alert(i) 條件為真時,重復(fù)運(yùn)行。 step i++ 在每次循環(huán)體迭代后執(zhí)行。

          4. 可選的 for 表達(dá)式

          for 語句頭部圓括號中的所有三個表達(dá)式都是可選的。

          • 不指定表達(dá)式中初始化塊
          var i = 0;
          for (; i < 3; i++) {
              console.log(i);
          }
          • 不指定表達(dá)式中條件塊,這就必須要求在循環(huán)體中結(jié)束循環(huán),否則會出現(xiàn)死循環(huán)
          for (var i = 0;; i++) {
             console.log(i);
             if (i > 3) break;
          }
          • 不指定所有表達(dá)式,也需要在循環(huán)體中指定結(jié)束循環(huán)的條件
          var i = 0;
          for (;;) {
            if (i > 3) break;
            console.log(i);
            i++;
          }

          5. break 語句

          詳細(xì)可以閱讀《MDN break》 。 break 語句中止當(dāng)前循環(huán),switch語句或label 語句,并把程序控制流轉(zhuǎn)到緊接著被中止語句后面的語句。 在 while 語句中:

          function testBreak(x) {
            var i = 0;
            while (i < 6) {
              if (i == 3) {
                break;
              }
              i += 1;
            }
            return i * x;
          }

          另外,也可以為代碼塊做標(biāo)記,并在 break 中指定要跳過的代碼塊語句的 label:

          outer_block:{
            inner_block:{
              console.log ('1');
              break outer_block;      // breaks out of both inner_block and outer_block
              console.log (':-(');    // skipped
            }
            console.log ('2');        // skipped
          }

          需要注意的是:break 語句需要內(nèi)嵌在它所應(yīng)用的標(biāo)簽或代碼塊中,否則報錯:

          block_1:{
            console.log ('1');
            break block_2;            // SyntaxError: label not found
          }
          block_2:{
            console.log ('2');
          }

          6. continue 語句

          continue 聲明終止當(dāng)前循環(huán)或標(biāo)記循環(huán)的當(dāng)前迭代中的語句執(zhí)行,并在下一次迭代時繼續(xù)執(zhí)行循環(huán)。 與 break 語句的區(qū)別在于, continue 并不會終止循環(huán)的迭代,而是:

          • 在 while 循環(huán)中,控制流跳轉(zhuǎn)回條件判斷;
          • 在 for 循環(huán)中,控制流跳轉(zhuǎn)到更新語句。 注意:continue 也必須在對應(yīng)循環(huán)內(nèi)部,否則報錯。
          i = 0;
          n = 0;
          while (i < 5) {
             i++;
             if (i === 3) {
                continue;
             }
             n += i;
          }

          帶 label:

          var i = 0, 
              j = 8;
          checkiandj: while (i < 4) {
             console.log("i: " + i);
             i += 1;
             checkj: while (j > 4) {
                console.log("j: "+ j);
                j -= 1;
                if ((j % 2) == 0)
                   continue checkj;
                console.log(j + " is odd.");
             }
             console.log("i = " + i);
             console.log("j = " + j);
          }

          7. 注意

          「禁止 「break/continue」 在 ‘?’ 的右邊:」

          (i > 5) ? console.log(i) : continue; // continue 不允許在這個位置

          這樣會提示語法錯誤。 請注意非表達(dá)式的語法結(jié)構(gòu)不能與三元運(yùn)算符 ? 一起使用。特別是 break/continue 這樣的指令是不允許這樣使用的。

          8. 總結(jié)

          三種循環(huán):

          • while —— 每次迭代之前都要檢查條件。
          • do..while —— 每次迭代后都要檢查條件。
          • for (;;) —— 每次迭代之前都要檢查條件,可以使用其他設(shè)置。 通常使用 while(true) 來構(gòu)造“無限”循環(huán)。這樣的循環(huán)和其他循環(huán)一樣,都可以通過 break 指令來終止。 如果我們不想在當(dāng)前迭代中做任何事,并且想要轉(zhuǎn)移至下一次迭代,那么可以使用 continue 指令。 break/continue 支持循環(huán)前的標(biāo)簽。標(biāo)簽是 break/continue 跳出嵌套循環(huán)以轉(zhuǎn)到外部的唯一方法。

          十三、"switch" 語句

          switch 語句用來將表達(dá)式的值與 case 語句匹配,并執(zhí)行與情況對應(yīng)的語句。 switch 語句可以替代多個 if 判斷,為多個分支選擇的情況提供一個更具描述性的方式。

          1. 語法

          switch 語句至少包含一個 case 代碼塊和一個可選的 default 代碼塊:

          switch(expression) {
            case 'value1':
              // do something ...
              [break]
             
            default:
              // ...
              [break]
          }

          當(dāng) expression 表達(dá)式的值與 value1 匹配時,則執(zhí)行其中代碼塊。 如果沒有 case 子句匹配,則會選擇 default 子句執(zhí)行,若連 default 子句都沒有,則直接執(zhí)行到 switch 結(jié)束。

          2. 使用 case 分組

          所謂 case 分組,就是與多個 case 分支共享同一段代碼,如下面例子中 case 1 和 case 2:

          let a = 2;
          switch (a) {
            case 1: // (*) 下面這兩個 case 被分在一組
            case 2:
              console.log('case is 1 or 2!');
              break;
            case 3:
              console.log('case is 3!');
              break;
            default:
              console.log('The result is default.');
          }
          // 'case is 1 or 2!'

          3. 注意點(diǎn)

          1. 「expression 表達(dá)式的值與 case 值的比較是嚴(yán)格相等:」
          function f(n){
              let a ;
              switch(n){
                  case 1:
                      a = 'number';
                      break;
                  case '1':
                      a = 'string';
                      break;
                  default:
                      a = 'default';
                      break;
              }
              console.log(a)
          }
          f(1);   // number
          f('1'); // string
          1. **如果沒有 break,程序?qū)⒉唤?jīng)過任何檢查就會繼續(xù)執(zhí)行下一個 ****case** :
          let a = 2 + 2;
          switch (a) {
           case 3:
              console.log( 'Too small' );
            case 4:
              console.log( 'Exactly!' );
            case 5:
              console.log( 'Too big' );
            default:
              console.log( "I don't know such values" );
          }
          // Exactly!
          // Too big
          // I don't know such values
          1. **default** **放在 ****case** 「之上不影響匹配:」
          function f(n){
            switch (n) {
              case 2:
                console.log(2);
                break;
              default:
                console.log('default')
                break;
              case 1:  
                console.log('1');
                break;
            }
          }
          f(1); // 1
          f(2); // 2
          f(3); // default
          • 「switch 語句中存在 let / const重復(fù)聲明問題:」
          // 以下定義會報錯
          function f(n){
              switch(n){
                  case 1:
                      let msg = 'hello';
                      console.log(1);
                      break;
                  case 2: 
                      let msg = 'leo';
                      break;
                  default: 
                      console.log('default');
                      break;
              }
          }
          // Error: Uncaught SyntaxError: Identifier 'msg' has already been declared

          這是由于兩個 let 處于同一個塊級作用域,所以它們被認(rèn)為是同一變量名的重復(fù)聲明。 解決方式,只需要將 case 語句包裝在括號內(nèi)即可解決:

          function f(n){
              switch(n){
                  case 1:{ // added brackets
                      let msg = 'hello';
                      console.log(msg);
                      break;
                  }
                  case 2: {
                      let msg = 'leo';
                      console.log(msg);
                      break;
                  }
                  default: 
                      console.log('default');
                      break;
              }
          }

          十四、函數(shù)

          函數(shù)可以讓一段代碼被多次調(diào)用,避免重復(fù)代碼。 如之前學(xué)習(xí)到的一些內(nèi)置函數(shù): alert(msg) / prompt(msg, default) / confirm(quesyion) 等。

          1. 函數(shù)定義

          定義函數(shù)有兩種方式:「函數(shù)聲明」「函數(shù)表達(dá)式」

          1.1 函數(shù)聲明

          如定義一個簡單 getUser 函數(shù):

          function getUser(name){
           return 'hello ' + name;
          }
          getUser('leo"); // 函數(shù)調(diào)用

          通過函數(shù)聲明來定義函數(shù)時,需要由以下幾部分組成:

          • 函數(shù)名稱 - getUser ;
          • 函數(shù)參數(shù)列表 - name ;
          • 函數(shù)的 JS 執(zhí)行語句 - return 'hello ' + name 。

          1.2 函數(shù)表達(dá)式

          類似聲明變量,還是以 getUser 為例:

          let getUser = function(name){
           return 'hello ' + name;
          }

          另外,函數(shù)表達(dá)式也可以提供函數(shù)名,并用于函數(shù)內(nèi)部指代函數(shù)本身:

          let fun = function f(n){
              return n < 3 ? 1 : n * f(n - 1);
          }
          fun(3);  // 3
          fun(5);  // 60

          2. 函數(shù)調(diào)用

          當(dāng)定義一個函數(shù)后,它并不會自動執(zhí)行,而是需要使用函數(shù)名稱進(jìn)行調(diào)用,如上面例子:

          fun(3);  // 3

          「只要注意:」 使用 「函數(shù)表達(dá)式」 定義函數(shù)時,調(diào)用函數(shù)的方法必須寫在定義之后,否則報錯:

          console.log(fun());  // Uncaught ReferenceError: fun is not defined
          let fun = function(){return 1};

          而使用 「函數(shù)聲明」 則不會出現(xiàn)該問題:

          console.log(fun());  // 1
          function fun(){return 1};

          原因就是:函數(shù)提升僅適用于函數(shù)聲明,而不適用于函數(shù)表達(dá)式。

          3. 函數(shù)中的變量

          在函數(shù)中,可以使用局部變量和外部變量。

          3.1 局部變量

          函數(shù)中聲明的變量只能在該函數(shù)內(nèi)可見。

          let fun = function(){
           let name = 'leo';
          }
          fun();
          console.log(name); // Uncaught ReferenceError: name is not defined

          3.2 全局變量

          函數(shù)內(nèi)可以使用外部變量,并且可以修改外部變量的值。

          let name = 'leo';
          let fun = function(){
           let text = 'Hello, ' + name;
            console.log(text);
          }
          fun(); // Hello, leo

          當(dāng)函數(shù)內(nèi)也有與外部變量名稱相同的變量,會忽略外部變量:

          let name = 'leo';
          let fun = function(){
            let name = 'pingan8787';
           let text = 'Hello, ' + name;
            console.log(text);
          }
          fun(); // Hello, pingan8787

          4. 函數(shù)參數(shù)

          從ECMAScript 6開始,有兩個新的類型的參數(shù):默認(rèn)參數(shù),剩余參數(shù)。

          4.1 默認(rèn)參數(shù)

          若函數(shù)沒有傳入?yún)?shù),則參數(shù)默認(rèn)值為undefined,通常設(shè)置參數(shù)默認(rèn)值是這樣做的:

          // ES6 之前,沒有設(shè)置默認(rèn)值
          function f(a, b){
              b = b ? b : 1;
              return a * b;
          }
          f(2,3);  // 6
          f(2);    // 2
          // ES6,設(shè)置默認(rèn)值
          function f(a, b = 1){
              return a * b;
          }
          f(2,3);  // 6
          f(2);    // 2

          4.2 剩余參數(shù)

          可以將參數(shù)中不確定數(shù)量的參數(shù)表示成數(shù)組,如下:

          function f (a, ...b){
              console.log(a, b);
          }
          f(1,2,3,4); // a => 1 b => [2, 3, 4]

          既然講到參數(shù),那就不能少了 arguments 對象。

          4.3 arguments 對象

          函數(shù)的實(shí)際參數(shù)會被保存在一個「類似數(shù)組的arguments對象」中。在函數(shù)內(nèi),我們可以使用 arguments 對象獲取函數(shù)的所有參數(shù):

          let fun = function(){
              console.log(arguments);
              console.log(arguments.length);
          }
          fun('leo'); 
          // Arguments ["leo", callee: ?, Symbol(Symbol.iterator): ?] 
          // 1

          以一個實(shí)際示例介紹,實(shí)現(xiàn)將任意數(shù)量參數(shù)連接成一個字符串,并輸出的函數(shù):

          let argumentConcat = function(separator){
           let result = '', i;
            for(i = 1; i < arguments.length; i ++){
             result += arguments[i] + separator;
            }
            return result;
          }
          argumentConcat(',', 'leo', 'pingan'); //"leo,pingan,"

          5. 函數(shù)返回值

          在函數(shù)任意位置,指定 return 指令來停止函數(shù)的執(zhí)行,并返回函數(shù)指定的返回值。

          let sum = function(a, b){
           return a + b;
          };
          let res = sum(1, 2);
          console.log(res); // 3

          默認(rèn)空值的 return 或沒有 return 的函數(shù)返回值為 undefined 。

          十五、函數(shù)表達(dá)式

          函數(shù)表達(dá)式是一種函數(shù)定義方式,在前面章節(jié)中已經(jīng)介紹到了,這個部分將著重介紹 「函數(shù)表達(dá)式」「函數(shù)聲明」 的區(qū)別:

          1. 語法差異

          // 函數(shù)表達(dá)式
          let fun = function(){};
          // 函數(shù)聲明
          function fun(){}

          2. 創(chuàng)建時機(jī)差異

          函數(shù)表達(dá)式會在代碼執(zhí)行到達(dá)時被創(chuàng)建,并且僅從那一刻可用。 而函數(shù)聲明被定義之前,它就可以被調(diào)用。

          // 函數(shù)表達(dá)式
          fun();  // Uncaught ReferenceError: fun is not defined
          let fun = function(){console.log('leo')};
          // 函數(shù)聲明
          fun();  // "leo"
          function fun(){console.log('leo')};

          3. 使用建議

          建議優(yōu)先考慮函數(shù)聲明語法,它能夠為組織代碼提供更多靈活性,因為我們可以在聲明函數(shù)前調(diào)用該函數(shù)。

          十六、箭頭函數(shù)

          「本章節(jié)簡單介紹箭頭函數(shù)基礎(chǔ)知識,后面章節(jié)會完整介紹。」 「函數(shù)箭頭表達(dá)式」是ES6新增的函數(shù)表達(dá)式的語法,也叫「胖箭頭函數(shù)」,變化:更簡潔的函數(shù)和this。

          1. 代碼更簡潔

          // 有1個參數(shù)
          let f = v => v;
          // 等同于
          let f = function (v){return v};
          // 有多個參數(shù)
          let f = (v, i) => {return v + i};
          // 等同于
          let f = function (v, i){return v + i};
          // 沒參數(shù)
          let f = () => 1;
          // 等同于
          let f = function (){return 1};
          let arr = [1,2,3,4];
          arr.map(ele => ele + 1);  // [2, 3, 4, 5]

          2. 注意點(diǎn)

          1. 箭頭函數(shù)不存在this;
          2. 箭頭函數(shù)不能當(dāng)做「構(gòu)造函數(shù)」,即不能用new實(shí)例化;
          3. 箭頭函數(shù)不存在arguments對象,即不能使用,可以使用rest參數(shù)代替;
          4. 箭頭函數(shù)不能使用yield命令,即不能用作Generator函數(shù)。 一個簡單的例子:
          function Person(){
            this.age = 0;
            setInterval(() => {
              this.age++;
            }, 1000);
          }
          var p = new Person(); // 定時器一直在執(zhí)行 p的值一直變化

          總結(jié)

          本文作為《初中級前端 JavaScript 自測清單》第一部分,介紹的內(nèi)容以常用基礎(chǔ)知識為主,并在學(xué)習(xí)資料中,將知識點(diǎn)結(jié)合實(shí)際開發(fā)中遇到的場景進(jìn)行展開介紹。希望能幫助大家自測自己的 JavaScript 水平并查缺補(bǔ)漏,溫故知新。

          Author 王平安 E-mail pingan8787@qq.com 博 客 www.pingan8787.com 微 信 pingan8787 每日文章推薦 https://github.com/pingan8787/Leo_Reading/issues ES小冊 js.pingan8787.com 語雀知識庫 Cute-FrontEnd

          bg


          著大數(shù)據(jù)時代的發(fā)展,各個公司的數(shù)據(jù)保護(hù)意識越來越強(qiáng),大家都在想盡辦法保護(hù)自家產(chǎn)品的數(shù)據(jù)不輕易被爬蟲爬走。由于網(wǎng)頁是提供信息和服務(wù)的重要載體,所以對網(wǎng)頁上的信息進(jìn)行保護(hù)就成了至關(guān)重要的一個環(huán)節(jié)。

          網(wǎng)頁是運(yùn)行在瀏覽器端的,當(dāng)我們?yōu)g覽一個網(wǎng)頁時,其 HTML 代碼、 JavaScript 代碼都會被下載到瀏覽器中執(zhí)行。借助瀏覽器的開發(fā)者工具,我們可以看到網(wǎng)頁在加載過程中所有網(wǎng)絡(luò)請求的詳細(xì)信息,也能清楚地看到網(wǎng)站運(yùn)行的 HTML 代碼和 JavaScript 代碼,這些代碼中就包含了網(wǎng)站加載的全部邏輯,如加載哪些資源、請求接口是如何構(gòu)造的、頁面是如何渲染的等等。正因為代碼是完全透明的,所以如果我們能夠把其中的執(zhí)行邏輯研究出來,就可以模擬各個網(wǎng)絡(luò)請求進(jìn)行數(shù)據(jù)爬取了。

          然而,事情沒有想象得那么簡單。隨著前端技術(shù)的發(fā)展,前端代碼的打包技術(shù)、混淆技術(shù)、加密技術(shù)也層出不窮,借助于這些技術(shù),各個公司可以在前端對 JavaScript 代碼采取一定的保護(hù),比如變量名混淆、執(zhí)行邏輯混淆、反調(diào)試、核心邏輯加密等,這些保護(hù)手段使得我們沒法很輕易地找出 JavaScript 代碼中包含的的執(zhí)行邏輯。

          在前幾章的案例中,我們也試著爬取了各種形式的網(wǎng)站。其中有的網(wǎng)站的數(shù)據(jù)接口是沒有任何驗證或加密參數(shù)的,我們可以輕松模擬并爬取其中的數(shù)據(jù);但有的網(wǎng)站稍顯復(fù)雜,網(wǎng)站的接口中增加了一些加密參數(shù),同時對 JavaScript 代碼采取了上文所述的一些防護(hù)措施,當(dāng)時我們沒有直接嘗試去破解,而是用 Selenium 等類似工具來實(shí)現(xiàn)模擬瀏覽器執(zhí)行的方式來進(jìn)行“所見即所得“的爬取。其實(shí)對于后者,我們還有另外一種解決方案,那就是直接逆向 JavaScript 代碼,找出其中的加密邏輯,從而直接實(shí)現(xiàn)該加密邏輯來進(jìn)行爬取。如果加密邏輯實(shí)在過于復(fù)雜,我們也可以找出一些關(guān)鍵入口,從而實(shí)現(xiàn)對加密邏輯的單獨(dú)模擬執(zhí)行和數(shù)據(jù)爬取。這些方案難度可能很大,比如關(guān)鍵入口很難尋找,或者加密邏輯難以模擬,可是一旦成功找到突破口,我們便可以不用借助于 Selenium 等工具進(jìn)行整頁數(shù)據(jù)的渲染而實(shí)現(xiàn)數(shù)據(jù)爬取,這樣爬取效率會大幅提升。

          本章我們首先會對 JavaScript 防護(hù)技術(shù)進(jìn)行介紹,然后介紹一些常用的 JavaScript 逆向技巧,包括瀏覽器工具的使用、Hook 技術(shù)、AST 技術(shù)、特殊混淆技術(shù)的處理、WebAssembly 技術(shù)的處理。了解了這些技術(shù),我們可以更從容地應(yīng)對 JavaScript 防護(hù)技術(shù)。

          1. 引入

          我們在爬取網(wǎng)站的時候,會遇到一些情況需要分析一些接口或 URL 信息,在這個過程中,我們會遇到各種各樣類似加密的情形,比如說:

          • 某個網(wǎng)站的 URL 帶有一些看不太懂的長串加密參數(shù),要抓取就必須要懂得這些參數(shù)是怎么構(gòu)造的,否則我們連完整的 URL 都構(gòu)造不出來,更不用說爬取了。
          • 分析某個網(wǎng)站的 Ajax 接口的時候,可以看到接口的一些參數(shù)也是加密的,或者 Request Headers 里面也可能帶有一些加密參數(shù),如果不知道這些參數(shù)的具體構(gòu)造邏輯就沒法直接用程序來模擬這些 Ajax 請求。
          • 翻看網(wǎng)站的 JavaScript 源代碼,可以發(fā)現(xiàn)很多壓縮了或者看不太懂的字符,比如 JavaScript 文件名被編碼,JavaScript 的文件內(nèi)容都壓縮成幾行,JavaScript 變量也被修改成單個字符或者一些十六進(jìn)制的字符,導(dǎo)致我們不好輕易根據(jù) JavaScript 找出某些接口的加密邏輯。

          這些情況呢,基本上都是網(wǎng)站為了保護(hù)其本身的一些數(shù)據(jù)不被輕易抓取而采取的一些措施,我們可以把它歸類為兩大類:

          • URL/API 參數(shù)加密
          • JavaScript 壓縮、混淆和加密

          這一節(jié)我們就來了解下這兩類技術(shù)的基本原理和一些常見的示例。知己知彼,百戰(zhàn)不殆,了解了這些技術(shù)的實(shí)現(xiàn)原理之后,我們才能更好地去逆向其中的邏輯,從而實(shí)現(xiàn)數(shù)據(jù)爬取。

          2. 網(wǎng)站數(shù)據(jù)防護(hù)方案

          當(dāng)今大數(shù)據(jù)時代,數(shù)據(jù)已經(jīng)變得越來越重要,網(wǎng)頁和 App 現(xiàn)在是主流的數(shù)據(jù)載體,如果其數(shù)據(jù)的 API 沒有設(shè)置任何保護(hù)措施,在爬蟲工程師解決了一些基本的反爬如封 IP、驗證碼的問題之后,那么數(shù)據(jù)還是可以被輕松爬取到的。

          那么,有沒有可能在 URL/API 層面或 JavaScript 層面也加上一層防護(hù)呢?答案是可以。

          URL/API 參數(shù)加密

          網(wǎng)站運(yùn)營者首先想到防護(hù)措施可能是對某些數(shù)據(jù)接口的參數(shù)進(jìn)行加密,比如說對某些 URL 的一些參數(shù)加上校驗碼或者把一些 id 信息進(jìn)行編碼,使其變得難以閱讀或構(gòu)造;或者對某些 API 請求加上一些 token、sign 等簽名,這樣這些請求發(fā)送到服務(wù)器時,服務(wù)器會通過客戶端發(fā)來的一些請求信息以及雙方約定好的秘鑰等來對當(dāng)前的請求進(jìn)行校驗,如果校驗通過,才返回對應(yīng)數(shù)據(jù)結(jié)果。

          比如說客戶端和服務(wù)端約定一種接口校驗邏輯,客戶端在每次請求服務(wù)端接口的時候都會附帶一個 sign 參數(shù),這個 sign 參數(shù)可能是由當(dāng)前時間信息、請求的 URL、請求的數(shù)據(jù)、設(shè)備的 ID、雙方約定好的秘鑰經(jīng)過一些加密算法構(gòu)造而成的,客戶端會實(shí)現(xiàn)這個加密算法構(gòu)造 sign,然后每次請求服務(wù)器的時候附帶上這個參數(shù)。服務(wù)端會根據(jù)約定好的算法和請求的數(shù)據(jù)對 sign 進(jìn)行校驗,如果校驗通過,才返回對應(yīng)的數(shù)據(jù),否則拒絕響應(yīng)。

          當(dāng)然登錄狀態(tài)的校驗也可以看作是此類方案,比如一個 API 的調(diào)用必須要傳一個 token,這個 token 必須用戶登錄之后才能獲取,如果請求的時候不帶該 token,API 就不會返回任何數(shù)據(jù)。

          倘若沒有這種措施,那么基本上 URL 或者 API 接口是完全公開可以訪問的,這意味著任何人都可以直接調(diào)用來獲取數(shù)據(jù),幾乎是零防護(hù)的狀態(tài),這樣是非常危險的,而且數(shù)據(jù)也可以被輕易地被爬蟲爬取。因此對 URL/API 參數(shù)一些加密和校驗是非常有必要的。

          JavaScript 壓縮、混淆和加密

          接口加密技術(shù)看起來的確是一個不錯的解決方案,但單純依靠它并不能很好地解決問題。為什么呢?

          對于網(wǎng)頁來說,其邏輯是依賴于 JavaScript 來實(shí)現(xiàn)的,JavaScript 有如下特點(diǎn):

          • JavaScript 代碼運(yùn)行于客戶端,也就是它必須要在用戶瀏覽器端加載并運(yùn)行。
          • JavaScript 代碼是公開透明的,也就是說瀏覽器可以直接獲取到正在運(yùn)行的 JavaScript 的源碼。

          由于這兩個原因,至使 JavaScript 代碼是不安全的,任何人都可以讀、分析、復(fù)制、盜用,甚至篡改。

          所以說,對于上述情形,客戶端 JavaScript 對于某些加密的實(shí)現(xiàn)是很容易被找到或模擬的,了解了加密邏輯后,模擬參數(shù)的構(gòu)造和請求也就是輕而易舉了,所以如果 JavaScript 沒有做任何層面的保護(hù)的話,接口加密技術(shù)基本上對數(shù)據(jù)起不到什么防護(hù)作用。

          如果你不想讓自己的數(shù)據(jù)被輕易獲取,不想他人了解 JavaScript 邏輯的實(shí)現(xiàn),或者想降低被不懷好意的人甚至是黑客攻擊。那么就需要用到 JavaScript 壓縮、混淆和加密技術(shù)了。

          這里壓縮、混淆和加密技術(shù)簡述如下:

          • 代碼壓縮:即去除 JavaScript 代碼中的不必要的空格、換行等內(nèi)容,使源碼都壓縮為幾行內(nèi)容,降低代碼可讀性,當(dāng)然同時也能提高網(wǎng)站的加載速度。
          • 代碼混淆:使用變量替換、字符串陣列化、控制流平坦化、多態(tài)變異、僵尸函數(shù)、調(diào)試保護(hù)等手段,使代碼變地難以閱讀和分析,達(dá)到最終保護(hù)的目的。但這不影響代碼原有功能。是理想、實(shí)用的 JavaScript 保護(hù)方案。
          • 代碼加密:可以通過某種手段將 JavaScript 代碼進(jìn)行加密,轉(zhuǎn)成人無法閱讀或者解析的代碼,如借用 WebAssembly 技術(shù),可以直接將 JavaScript 代碼用 C/C++ 實(shí)現(xiàn),JavaScript 調(diào)用其編譯后形成的文件來執(zhí)行相應(yīng)的功能。

          下面我們對上面的技術(shù)分別予以介紹。

          3. URL/API 參數(shù)加密

          現(xiàn)在絕大多數(shù)網(wǎng)站的數(shù)據(jù)一般都是通過服務(wù)器提供的 API 來獲取的,網(wǎng)站或 App 可以請求某個數(shù)據(jù) API 獲取到對應(yīng)的數(shù)據(jù),然后再把獲取的數(shù)據(jù)展示出來。但有些數(shù)據(jù)是比較寶貴或私密的,這些數(shù)據(jù)肯定是需要一定層面上的保護(hù)。所以不同 API 的實(shí)現(xiàn)也就對應(yīng)著不同的安全防護(hù)級別,我們這里來總結(jié)下。

          為了提升接口的安全性,客戶端會和服務(wù)端約定一種接口校驗方式,一般來說會使用到各種加密和編碼算法,如 Base64、Hex 編碼,MD5、AES、DES、RSA 等對稱或非對稱加密。

          舉個例子,比如說客戶端和服務(wù)器雙方約定一個 sign 用作接口的簽名校驗,其生成邏輯是客戶端將 URL Path 進(jìn)行 MD5 加密然后拼接上 URL 的某個參數(shù)再進(jìn)行 Base64 編碼,最后得到一個字符串 sign,這個 sign 會通過 Request URL 的某個參數(shù)或 Request Headers 發(fā)送給服務(wù)器。服務(wù)器接收到請求后,對 URL Path 同樣進(jìn)行 MD5 加密,然后拼接上 URL 的某個參數(shù),也進(jìn)行 Base64 編碼也得到了一個 sign,然后比對生成的 sign 和客戶端發(fā)來的 sign 是否是一致的,如果是一致的,那就返回正確的結(jié)果,否則拒絕響應(yīng)。這就是一個比較簡單的接口參數(shù)加密的實(shí)現(xiàn)。如果有人想要調(diào)用這個接口的話,必須要定義好 sign 的生成邏輯,否則是無法正常調(diào)用接口的。

          當(dāng)然上面的這個實(shí)現(xiàn)思路比較簡單,這里還可以增加一些時間戳信息增加時效性判斷,或增加一些非對稱加密進(jìn)一步提高加密的復(fù)雜程度。但不管怎樣,只要客戶端和服務(wù)器約定好了加密和校驗邏輯,任何形式加密算法都是可以的。

          這里要實(shí)現(xiàn)接口參數(shù)加密就需要用到一些加密算法,客戶端和服務(wù)器肯定也都有對應(yīng)的 SDK 實(shí)現(xiàn)這些加密算法,如 JavaScript 的 crypto-js,Python 的 hashlib、Crypto 等等。

          但還是如上文所說,如果是網(wǎng)頁的話,客戶端實(shí)現(xiàn)加密邏輯如果是用 JavaScript 來實(shí)現(xiàn),其源代碼對用戶是完全可見的,如果沒有對 JavaScript 做任何保護(hù)的話,是很容易弄清楚客戶端加密的流程的。

          因此,我們需要對 JavaScript 利用壓縮、混淆等方式來對客戶端的邏輯進(jìn)行一定程度上的保護(hù)。

          4. JavaScript 壓縮

          這個非常簡單,JavaScript 壓縮即去除 JavaScript 代碼中的不必要的空格、換行等內(nèi)容或者把一些可能公用的代碼進(jìn)行處理實(shí)現(xiàn)共享,最后輸出的結(jié)果都壓縮為幾行內(nèi)容,代碼可讀性變得很差,同時也能提高網(wǎng)站加載速度。

          如果僅僅是去除空格換行這樣的壓縮方式,其實(shí)幾乎是沒有任何防護(hù)作用的,因為這種壓縮方式僅僅是降低了代碼的直接可讀性。如果我們有一些格式化工具可以輕松將 JavaScript 代碼變得易讀,比如利用 IDE、在線工具或 Chrome 瀏覽器都能還原格式化的代碼。

          比如這里舉一個最簡單的 JavaScript 壓縮示例,原來的 JavaScript 代碼是這樣的:

          function echo(stringA, stringB) {
            const name = "Germey";
            alert("hello " + name);
          }

          壓縮之后就變成這樣子:

          function echo(d, c) {
            const e = "Germey";
            alert("hello " + e);
          }

          可以看到這里參數(shù)的名稱都被簡化了,代碼中的空格也被去掉了,整個代碼也被壓縮成了一行,代碼的整體可讀性降低了。

          目前主流的前端開發(fā)技術(shù)大多都會利用 Webpack、Rollup 等工具進(jìn)行打包,Webpack、Rollup 會對源代碼進(jìn)行編譯和壓縮,輸出幾個打包好的 JavaScript 文件,其中我們可以看到輸出的 JavaScript 文件名帶有一些不規(guī)則字符串,同時文件內(nèi)容可能只有幾行內(nèi)容,變量名都是一些簡單字母表示。這其中就包含 JavaScript 壓縮技術(shù),比如一些公共的庫輸出成 bundle 文件,一些調(diào)用邏輯壓縮和轉(zhuǎn)義成冗長的幾行代碼,這些都屬于 JavaScript 壓縮。另外其中也包含了一些很基礎(chǔ)的 JavaScript 混淆技術(shù),比如把變量名、方法名替換成一些簡單字符,降低代碼可讀性。

          但整體來說,JavaScript 壓縮技術(shù)只能在很小的程度上起到防護(hù)作用,要想真正提高防護(hù)效果還得依靠 JavaScript 混淆和加密技術(shù)。

          5. JavaScript 混淆

          JavaScript 混淆是完全是在 JavaScript 上面進(jìn)行的處理,它的目的就是使得 JavaScript 變得難以閱讀和分析,大大降低代碼可讀性,是一種很實(shí)用的 JavaScript 保護(hù)方案。

          JavaScript 混淆技術(shù)主要有以下幾種:

          • 變量混淆:將帶有含義的變量名、方法名、常量名隨機(jī)變?yōu)闊o意義的類亂碼字符串,降低代碼可讀性,如轉(zhuǎn)成單個字符或十六進(jìn)制字符串。
          • 字符串混淆:將字符串陣列化集中放置、并可進(jìn)行 MD5 或 Base64 加密存儲,使代碼中不出現(xiàn)明文字符串,這樣可以避免使用全局搜索字符串的方式定位到入口點(diǎn)。
          • 屬性加密:針對 JavaScript 對象的屬性進(jìn)行加密轉(zhuǎn)化,隱藏代碼之間的調(diào)用關(guān)系。
          • 控制流平坦化:打亂函數(shù)原有代碼執(zhí)行流程及函數(shù)調(diào)用關(guān)系,使代碼邏變得混亂無序。
          • 無用代碼注入:隨機(jī)在代碼中插入不會被執(zhí)行到的無用代碼,進(jìn)一步使代碼看起來更加混亂。
          • 調(diào)試保護(hù):基于調(diào)試器特性,對當(dāng)前運(yùn)行環(huán)境進(jìn)行檢驗,加入一些強(qiáng)制調(diào)試 debugger 語句,使其在調(diào)試模式下難以順利執(zhí)行 JavaScript 代碼。
          • 多態(tài)變異:使 JavaScript 代碼每次被調(diào)用時,將代碼自身即立刻自動發(fā)生變異,變化為與之前完全不同的代碼,即功能完全不變,只是代碼形式變異,以此杜絕代碼被動態(tài)分析調(diào)試。
          • 鎖定域名:使 JavaScript 代碼只能在指定域名下執(zhí)行。
          • 反格式化:如果對 JavaScript 代碼進(jìn)行格式化,則無法執(zhí)行,導(dǎo)致瀏覽器假死。
          • 特殊編碼:將 JavaScript 完全編碼為人不可讀的代碼,如表情符號、特殊表示內(nèi)容等等。

          總之,以上方案都是 JavaScript 混淆的實(shí)現(xiàn)方式,可以在不同程度上保護(hù) JavaScript 代碼。

          在前端開發(fā)中,現(xiàn)在 JavaScript 混淆主流的實(shí)現(xiàn)是 javascript-obfuscator (https://github.com/javascript-obfuscator/javascript-obfuscator) 和 terser (https://github.com/terser/terser) 這兩個庫,其都能提供一些代碼混淆功能,也都有對應(yīng)的 Webpack 和 Rollup 打包工具的插件,利用它們我們可以非常方便地實(shí)現(xiàn)頁面的混淆,最終可以輸出壓縮和混淆后的 JavaScript 代碼,使得 JavaScript 代碼可讀性大大降低。

          下面我們以 javascript-obfuscator 為例來介紹一些代碼混淆的實(shí)現(xiàn),了解了實(shí)現(xiàn),那么自然我們就對混淆的機(jī)理有了更加深刻的認(rèn)識。

          javascript-obfuscator 的官網(wǎng)地址為:https://obfuscator.io/,其官方介紹內(nèi)容如下:

          A free and efficient obfuscator for JavaScript (including ES2017). Make your code harder to copy and prevent people from stealing your work.

          它是支持 ES8 的免費(fèi)、高效的 JavaScript 混淆庫,它可以使得你的 JavaScript 代碼經(jīng)過混淆后難以被復(fù)制、盜用,混淆后的代碼具有和原來的代碼一模一樣的功能。

          怎么使用呢?首先,我們需要安裝好 Node.js 12.x 版本及以上,確保可以正常使用 npm 命令,具體的安裝方式可以參考:https://setup.scrape.center/nodejs。

          接著新建一個文件夾,比如 js-obfuscate,然后進(jìn)入該文件夾,初始化工作空間:

          npm init

          這里會提示我們輸入一些信息,創(chuàng)建一個 package.json 文件,這就完成了項目初始化了。

          接下來我們來安裝 javascript-obfuscator 這個庫:

          npm i -D javascript-obfuscator

          稍等片刻,即可看到本地 js-obfuscate 文件夾下生成了一個 node_modules 文件夾,里面就包含了 javascript-obfuscator 這個庫,這就說明安裝成功了,文件夾結(jié)構(gòu)如圖所示:

          接下來我們就可以編寫代碼來實(shí)現(xiàn)一個混淆樣例了,如新建一個 main.js 文件,內(nèi)容如下:

          const code = `
          let x = '1' + 1
          console.log('x', x)
          `;
          
          const options = {
            compact: false,
            controlFlowFlattening: true,
          };
          
          const obfuscator = require("javascript-obfuscator");
          function obfuscate(code, options) {
            return obfuscator.obfuscate(code, options).getObfuscatedCode();
          }
          console.log(obfuscate(code, options));

          在這里我們定義了兩個變量,一個是 code,即需要被混淆的代碼,另一個是混淆選項,是一個 Object。接下來我們引入了 javascript-obfuscator 這庫,然后定義了一個方法,傳入 code 和 options,來獲取混淆后的代碼,最后控制臺輸出混淆后的代碼。

          代碼邏輯比較簡單,我們來執(zhí)行一下代碼:

          node main.js

          輸出結(jié)果如下:

          var _0x53bf = ["log"];
          (function (_0x1d84fe, _0x3aeda0) {
            var _0x10a5a = function (_0x2f0a52) {
              while (--_0x2f0a52) {
                _0x1d84fe["push"](_0x1d84fe["shift"]());
              }
            };
            _0x10a5a(++_0x3aeda0);
          })(_0x53bf, 0x172);
          var _0x480a = function (_0x4341e5, _0x5923b4) {
            _0x4341e5 = _0x4341e5 - 0x0;
            var _0xb3622e = _0x53bf[_0x4341e5];
            return _0xb3622e;
          };
          let x = "1" + 0x1;
          console[_0x480a("0x0")]("x", x);

          看到了吧,那么簡單的兩行代碼,被我們混淆成了這個樣子,其實(shí)這里我們就是設(shè)定了一個「控制流平坦化」的選項。整體看來,代碼的可讀性大大降低,也大大加大了 JavaScript 調(diào)試的難度。

          好,那么我們來跟著 javascript-obfuscator 走一遍,就能具體知道 JavaScript 混淆到底有多少方法了。

          注意:由于這些例子中,調(diào)用 javascript-obfuscator 進(jìn)行混淆的實(shí)現(xiàn)是一樣的,所以下文的示例只說明 code 和 options 變量的修改,完整代碼請自行補(bǔ)全。

          代碼壓縮

          這里 javascript-obfuscator 也提供了代碼壓縮的功能,使用其參數(shù) compact 即可完成 JavaScript 代碼的壓縮,輸出為一行內(nèi)容。默認(rèn)是 true,如果定義為 false,則混淆后的代碼會分行顯示。

          示例如下:

          const code = `
          let x = '1' + 1
          console.log('x', x)
          `;
          const options = {
            compact: false,
          };

          這里我們先把代碼壓縮 compact 選項設(shè)置為 false,運(yùn)行結(jié)果如下:

          let x = "1" + 0x1;
          console["log"]("x", x);

          如果不設(shè)置 compact 或把 compact 設(shè)置為 true,結(jié)果如下:

          var _0x151c = ["log"];
          (function (_0x1ce384, _0x20a7c7) {
            var _0x25fc92 = function (_0x188aec) {
              while (--_0x188aec) {
                _0x1ce384["push"](_0x1ce384["shift"]());
              }
            };
            _0x25fc92(++_0x20a7c7);
          })(_0x151c, 0x1b7);
          var _0x553e = function (_0x259219, _0x241445) {
            _0x259219 = _0x259219 - 0x0;
            var _0x56d72d = _0x151c[_0x259219];
            return _0x56d72d;
          };
          let x = "1" + 0x1;
          console[_0x553e("0x0")]("x", x);

          可以看到單行顯示的時候,對變量名進(jìn)行了進(jìn)一步的混淆,這里變量的命名都變成了 16 進(jìn)制形式的字符串,這是因為啟用了一些默認(rèn)壓縮和混淆配置導(dǎo)致的。總之我們可以看到代碼的可讀性相比之前大大降低了。

          變量名混淆

          變量名混淆可以通過在 javascript-obfuscator 中配置 identifierNamesGenerator 參數(shù)實(shí)現(xiàn),我們通過這個參數(shù)可以控制變量名混淆的方式,如 hexadecimal 則會替換為 16 進(jìn)制形式的字符串,在這里我們可以設(shè)定如下值:

          • hexadecimal:將變量名替換為 16 進(jìn)制形式的字符串,如 0xabc123。
          • mangled:將變量名替換為普通的簡寫字符,如 a、b、c 等。

          該參數(shù)的值默認(rèn)為 hexadecimal。

          我們將該參數(shù)修改為 mangled 來試一下:

          const code = `
          let hello = '1' + 1
          console.log('hello', hello)
          `;
          const options = {
            compact: true,
            identifierNamesGenerator: "mangled",
          };

          運(yùn)行結(jié)果如下:

          var a = ["hello"];
          (function (c, d) {
            var e = function (f) {
              while (--f) {
                c["push"](c["shift"]());
              }
            };
            e(++d);
          })(a, 0x9b);
          var b = function (c, d) {
            c = c - 0x0;
            var e = a[c];
            return e;
          };
          let hello = "1" + 0x1;
          console["log"](b("0x0"), hello);

          可以看到這里的變量命名都變成了 a、b 等形式。

          如果我們將 identifierNamesGenerator 修改為 hexadecimal 或者不設(shè)置,運(yùn)行結(jié)果如下:

          var _0x4e98 = ["log", "hello"];
          (function (_0x4464de, _0x39de6c) {
            var _0xdffdda = function (_0x6a95d5) {
              while (--_0x6a95d5) {
                _0x4464de["push"](_0x4464de["shift"]());
              }
            };
            _0xdffdda(++_0x39de6c);
          })(_0x4e98, 0xc8);
          var _0x53cb = function (_0x393bda, _0x8504e7) {
            _0x393bda = _0x393bda - 0x0;
            var _0x46ab80 = _0x4e98[_0x393bda];
            return _0x46ab80;
          };
          let hello = "1" + 0x1;
          console[_0x53cb("0x0")](_0x53cb("0x1"), hello);

          可以看到選用了 mangled,其代碼體積會更小,但 hexadecimal 其可讀性會更低。

          另外我們還可以通過設(shè)置 identifiersPrefix 參數(shù)來控制混淆后的變量前綴,示例如下:

          const code = `
          let hello = '1' + 1
          console.log('hello', hello)
          `;
          const options = {
            identifiersPrefix: "germey",
          };

          運(yùn)行結(jié)果如下:

          var germey_0x3dea = ["log", "hello"];
          (function (_0x348ff3, _0x5330e8) {
            var _0x1568b1 = function (_0x4740d8) {
              while (--_0x4740d8) {
                _0x348ff3["push"](_0x348ff3["shift"]());
              }
            };
            _0x1568b1(++_0x5330e8);
          })(germey_0x3dea, 0x94);
          var germey_0x30e4 = function (_0x2e8f7c, _0x1066a8) {
            _0x2e8f7c = _0x2e8f7c - 0x0;
            var _0x5166ba = germey_0x3dea[_0x2e8f7c];
            return _0x5166ba;
          };
          let hello = "1" + 0x1;
          console[germey_0x30e4("0x0")](germey_0x30e4("0x1"), hello);

          可以看到混淆后的變量前綴加上了我們自定義的字符串 germey。

          另外 renameGlobals 這個參數(shù)還可以指定是否混淆全局變量和函數(shù)名稱,默認(rèn)為 false。示例如下:

          const code = `
          var $ = function(id) {
              return document.getElementById(id);
          };
          `;
          const options = {
            renameGlobals: true,
          };

          運(yùn)行結(jié)果如下:

          var _0x4864b0 = function (_0x5763be) {
            return document["getElementById"](_0x5763be);
          };

          可以看到這里我們聲明了一個全局變量 $,在 renameGlobals 設(shè)置為 true 之后,$ 這個變量也被替換了。如果后文用到了這個 $ 對象,可能就會有找不到定義的錯誤,因此這個參數(shù)可能導(dǎo)致代碼執(zhí)行不通。

          如果我們不設(shè)置 renameGlobals 或者設(shè)置為 false,結(jié)果如下:

          var _0x239a = ["getElementById"];
          (function (_0x3f45a3, _0x583dfa) {
            var _0x2cade2 = function (_0x28479a) {
              while (--_0x28479a) {
                _0x3f45a3["push"](_0x3f45a3["shift"]());
              }
            };
            _0x2cade2(++_0x583dfa);
          })(_0x239a, 0xe1);
          var _0x3758 = function (_0x18659d, _0x50c21d) {
            _0x18659d = _0x18659d - 0x0;
            var _0x531b8d = _0x239a[_0x18659d];
            return _0x531b8d;
          };
          var $ = function (_0x3d8723) {
            return document[_0x3758("0x0")](_0x3d8723);
          };

          可以看到,最后還是有 $ 的聲明,其全局名稱沒有被改變。

          字符串混淆

          字符串混淆,即將一個字符串聲明放到一個數(shù)組里面,使之無法被直接搜索到。我們可以通過控制 stringArray 參數(shù)來控制,默認(rèn)為 true。

          我們還可以通過 rotateStringArray 參數(shù)來控制數(shù)組化后結(jié)果的的元素順序,默認(rèn)為 true。還可以通過 stringArrayEncoding 參數(shù)來控制數(shù)組的編碼形式,默認(rèn)不開啟編碼,如果設(shè)置為 true 或 base64,則會使用 Base64 編碼,如果設(shè)置為 rc4,則使用 RC4 編碼。另外可以通過 stringArrayThreshold 來控制啟用編碼的概率,范圍 0 到 1,默認(rèn) 0.8。

          示例如下:

          const code = `
          var a = 'hello world'   
          `;
          const options = {
            stringArray: true,
            rotateStringArray: true,
            stringArrayEncoding: true, // 'base64' 或 'rc4' 或 false
            stringArrayThreshold: 1,
          };

          運(yùn)行結(jié)果如下:

          var _0x4215 = ["aGVsbG8gd29ybGQ="];
          (function (_0x42bf17, _0x4c348f) {
            var _0x328832 = function (_0x355be1) {
              while (--_0x355be1) {
                _0x42bf17["push"](_0x42bf17["shift"]());
              }
            };
            _0x328832(++_0x4c348f);
          })(_0x4215, 0x1da);
          var _0x5191 = function (_0x3cf2ba, _0x1917d8) {
            _0x3cf2ba = _0x3cf2ba - 0x0;
            var _0x1f93f0 = _0x4215[_0x3cf2ba];
            if (_0x5191["LqbVDH"] === undefined) {
              (function () {
                var _0x5096b2;
                try {
                  var _0x282db1 = Function(
                    "return\x20(function()\x20" +
                      "{}.constructor(\x22return\x20this\x22)(\x20)" +
                      ");"
                  );
                  _0x5096b2 = _0x282db1();
                } catch (_0x2acb9c) {
                  _0x5096b2 = window;
                }
                var _0x388c14 =
                  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
                _0x5096b2["atob"] ||
                  (_0x5096b2["atob"] = function (_0x4cc27c) {
                    var _0x2af4ae = String(_0x4cc27c)["replace"](/=+$/, "");
                    for (
                      var _0x21400b = 0x0,
                        _0x3f4e2e,
                        _0x5b193b,
                        _0x233381 = 0x0,
                        _0x3dccf7 = "";
                      (_0x5b193b = _0x2af4ae["charAt"](_0x233381++));
                      ~_0x5b193b &&
                      ((_0x3f4e2e =
                        _0x21400b % 0x4 ? _0x3f4e2e * 0x40 + _0x5b193b : _0x5b193b),
                      _0x21400b++ % 0x4)
                        ? (_0x3dccf7 += String["fromCharCode"](
                            0xff & (_0x3f4e2e >> ((-0x2 * _0x21400b) & 0x6))
                          ))
                        : 0x0
                    ) {
                      _0x5b193b = _0x388c14["indexOf"](_0x5b193b);
                    }
                    return _0x3dccf7;
                  });
              })();
              _0x5191["DuIurT"] = function (_0x51888e) {
                var _0x29801f = atob(_0x51888e);
                var _0x561e62 = [];
                for (
                  var _0x5dd788 = 0x0, _0x1a8b73 = _0x29801f["length"];
                  _0x5dd788 < _0x1a8b73;
                  _0x5dd788++
                ) {
                  _0x561e62 +=
                    "%" +
                    ("00" + _0x29801f["charCodeAt"](_0x5dd788)["toString"](0x10))[
                      "slice"
                    ](-0x2);
                }
                return decodeURIComponent(_0x561e62);
              };
              _0x5191["mgoBRd"] = {};
              _0x5191["LqbVDH"] = !![];
            }
            var _0x1741f0 = _0x5191["mgoBRd"][_0x3cf2ba];
            if (_0x1741f0 === undefined) {
              _0x1f93f0 = _0x5191["DuIurT"](_0x1f93f0);
              _0x5191["mgoBRd"][_0x3cf2ba] = _0x1f93f0;
            } else {
              _0x1f93f0 = _0x1741f0;
            }
            return _0x1f93f0;
          };
          var a = _0x5191("0x0");

          可以看到這里就把字符串進(jìn)行了 Base64 編碼,我們再也無法通過查找的方式找到字符串的位置了。

          如果將 stringArray 設(shè)置為 false 的話,輸出就是這樣:

          var a = "hello\x20world";

          字符串就仍然是明文顯示的,沒有被編碼。

          另外我們還可以使用 unicodeEscapeSequence 這個參數(shù)對字符串進(jìn)行 Unicode 轉(zhuǎn)碼,使之更加難以辨認(rèn),示例如下:

          const code = `
          var a = 'hello world'
          `;
          const options = {
            compact: false,
            unicodeEscapeSequence: true,
          };

          運(yùn)行結(jié)果如下:

          var _0x5c0d = ["\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64"];
          (function (_0x54cc9c, _0x57a3b2) {
            var _0xf833cf = function (_0x3cd8c6) {
              while (--_0x3cd8c6) {
                _0x54cc9c["push"](_0x54cc9c["shift"]());
              }
            };
            _0xf833cf(++_0x57a3b2);
          })(_0x5c0d, 0x17d);
          var _0x28e8 = function (_0x3fd645, _0x2cf5e7) {
            _0x3fd645 = _0x3fd645 - 0x0;
            var _0x298a20 = _0x5c0d[_0x3fd645];
            return _0x298a20;
          };
          var a = _0x28e8("0x0");

          可以看到,這里字符串被數(shù)字化和 Unicode 化,非常難以辨認(rèn)。

          在很多 JavaScript 逆向的過程中,一些關(guān)鍵的字符串可能會作為切入點(diǎn)來查找加密入口。用了這種混淆之后,如果有人想通過全局搜索的方式搜索 hello 這樣的字符串找加密入口,也沒法搜到了。

          代碼自我保護(hù)

          我們可以通過設(shè)置 selfDefending 參數(shù)來開啟代碼自我保護(hù)功能。開啟之后,混淆后的 JavaScript 會以強(qiáng)制一行形式顯示,如果我們將混淆后的代碼進(jìn)行格式化或者重命名,該段代碼將無法執(zhí)行。

          示例如下:

          const code = `
          console.log('hello world')
          `;
          const options = {
            selfDefending: true,
          };

          運(yùn)行結(jié)果如下:

          var _0x26da = ["log", "hello\x20world"];
          (function (_0x190327, _0x57c2c0) {
            var _0x577762 = function (_0xc9dabb) {
              while (--_0xc9dabb) {
                _0x190327["push"](_0x190327["shift"]());
              }
            };
            var _0x35976e = function () {
              var _0x16b3fe = {
                data: { key: "cookie", value: "timeout" },
                setCookie: function (_0x2d52d5, _0x16feda, _0x57cadf, _0x56056f) {
                  _0x56056f = _0x56056f || {};
                  var _0x5b6dc3 = _0x16feda + "=" + _0x57cadf;
                  var _0x333ced = 0x0;
                  for (
                    var _0x333ced = 0x0, _0x19ae36 = _0x2d52d5["length"];
                    _0x333ced < _0x19ae36;
                    _0x333ced++
                  ) {
                    var _0x409587 = _0x2d52d5[_0x333ced];
                    _0x5b6dc3 += ";\x20" + _0x409587;
                    var _0x4aa006 = _0x2d52d5[_0x409587];
                    _0x2d52d5["push"](_0x4aa006);
                    _0x19ae36 = _0x2d52d5["length"];
                    if (_0x4aa006 !== !![]) {
                      _0x5b6dc3 += "=" + _0x4aa006;
                    }
                  }
                  _0x56056f["cookie"] = _0x5b6dc3;
                },
                removeCookie: function () {
                  return "dev";
                },
                getCookie: function (_0x30c497, _0x51923d) {
                  _0x30c497 =
                    _0x30c497 ||
                    function (_0x4b7e18) {
                      return _0x4b7e18;
                    };
                  var _0x557e06 = _0x30c497(
                    new RegExp(
                      "(?:^|;\x20)" +
                        _0x51923d["replace"](/([.$?*|{}()[]\/+^])/g, "$1") +
                        "=([^;]*)"
                    )
                  );
                  var _0x817646 = function (_0xf3fae7, _0x5d8208) {
                    _0xf3fae7(++_0x5d8208);
                  };
                  _0x817646(_0x577762, _0x57c2c0);
                  return _0x557e06 ? decodeURIComponent(_0x557e06[0x1]) : undefined;
                },
              };
              var _0x4673cd = function () {
                var _0x4c6c5c = new RegExp(
                  "\x5cw+\x20*\x5c(\x5c)\x20*{\x5cw+\x20*[\x27|\x22].+[\x27|\x22];?\x20*}"
                );
                return _0x4c6c5c["test"](_0x16b3fe["removeCookie"]["toString"]());
              };
              _0x16b3fe["updateCookie"] = _0x4673cd;
              var _0x5baa80 = "";
              var _0x1faf19 = _0x16b3fe["updateCookie"]();
              if (!_0x1faf19) {
                _0x16b3fe["setCookie"](["*"], "counter", 0x1);
              } else if (_0x1faf19) {
                _0x5baa80 = _0x16b3fe["getCookie"](null, "counter");
              } else {
                _0x16b3fe["removeCookie"]();
              }
            };
            _0x35976e();
          })(_0x26da, 0x140);
          var _0x4391 = function (_0x1b42d8, _0x57edc8) {
            _0x1b42d8 = _0x1b42d8 - 0x0;
            var _0x2fbeca = _0x26da[_0x1b42d8];
            return _0x2fbeca;
          };
          var _0x197926 = (function () {
            var _0x10598f = !![];
            return function (_0xffa3b3, _0x7a40f9) {
              var _0x48e571 = _0x10598f
                ? function () {
                    if (_0x7a40f9) {
                      var _0x2194b5 = _0x7a40f9["apply"](_0xffa3b3, arguments);
                      _0x7a40f9 = null;
                      return _0x2194b5;
                    }
                  }
                : function () {};
              _0x10598f = ![];
              return _0x48e571;
            };
          })();
          var _0x2c6fd7 = _0x197926(this, function () {
            var _0x4828bb = function () {
                return "\x64\x65\x76";
              },
              _0x35c3bc = function () {
                return "\x77\x69\x6e\x64\x6f\x77";
              };
            var _0x456070 = function () {
              var _0x4576a4 = new RegExp(
                "\x5c\x77\x2b\x20\x2a\x5c\x28\x5c\x29\x20\x2a\x7b\x5c\x77\x2b\x20\x2a\x5b\x27\x7c\x22\x5d\x2e\x2b\x5b\x27\x7c\x22\x5d\x3b\x3f\x20\x2a\x7d"
              );
              return !_0x4576a4["\x74\x65\x73\x74"](
                _0x4828bb["\x74\x6f\x53\x74\x72\x69\x6e\x67"]()
              );
            };
            var _0x3fde69 = function () {
              var _0xabb6f4 = new RegExp(
                "\x28\x5c\x5c\x5b\x78\x7c\x75\x5d\x28\x5c\x77\x29\x7b\x32\x2c\x34\x7d\x29\x2b"
              );
              return _0xabb6f4["\x74\x65\x73\x74"](
                _0x35c3bc["\x74\x6f\x53\x74\x72\x69\x6e\x67"]()
              );
            };
            var _0x2d9a50 = function (_0x58fdb4) {
              var _0x2a6361 = ~-0x1 >> (0x1 + (0xff % 0x0));
              if (_0x58fdb4["\x69\x6e\x64\x65\x78\x4f\x66"]("\x69" === _0x2a6361)) {
                _0xc388c5(_0x58fdb4);
              }
            };
            var _0xc388c5 = function (_0x2073d6) {
              var _0x6bb49f = ~-0x4 >> (0x1 + (0xff % 0x0));
              if (
                _0x2073d6["\x69\x6e\x64\x65\x78\x4f\x66"]((!![] + "")[0x3]) !== _0x6bb49f
              ) {
                _0x2d9a50(_0x2073d6);
              }
            };
            if (!_0x456070()) {
              if (!_0x3fde69()) {
                _0x2d9a50("\x69\x6e\x64\u0435\x78\x4f\x66");
              } else {
                _0x2d9a50("\x69\x6e\x64\x65\x78\x4f\x66");
              }
            } else {
              _0x2d9a50("\x69\x6e\x64\u0435\x78\x4f\x66");
            }
          });
          _0x2c6fd7();
          console[_0x4391("0x0")](_0x4391("0x1"));

          如果我們將上述代碼放到控制臺,它的執(zhí)行結(jié)果和之前是一模一樣的,沒有任何問題。

          如果我們將其進(jìn)行格式化,然后貼到到瀏覽器控制臺里面,瀏覽器會直接卡死無法運(yùn)行。這樣如果有人對代碼進(jìn)行了格式化,就無法正常對代碼進(jìn)行運(yùn)行和調(diào)試,從而起到了保護(hù)作用。

          控制流平坦化

          控制流平坦化其實(shí)就是將代碼的執(zhí)行邏輯混淆,使其變得復(fù)雜難讀。其基本思想是將一些邏輯處理塊都統(tǒng)一加上一個前驅(qū)邏輯塊,每個邏輯塊都由前驅(qū)邏輯塊進(jìn)行條件判斷和分發(fā),構(gòu)成一個個閉環(huán)邏輯,導(dǎo)致整個執(zhí)行邏輯十分復(fù)雜難讀。

          比如說這里有一段示例代碼:

          console.log(c);
          console.log(a);
          console.log(b);

          代碼邏輯一目了然,依次在控制臺輸出了 c、a、b 三個變量的值,但如果把這段代碼進(jìn)行控制流平坦化處理后,代碼就會變成這樣:

          const s = "3|1|2".split("|");
          let x = 0;
          while (true) {
            switch (s[x++]) {
              case "1":
                console.log(a);
                continue;
              case "2":
                console.log(b);
                continue;
              case "3":
                console.log(c);
                continue;
            }
            break;
          }

          可以看到,混淆后的代碼首先聲明了一個變量 s,它的結(jié)果是一個列表,其實(shí)是 ["3", "1", "2"],然后下面通過 switch 語句對 s 中的元素進(jìn)行了判斷,每個 case 都加上了各自的代碼邏輯。通過這樣的處理,一些連續(xù)的執(zhí)行邏輯就被打破了,代碼被修改為一個 switch 語句,原本我們可以一眼看出的邏輯是控制臺先輸出 c,然后才是 a、b,但是現(xiàn)在我們必須要結(jié)合 switch 的判斷條件和對應(yīng) case 的內(nèi)容進(jìn)行判斷,我們很難再一眼每條語句的執(zhí)行順序了,這就大大降低了代碼的可讀性。

          在 javascript-obfuscator 中我們通過 controlFlowFlattening 變量可以控制是否開啟控制流平坦化,示例如下:

          const options = {
            compact: false,
            controlFlowFlattening: true,
          };

          使用控制流平坦化可以使得執(zhí)行邏輯更加復(fù)雜難讀,目前非常多的前端混淆都會加上這個選項。但啟用控制流平坦化之后,代碼的執(zhí)行時間會變長,最長達(dá) 1.5 倍之多。

          另外我們還能使用 controlFlowFlatteningThreshold 這個參數(shù)來控制比例,取值范圍是 0 到 1,默認(rèn) 0.75,如果設(shè)置為 0,那相當(dāng)于 controlFlowFlattening 設(shè)置為 false,即不開啟控制流扁平化 。

          無用代碼注入

          無用代碼即不會被執(zhí)行的代碼或?qū)ι舷挛臎]有任何影響的代碼,注入之后可以對現(xiàn)有的 JavaScript 代碼的閱讀形成干擾。我們可以使用 deadCodeInjection 參數(shù)開啟這個選項,默認(rèn)為 false。

          比如這里有一段代碼:

          const a = function () {
            console.log("hello world");
          };
          
          const b = function () {
            console.log("nice to meet you");
          };
          
          a();
          b();

          這里就聲明了方法 a 和 b,然后依次進(jìn)行調(diào)用,分別輸出兩句話。

          但經(jīng)過無用代碼注入處理之后,代碼就會變成類似這樣的結(jié)果:

          const _0x16c18d = function () {
            if (!![[]]) {
              console.log("hello world");
            } else {
              console.log("this");
              console.log("is");
              console.log("dead");
              console.log("code");
            }
          };
          const _0x1f7292 = function () {
            if ("xmv2nOdfy2N".charAt(4) !== String.fromCharCode(110)) {
              console.log("this");
              console.log("is");
              console.log("dead");
              console.log("code");
            } else {
              console.log("nice to meet you");
            }
          };
          
          _0x16c18d();
          _0x1f7292();

          可以看到,每個方法內(nèi)部都增加了額外的 if else 語句,其中 if 的判斷條件還是一個表達(dá)式,其結(jié)果是 true 還是 false 我們還不太一眼能看出來,比如說 _0x1f7292 這個方法,它的 if 判斷條件是:

          "xmv2nOdfy2N".charAt(4) !== String.fromCharCode(110)

          在不等號前面其實(shí)是從字符串中取出指定位置的字符,不等號后面則調(diào)用了 fromCharCode 方法來根據(jù) ascii 碼轉(zhuǎn)換得到一個字符,然后比較兩個字符的結(jié)果是否是不一樣的。前者經(jīng)過我們推算可以知道結(jié)果是 n,但對于后者,多數(shù)情況下我們還得去查一下 ascii 碼表才能知道其結(jié)果也是 n,最后兩個結(jié)果是相同的,所以整個表達(dá)式的結(jié)果是 false,所以 if 后面跟的邏輯實(shí)際上就是不會被執(zhí)行到的無用代碼,但這些代碼對我們閱讀代碼起到了一定的干擾作用。

          因此,這種混淆方式通過混入一些特殊的判斷條件并加入一些不會被執(zhí)行的代碼,可以對代碼起到一定的混淆干擾作用。

          在 javascript-obfuscator 中,我們可以通過 deadCodeInjection 參數(shù)控制無用代碼的注入,配置如下:

          const options = {
            compact: false,
            deadCodeInjection: true,
          };

          另外我們還可以通過設(shè)置 deadCodeInjectionThreshold 參數(shù)來控制無用代碼注入的比例,取值 0 到 1,默認(rèn)是 0.4。

          對象鍵名替換

          如果是一個對象,可以使用 transformObjectKeys 來對對象的鍵值進(jìn)行替換,示例如下:

          const code = `
          (function(){
              var object = {
                  foo: 'test1',
                  bar: {
                      baz: 'test2'
                  }
              };
          })(); 
          `;
          const options = {
            compact: false,
            transformObjectKeys: true,
          };

          輸出結(jié)果如下:

          var _0x7a5d = ["bar", "test2", "test1"];
          (function (_0x59fec5, _0x2e4fac) {
            var _0x231e7a = function (_0x46f33e) {
              while (--_0x46f33e) {
                _0x59fec5["push"](_0x59fec5["shift"]());
              }
            };
            _0x231e7a(++_0x2e4fac);
          })(_0x7a5d, 0x167);
          var _0x3bc4 = function (_0x309ad3, _0x22d5ac) {
            _0x309ad3 = _0x309ad3 - 0x0;
            var _0x3a034e = _0x7a5d[_0x309ad3];
            return _0x3a034e;
          };
          (function () {
            var _0x9f1fd1 = {};
            _0x9f1fd1["foo"] = _0x3bc4("0x0");
            _0x9f1fd1[_0x3bc4("0x1")] = {};
            _0x9f1fd1[_0x3bc4("0x1")]["baz"] = _0x3bc4("0x2");
          })();

          可以看到,Object 的變量名被替換為了特殊的變量,使得可讀性變差,這樣我們就不好直接通過變量名進(jìn)行搜尋了,這也可以起到一定的防護(hù)作用。

          禁用控制臺輸出

          可以使用 disableConsoleOutput 來禁用掉 console.log 輸出功能,加大調(diào)試難度,示例如下:

          const code = `
          console.log('hello world')
          `;
          const options = {
            disableConsoleOutput: true,
          };

          運(yùn)行結(jié)果如下:

          var _0x3a39 = [
            "debug",
            "info",
            "error",
            "exception",
            "trace",
            "hello\x20world",
            "apply",
            "{}.constructor(\x22return\x20this\x22)(\x20)",
            "console",
            "log",
            "warn",
          ];
          (function (_0x2a157a, _0x5d9d3b) {
            var _0x488e2c = function (_0x5bcb73) {
              while (--_0x5bcb73) {
                _0x2a157a["push"](_0x2a157a["shift"]());
              }
            };
            _0x488e2c(++_0x5d9d3b);
          })(_0x3a39, 0x10e);
          var _0x5bff = function (_0x43bdfc, _0x52e4c6) {
            _0x43bdfc = _0x43bdfc - 0x0;
            var _0xb67384 = _0x3a39[_0x43bdfc];
            return _0xb67384;
          };
          var _0x349b01 = (function () {
            var _0x1f484b = !![];
            return function (_0x5efe0d, _0x33db62) {
              var _0x20bcd2 = _0x1f484b
                ? function () {
                    if (_0x33db62) {
                      var _0x77054c = _0x33db62[_0x5bff("0x0")](_0x5efe0d, arguments);
                      _0x33db62 = null;
                      return _0x77054c;
                    }
                  }
                : function () {};
              _0x1f484b = ![];
              return _0x20bcd2;
            };
          })();
          var _0x19f538 = _0x349b01(this, function () {
            var _0x7ab6e4 = function () {};
            var _0x157bff;
            try {
              var _0x5e672c = Function(
                "return\x20(function()\x20" + _0x5bff("0x1") + ");"
              );
              _0x157bff = _0x5e672c();
            } catch (_0x11028d) {
              _0x157bff = window;
            }
            if (!_0x157bff[_0x5bff("0x2")]) {
              _0x157bff[_0x5bff("0x2")] = (function (_0x7ab6e4) {
                var _0x5a8d9e = {};
                _0x5a8d9e[_0x5bff("0x3")] = _0x7ab6e4;
                _0x5a8d9e[_0x5bff("0x4")] = _0x7ab6e4;
                _0x5a8d9e[_0x5bff("0x5")] = _0x7ab6e4;
                _0x5a8d9e[_0x5bff("0x6")] = _0x7ab6e4;
                _0x5a8d9e[_0x5bff("0x7")] = _0x7ab6e4;
                _0x5a8d9e[_0x5bff("0x8")] = _0x7ab6e4;
                _0x5a8d9e[_0x5bff("0x9")] = _0x7ab6e4;
                return _0x5a8d9e;
              })(_0x7ab6e4);
            } else {
              _0x157bff[_0x5bff("0x2")][_0x5bff("0x3")] = _0x7ab6e4;
              _0x157bff[_0x5bff("0x2")][_0x5bff("0x4")] = _0x7ab6e4;
              _0x157bff[_0x5bff("0x2")]["debug"] = _0x7ab6e4;
              _0x157bff[_0x5bff("0x2")][_0x5bff("0x6")] = _0x7ab6e4;
              _0x157bff[_0x5bff("0x2")][_0x5bff("0x7")] = _0x7ab6e4;
              _0x157bff[_0x5bff("0x2")][_0x5bff("0x8")] = _0x7ab6e4;
              _0x157bff[_0x5bff("0x2")][_0x5bff("0x9")] = _0x7ab6e4;
            }
          });
          _0x19f538();
          console[_0x5bff("0x3")](_0x5bff("0xa"));

          此時,我們?nèi)绻麍?zhí)行這個代碼,發(fā)現(xiàn)是沒有任何輸出的,這里實(shí)際上就是將 console 的一些功能禁用了。

          調(diào)試保護(hù)

          我們知道,在 JavaScript 代碼中如果加入 debugger 這個關(guān)鍵字,那么在執(zhí)行到該位置的時候控制它就會進(jìn)入斷點(diǎn)調(diào)試模式。如果在代碼多個位置都加入 debugger 這個關(guān)鍵字,或者定義某個邏輯來反復(fù)執(zhí)行 debugger,那就會不斷進(jìn)入斷點(diǎn)調(diào)試模式,原本的代碼無法就無法順暢地執(zhí)行了。這個過程可以稱為調(diào)試保護(hù),即通過反復(fù)執(zhí)行 debugger 來使得原來的代碼無法順暢執(zhí)行。

          其效果類似于執(zhí)行了如下代碼:

          setInterval(() => {
            debugger;
          }, 3000);

          如果我們把這段代碼粘貼到控制臺,它就會反復(fù)地執(zhí)行 debugger 語句進(jìn)入斷點(diǎn)調(diào)試模式,從而干擾正常的調(diào)試流程。

          在 javascript-obfuscator 中可以使用 debugProtection 來啟用調(diào)試保護(hù)機(jī)制,還可以使用 debugProtectionInterval 來啟用無限 Debug ,使得代碼在調(diào)試過程中會不斷進(jìn)入斷點(diǎn)模式,無法順暢執(zhí)行,配置如下:

          const options = {
            debugProtection: true,
            debugProtectionInterval: true,
          };

          混淆后的代碼會不斷跳到 debugger 代碼的位置,使得整個代碼無法順暢執(zhí)行,對 JavaScript 代碼的調(diào)試形成一定的干擾。

          域名鎖定

          我們還可以通過控制 domainLock 來控制 JavaScript 代碼只能在特定域名下運(yùn)行,這樣就可以降低代碼被模擬或盜用的風(fēng)險。

          示例如下:

          const code = `
          console.log('hello world')
          `;
          const options = {
            domainLock: ["cuiqingcai.com"],
          };

          這里我們使用了 domainLock 指定了一個域名叫做 cuiqingcai.com,也就是設(shè)置了一個域名白名單,混淆后的代碼結(jié)果如下:

          var _0x3203 = [
            "apply",
            "return\x20(function()\x20",
            "{}.constructor(\x22return\x20this\x22)(\x20)",
            "item",
            "attribute",
            "value",
            "replace",
            "length",
            "charCodeAt",
            "log",
            "hello\x20world",
          ];
          (function (_0x2ed22c, _0x3ad370) {
            var _0x49dc54 = function (_0x53a786) {
              while (--_0x53a786) {
                _0x2ed22c["push"](_0x2ed22c["shift"]());
              }
            };
            _0x49dc54(++_0x3ad370);
          })(_0x3203, 0x155);
          var _0x5b38 = function (_0xd7780b, _0x19c0f2) {
            _0xd7780b = _0xd7780b - 0x0;
            var _0x2d2f44 = _0x3203[_0xd7780b];
            return _0x2d2f44;
          };
          var _0x485919 = (function () {
            var _0x5cf798 = !![];
            return function (_0xd1fa29, _0x2ed646) {
              var _0x56abf = _0x5cf798
                ? function () {
                    if (_0x2ed646) {
                      var _0x33af63 = _0x2ed646[_0x5b38("0x0")](_0xd1fa29, arguments);
                      _0x2ed646 = null;
                      return _0x33af63;
                    }
                  }
                : function () {};
              _0x5cf798 = ![];
              return _0x56abf;
            };
          })();
          var _0x67dcc8 = _0x485919(this, function () {
            var _0x276a31;
            try {
              var _0x5c8be2 = Function(_0x5b38("0x1") + _0x5b38("0x2") + ");");
              _0x276a31 = _0x5c8be2();
            } catch (_0x5f1c00) {
              _0x276a31 = window;
            }
            var _0x254a0d = function () {
              return {
                key: _0x5b38("0x3"),
                value: _0x5b38("0x4"),
                getAttribute: (function () {
                  for (var _0x5cc3c7 = 0x0; _0x5cc3c7 < 0x3e8; _0x5cc3c7--) {
                    var _0x35b30b = _0x5cc3c7 > 0x0;
                    switch (_0x35b30b) {
                      case !![]:
                        return (
                          this[_0x5b38("0x3")] +
                          "_" +
                          this[_0x5b38("0x5")] +
                          "_" +
                          _0x5cc3c7
                        );
                      default:
                        this[_0x5b38("0x3")] + "_" + this[_0x5b38("0x5")];
                    }
                  }
                })(),
              };
            };
            var _0x3b375a = new RegExp("[QLCIKYkCFzdWpzRAXMhxJOYpTpYWJHPll]", "g");
            var _0x5a94d2 = "cuQLiqiCInKYkgCFzdWcpzRAaXMi.hcoxmJOYpTpYWJHPll"
              [_0x5b38("0x6")](_0x3b375a, "")
              ["split"](";");
            var _0x5c0da2;
            var _0x19ad5d;
            var _0x5992ca;
            var _0x40bd39;
            for (var _0x5cad1 in _0x276a31) {
              if (
                _0x5cad1[_0x5b38("0x7")] == 0x8 &&
                _0x5cad1[_0x5b38("0x8")](0x7) == 0x74 &&
                _0x5cad1[_0x5b38("0x8")](0x5) == 0x65 &&
                _0x5cad1[_0x5b38("0x8")](0x3) == 0x75 &&
                _0x5cad1[_0x5b38("0x8")](0x0) == 0x64
              ) {
                _0x5c0da2 = _0x5cad1;
                break;
              }
            }
            for (var _0x29551 in _0x276a31[_0x5c0da2]) {
              if (
                _0x29551[_0x5b38("0x7")] == 0x6 &&
                _0x29551[_0x5b38("0x8")](0x5) == 0x6e &&
                _0x29551[_0x5b38("0x8")](0x0) == 0x64
              ) {
                _0x19ad5d = _0x29551;
                break;
              }
            }
            if (!("~" > _0x19ad5d)) {
              for (var _0x2b71bd in _0x276a31[_0x5c0da2]) {
                if (
                  _0x2b71bd[_0x5b38("0x7")] == 0x8 &&
                  _0x2b71bd[_0x5b38("0x8")](0x7) == 0x6e &&
                  _0x2b71bd[_0x5b38("0x8")](0x0) == 0x6c
                ) {
                  _0x5992ca = _0x2b71bd;
                  break;
                }
              }
              for (var _0x397f55 in _0x276a31[_0x5c0da2][_0x5992ca]) {
                if (
                  _0x397f55["length"] == 0x8 &&
                  _0x397f55[_0x5b38("0x8")](0x7) == 0x65 &&
                  _0x397f55[_0x5b38("0x8")](0x0) == 0x68
                ) {
                  _0x40bd39 = _0x397f55;
                  break;
                }
              }
            }
            if (!_0x5c0da2 || !_0x276a31[_0x5c0da2]) {
              return;
            }
            var _0x5f19be = _0x276a31[_0x5c0da2][_0x19ad5d];
            var _0x674f76 =
              !!_0x276a31[_0x5c0da2][_0x5992ca] &&
              _0x276a31[_0x5c0da2][_0x5992ca][_0x40bd39];
            var _0x5e1b34 = _0x5f19be || _0x674f76;
            if (!_0x5e1b34) {
              return;
            }
            var _0x593394 = ![];
            for (var _0x479239 = 0x0; _0x479239 < _0x5a94d2["length"]; _0x479239++) {
              var _0x19ad5d = _0x5a94d2[_0x479239];
              var _0x112c24 = _0x5e1b34["length"] - _0x19ad5d["length"];
              var _0x51731c = _0x5e1b34["indexOf"](_0x19ad5d, _0x112c24);
              var _0x173191 = _0x51731c !== -0x1 && _0x51731c === _0x112c24;
              if (_0x173191) {
                if (
                  _0x5e1b34["length"] == _0x19ad5d[_0x5b38("0x7")] ||
                  _0x19ad5d["indexOf"](".") === 0x0
                ) {
                  _0x593394 = !![];
                }
              }
            }
            if (!_0x593394) {
              data;
            } else {
              return;
            }
            _0x254a0d();
          });
          _0x67dcc8();
          console[_0x5b38("0x9")](_0x5b38("0xa"));

          這段代碼就只能在指定域名 cuiqingcai.com 下運(yùn)行,不能在其他網(wǎng)站運(yùn)行。這樣的話,如果一些相關(guān) JavaScript 代碼被單獨(dú)剝離出來,想在其他網(wǎng)站運(yùn)行或者使用程序模擬運(yùn)行的話,運(yùn)行結(jié)果只有是失敗,這樣就可以有效降低被代碼被模擬或盜用的風(fēng)險。

          特殊編碼

          另外還有一些特殊的工具包,如使用 aaencode、jjencode、jsfuck 等工具對代碼進(jìn)行混淆和編碼。

          示例如下:

          var a = 1

          jsfuck 的結(jié)果:

          [][(![]+[])[!+[]+!![]+!![]]+([]+{})[+!![]]+(!![]+[])[+!![]]+(!![]+[])[+[]]][([]+{})[!+[]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([][[]]+[])[+!![]]+(![]+[])[!+[]+!![]+!![]]+(!![]+[])[+[]]+(!![]+[])[+!![]]+([][[]]+[])[+[]]+([]+{})[!+[]+!![]+!![]+!![]+!![]]+(!![]+[])[+[]]+([]+{})[+!![]]+(!![]+[])[+!![]]]([][(![]+[])[!+[]+!![]+!![]]+([]+{})[+!![]]+(!![]+[])[+!![]]+(!![]+[])[+[]]][([]+{})[!+[]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([][[]]+[])[+!![]]+
          ...
          ([]+{})[+!![]]+(!![]+[])[+!![]]]((!![]+[])[+!![]]+([][[]]+[])[!+[]+!![]+!![]]+(!![]+[])[+[]]+([][[]]+[])[+[]]+(!![]+[])[+!![]]+([][[]]+[])[+!![]]+([]+{})[!+[]+!![]+!![]+!![]+!![]+!![]+!![]]+(![]+[])[!+[]+!![]]+([]+{})[+!![]]+([]+{})[!+[]+!![]+!![]+!![]+!![]]+(+{}+[])[+!![]]+(!![]+[])[+[]]+([][[]]+[])[!+[]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([][[]]+[])[+!![]])(!+[]+!![]+!![]+!![]+!![]))[!+[]+!![]+!![]]+([][[]]+[])[!+[]+!![]+!![]])(!+[]+!![]+!![]+!![]+!![])(([]+{})[+[]])[+[]]+(!+[]+!![]+!![]+[])+([][[]]+[])[!+[]+!![]])+([]+{})[!+[]+!![]+!![]+!![]+!![]+!![]+!![]]+(+!![]+[]))(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![])

          aaencode 的結(jié)果:

          ?ω??= /`m′)? ~┻━┻   / ['_']; o=(???)  =_=3; c=(?Θ?) =(???)-(???); (?Д?) =(?Θ?)= (o^_^o)/ (o^_^o);(?Д?)={?Θ?: '_' ,?ω?? : ((?ω??==3) +'_') [?Θ?] ,???? :(?ω??+ '_')[o^_^o -(?Θ?)] ,?Д??:((???==3) +'_')[???] }; (?Д?) [?Θ?] =((?ω??==3) +'_') [c^_^o];(?Д?) ['c'] = ((?Д?)+'_') [ (???)+(???)-(?Θ?) ];(?Д?) ['o'] = ((?Д?)+'_') [?Θ?];(?o?)=(?Д?) ['c']+(?Д?) ['o']+(?ω?? +'_')[?Θ?]+ ((?ω??==3) +'_') [???] + ((?Д?) +'_') [(???)+(???)]+ ((???==3) +'_') [?Θ?]+((???==3) +'_') [(???) - (?Θ?)]+(?Д?) ['c']+((?Д?)+'_') [(???)+(???)]+ (?Д?) ['o']+((???==3) +'_') [?Θ?];(?Д?) ['_'] =(o^_^o) [?o?] [?o?];(?ε?)=((???==3) +'_') [?Θ?]+ (?Д?) .?Д??+((?Д?)+'_') [(???) + (???)]+((???==3) +'_') [o^_^o -?Θ?]+((???==3) +'_') [?Θ?]+ (?ω?? +'_') [?Θ?]; (???)+=(?Θ?); (?Д?)[?ε?]='\'; (?Д?).?Θ??=(?Д?+ ???)[o^_^o -(?Θ?)];(o???o)=(?ω?? +'_')[c^_^o];(?Д?) [?o?]='\"';(?Д?) ['_'] ( (?Д?) ['_'] (?ε?+(?Д?)[?o?]+ (?Д?)[?ε?]+(?Θ?)+ ((o^_^o) +(o^_^o))+ ((o^_^o) +(o^_^o))+ (?Д?)[?ε?]+(?Θ?)+ (???)+ (?Θ?)+ (?Д?)[?ε?]+(?Θ?)+ ((o^_^o) +(o^_^o))+ ((o^_^o) - (?Θ?))+ (?Д?)[?ε?]+(???)+ (c^_^o)+ (?Д?)[?ε?]+(?Θ?)+ (???)+ (?Θ?)+ (?Д?)[?ε?]+(???)+ (c^_^o)+ (?Д?)[?ε?]+((???) + (o^_^o))+ ((???) + (?Θ?))+ (?Д?)[?ε?]+(???)+ (c^_^o)+ (?Д?)[?ε?]+((o^_^o) +(o^_^o))+ (?Θ?)+ (?Д?)[?o?])(?Θ?))((?Θ?)+(?Д?)[?ε?]+((???)+(?Θ?))+(?Θ?)+(?Д?)[?o?]);

          jjencode 的結(jié)果:

          $=~[];$={___:++$,$$$$:(![]+"")[$],__$:++$,$_$_:(![]+"")[$],_$_:++$,$_$$:({}+"")[$],$$_$:($[$]+"")[$],_$$:++$,$$$_:(!""+"")[$],$__:++$,$_$:++$,$$__:({}+"")[$],$$_:++$,$$$:++$,$___:++$,$__$:++$};$.$_=($.$_=$+"")[$.$_$]+($._$=$.$_[$.__$])+($.$$=($.$+"")[$.__$])+((!$)+"")[$._$$]+($.__=$.$_[$.$$_])+($.$=(!""+"")[$.__$])+($._=(!""+"")[$._$_])+$.$_[$.$_$]+$.__+$._$+$.$;$.$$=$.$+(!""+"")[$._$$]+$.__+$._+$.$+$.$$;$.$=($.___)[$.$_][$.$_];$.$($.$($.$$+"\""+"\"+$.__$+$.$$_+$.$$_+$.$_$_+"\"+$.__$+$.$$_+$._$_+"\"+$.$__+$.___+$.$_$_+"\"+$.$__+$.___+"=\"+$.$__+$.___+$.__$+"\"")())();

          可以看到,通過這些工具,原本非常簡單的代碼被轉(zhuǎn)化為一些幾乎完全不可讀的代碼,但實(shí)際上運(yùn)行效果還是相同的。這些混淆方式比較另類,看起來雖然沒有什么頭緒,但實(shí)際上找到規(guī)律是非常好還原的,其沒有真正達(dá)到強(qiáng)力混淆的效果。

          以上便是對 JavaScript 混淆方式的介紹和總結(jié)。總的來說,經(jīng)過混淆的 JavaScript 代碼其可讀性大大降低,同時防護(hù)效果也大大增強(qiáng)。

          6. WebAssembly

          隨著技術(shù)的發(fā)展,WebAssembly 逐漸流行起來。不同于 JavaScript 混淆技術(shù), WebAssembly 其基本思路是將一些核心邏輯使用其他語言(如 C/C++ 語言)來編寫,并編譯成類似字節(jié)碼的文件,并通過 JavaScript 調(diào)用執(zhí)行,從而起到二進(jìn)制級別的防護(hù)作用。

          WebAssembly 是一種可以使用非 JavaScript 編程語言編寫代碼并且能在瀏覽器上運(yùn)行的技術(shù)方案,比如借助于我們能將 C/C++ 利用 Emscripten 編譯工具轉(zhuǎn)成 wasm 格式的文件, JavaScript 可以直接調(diào)用該文件執(zhí)行其中的方法。

          WebAssembly 是經(jīng)過編譯器編譯之后的字節(jié)碼,可以從 C/C++ 編譯而來,得到的字節(jié)碼具有和 JavaScript 相同的功能,運(yùn)行速度更快,體積更小,而且在語法上完全脫離 JavaScript,同時具有沙盒化的執(zhí)行環(huán)境。

          比如這就是一個基本的 WebAssembly 示例:

          WebAssembly.compile(
            new Uint8Array(
              `
            00 61 73 6d  01 00 00 00  01 0c 02 60  02 7f 7f 01
            7f 60 01 7f  01 7f 03 03  02 00 01 07  10 02 03 61
            64 64 00 00  06 73 71 75  61 72 65 00  01 0a 13 02
            08 00 20 00  20 01 6a 0f  0b 08 00 20  00 20 00 6c
            0f 0b`
                .trim()
                .split(/[\s\r\n]+/g)
                .map((str) => parseInt(str, 16))
            )
          ).then((module) => {
            const instance = new WebAssembly.Instance(module);
            const { add, square } = instance.exports;
            console.log("2 + 4 =", add(2, 4));
            console.log("3^2 =", square(3));
            console.log("(2 + 5)^2 =", square(add(2 + 5)));
          });

          這里其實(shí)是利用 WebAssembly 定義了兩個方法,分別是 add 和 square,可以分別用于求和和開平方計算。那這兩個方法在哪里聲明的呢?其實(shí)它們被隱藏在了一個 Uint8Array 里面,僅僅查看明文代碼我們確實(shí)無從知曉里面究竟定義了什么邏輯,但確實(shí)是可以執(zhí)行的,我們將這段代碼輸入到瀏覽器控制臺下,運(yùn)行結(jié)果如下:

          2 + 4 = 6
          3^2 = 9
          (2 + 5)^2 = 49

          由此可見,通過 WebAssembly 我們可以成功將核心邏輯“隱藏”起來,這樣某些核心邏輯就不能被輕易找出來了。

          所以,很多網(wǎng)站越來越多使用 WebAssembly 技術(shù)來保護(hù)一些核心邏輯不被輕易被人識別或破解,可以起到更好的防護(hù)效果。

          7. 總結(jié)

          以上,我們就介紹了接口加密技術(shù)和 JavaScript 的壓縮、混淆技術(shù),也對 WebAssembly 技術(shù)有了初步的了解,知己知彼方能百戰(zhàn)不殆,了解了原理,我們才能更好地去實(shí)現(xiàn) JavaScript 的逆向。

          本節(jié)代碼:https://github.com/Python3WebSpider/JavaScriptObfuscate。

          由于本節(jié)涉及一些專業(yè)名詞,部分內(nèi)容參考來源如下:

          • GitHub - javascript-obfuscator 官方 GitHub 倉庫:https://github.com/javascript-obfuscator/javascript-obfuscator
          • 官網(wǎng) - javascript-obfuscator 官網(wǎng):https://obfuscator.io/
          • 博客 - asm.js 和 Emscripten 入門教程:https://www.ruanyifeng.com/blog/2017/09/asmjs_emscripten.html
          • 博客 - JavaScript 混淆安全加固:https://juejin.im/post/5cfcb9d25188257e853fa71c

          主站蜘蛛池模板: 天堂一区人妻无码| 国产在线一区二区在线视频| 一区二区在线观看视频| 久久精品道一区二区三区| 中文字幕人妻丝袜乱一区三区| 亚洲日本一区二区三区在线| 精品一区二区视频在线观看 | 国产日韩综合一区二区性色AV| 精品一区二区三区四区电影| 国产伦精品一区二区三区四区| 无码人妻精品一区二区三区东京热 | 日韩精品一区二区三区四区| 亚洲av无码一区二区三区天堂古代| 欧美激情国产精品视频一区二区| 亚洲一区二区视频在线观看| 无码人妻精品一区二区三区久久| 中文字幕AV无码一区二区三区| 无码人妻精品一区二区三区99不卡| 爆乳熟妇一区二区三区霸乳| 精品福利一区二区三区免费视频| 色系一区二区三区四区五区| 亚洲AV综合色一区二区三区 | 日本在线视频一区| 高清一区二区三区日本久| 国产在线视频一区二区三区98 | 天天爽夜夜爽人人爽一区二区| 久久免费国产精品一区二区| 国产一区二区三区在线| 亚洲视频一区在线观看| 一区二区在线观看视频| 中文字幕日韩人妻不卡一区 | 竹菊影视欧美日韩一区二区三区四区五区 | 无码视频一区二区三区| 视频精品一区二区三区| 国产福利一区二区三区在线视频 | 亚洲乱色熟女一区二区三区蜜臀 | 在线免费视频一区二区| 一区二区免费视频| 久久精品国产一区二区三区不卡| 欧美日韩综合一区二区三区| 国产在线视频一区|