<address id="ttjl9"></address>

      <noframes id="ttjl9"><address id="ttjl9"><nobr id="ttjl9"></nobr></address>
      <form id="ttjl9"></form>
        <em id="ttjl9"><span id="ttjl9"></span></em>
        <address id="ttjl9"></address>

          <noframes id="ttjl9"><form id="ttjl9"></form>

          首頁

          JavaScript必須掌握的基礎 ---> this

          seo達人

          this

          this是我們在書寫代碼時最常用的關鍵詞之一,即使如此,它也是JavaScript最容易被最頭疼的關鍵詞。那么this到底是什么呢?


          如果你了解執行上下文,那么你就會知道,其實this是執行上下文對象的一個屬性:


          executionContext = {

             scopeChain:[ ... ],

             VO:{

                 ...

             },

             this:  ?

          }

          執行上下文中有三個重要的屬性,作用域鏈(scopeChain)、變量對象(VO)和this。


          this是在進入執行上下文時確定的,也就是在函數執行時才確定,并且在運行期間不允許修改并且是永久不變的


          在全局代碼中的this

          在全局代碼中this 是不變的,this始終是全局對象本身。


          var a = 10;

          this.b = 20;

          window.c = 30;


          console.log(this.a);

          console.log(b);

          console.log(this.c);


          console.log(this === window) // true

          // 由于this就是全局對象window,所以上述 a ,b ,c 都相當于在全局對象上添加相應的屬性

          如果我們在代碼運行期嘗試修改this的值,就會拋出錯誤:


          this = { a : 1 } ; // Uncaught SyntaxError: Invalid left-hand side in assignment

          console.log(this === window) // true

          函數代碼中的this

          在函數代碼中使用this,才是令我們最容易困惑的,這里我們主要是對函數代碼中的this進行分析。


          我們在上面說過this的值是,進入當前執行上下文時確定的,也就是在函數執行時并且是執行前確定的。但是同一個函數,作用域中的this指向可能完全不同,但是不管怎樣,函數在運行時的this的指向是不變的,而且不能被賦值。


          function foo() {

             console.log(this);

          }


          foo();  // window

          var obj={

             a: 1,

             bar: foo,

          }

          obj.bar(); // obj

          函數中this的指向豐富的多,它可以是全局對象、當前對象、或者是任意對象,當然這取決于函數的調用方式。在JavaScript中函數的調用方式有一下幾種方式:作為函數調用、作為對象屬性調用、作為構造函數調用、使用apply或call調用。下面我們將按照這幾種調用方式一一討論this的含義。


          作為函數調用

          什么是作為函數調用:就是獨立的函數調用,不加任何修飾符。


          function foo(){

             console.log(this === window); // true

             this.a = 1;

             console.log(b); // 2

          }

          var b = 2;

          foo();

          console.log(a); // 1

          上述代碼中this綁定到了全局對象window。this.a相當于在全局對象上添加一個屬性 a 。


          在嚴格模式下,獨立函數調用,this的綁定不再是window,而是undefined。


          function foo() {

             "use strict";

             console.log(this===window); // false

             console.log(this===undefined); // true

          }

          foo();

          這里要注意,如果函數調用在嚴格模式下,而內部代碼執行在非嚴格模式下,this 還是會默認綁定為 window。


          function foo() {

             console.log(this===window); // true

          }



          (function() {

             "use strict";

             foo();

          })()

          對于在函數內部的函數獨立調用 this 又指向了誰呢?


          function foo() {

             function bar() {

                 this.a=1;

                 console.log(this===window); // true

             }

             bar()

          }

          foo();

          console.log(a); // 1

          上述代碼中,在函數內部的函數獨立調用,此時this還是被綁定到了window。


          總結:當函數作為獨立函數被調用時,內部this被默認綁定為(指向)全局對象window,但是在嚴格模式下會有區別,在嚴格模式下this被綁定為undefined。


          作為對象屬性調用

          var a=1;

          var obj={

             a: 2,

             foo: function() {

                 console.log(this===obj); // true

                 console.log(this.a); // 2

             }

          }

          obj.foo();

          上述代碼中 foo屬性的值為一個函數。這里稱 foo 為 對象obj 的方法。foo的調用方式為 對象 . 方法 調用。此時 this 被綁定到當前調用方法的對象。在這里為 obj 對象。


          再看一個例子:


          var a=1;

          var obj={

             a: 2,

             bar: {

                 a: 3,

                 foo: function() {

                     console.log(this===bar); // true

                     console.log(this.a); // 3

                 }

             }

          }

          obj.bar.foo();

          遵循上面說的規則 對象 . 屬性 。這里的對象為 obj.bar 。此時 foo 內部this被綁定到了 obj.bar 。 因此 this.a 即為 obj.bar.a 。


          再來看一個例子:


          var a=1;

          var obj={

             a: 2,

             foo: function() {

                 console.log(this===obj); // false

                 console.log(this===window); // true

                 console.log(this.a); // 1

             }

          }


          var baz=obj.foo;

          baz();

          這里 foo 函數雖然作為對象obj 的方法。但是它被賦值給變量 baz 。當baz調用時,相當于 foo 函數獨立調用,因此內部 this被綁定到 window。


          使用apply或call調用

          apply和call為函數原型上的方法。它可以更改函數內部this的指向。


          var a=1;

          function foo() {

             console.log(this.a);

          }

          var obj1={

             a: 2

          }

          var obj2={

             a: 3

          }

          var obj3={

             a: 4

          }

          var bar=foo.bind(obj1);

          bar();// 2  this => obj1

          foo(); // 1  this => window

          foo.call(obj2); // 3  this => obj2

          foo.call(obj3); // 4  this => obj3

          當函數foo 作為獨立函數調用時,this被綁定到了全局對象window,當使用bind、call或者apply方法調用時,this 被分別綁定到了不同的對象。


          作為構造函數調用

          var a=1;

          function Person() {

             this.a=2;  // this => p;

          }

          var p=new Person();

          console.log(p.a); // 2

          上述代碼中,構造函數 Person 內部的 this 被綁定為 Person的一個實例。


          總結:


          當我們要判斷當前函數內部的this綁定,可以依照下面的原則:


          函數是否在是通過 new 操作符調用?如果是,this 綁定為新創建的對象

          var bar = new foo();     // this => bar;

          函數是否通過call或者apply調用?如果是,this 綁定為指定的對象

          foo.call(obj1);  // this => obj1;

          foo.apply(obj2);  // this => obj2;

          函數是否通過 對象 . 方法調用?如果是,this 綁定為當前對象

          obj.foo(); // this => obj;

          函數是否獨立調用?如果是,this 綁定為全局對象。

          foo(); // this => window

          DOM事件處理函數中的this

          1). 事件綁定


          <button id="btn">點擊我</button>


          // 事件綁定


          function handleClick(e) {

             console.log(this); // <button id="btn">點擊我</button>

          }

                 document.getElementById('btn').addEventListener('click',handleClick,false);  //   <button id="btn">點擊我</button>

                 

          document.getElementById('btn').onclick= handleClick; //  <button id="btn">點擊我</button>

          根據上述代碼我們可以得出:當通過事件綁定來給DOM元素添加事件,事件將被綁定為當前DOM對象。


          2).內聯事件


          <button onclick="handleClick()" id="btn1">點擊我</button>

          <button onclick="console.log(this)" id="btn2">點擊我</button>


          function handleClick(e) {

             console.log(this); // window

          }


          //第二個 button 打印的是   <button id="btn">點擊我</button>

          我認為內聯事件可以這樣理解:


          //偽代碼


          <button onclick=function(){  handleClick() } id="btn1">點擊我</button>

          <button onclick=function() { console.log(this) } id="btn2">點擊我</button>

          這樣我們就能理解上述代碼中為什么內聯事件一個指向window,一個指向當前DOM元素。(當然瀏覽器處理內聯事件時并不是這樣的)


          定時器中的this

          定時器中的 this 指向哪里呢?


          function foo() {

             setTimeout(function() {

                 console.log(this); // window

             },1000)

          }

          foo();  

          再來看一個例子


          var name="chen";

          var obj={

             name: "erdong",

             foo: function() {

                 console.log(this.name); // erdong

                 setTimeout(function() {

                     console.log(this.name); // chen

                 },1000)

             }

          }

          obj.foo();

          到這里我們可以看到,函數 foo 內部this指向為調用它的對象,即:obj 。定時器中的this指向為 window。那么有什么辦法讓定時器中的this跟包裹它的函數綁定為同一個對象呢?


          1). 利用閉包:


          var name="chen";

          var obj={

             name: "erdong",

             foo: function() {

                 console.log(this.name) // erdong

                 var that=this;

                 setTimeout(function() {

                     // that => obj

                     console.log(that.name); // erdong

                 },1000)

             }

          }

          obj.foo();

          利用閉包的特性,函數內部的函數可以訪問含義訪問當前詞法作用域中的變量,此時定時器中的 that 即為包裹它的函數中的 this 綁定的對象。在下面我們會介紹利用 ES6的箭頭函數實現這一功能。


          當然這里也可以適用bind來實現:


          var name="chen";

          var obj={

             name: "erdong",

             foo: function() {

                 console.log(this.name); // erdong

                 setTimeout(function() {

                     // this => obj

                     console.log(this.name); // erdong

                 }.bind(this),1000)

             }

          }

          obj.foo();

          被忽略的this

          如果你把 null 或者 undefined 作為 this 的綁定對象傳入 call 、apply或者bind,這些值在調用時會被忽略,實例 this 被綁定為對應上述規則。


          var a=1;

          function foo() {

             console.log(this.a); // 1  this => window

          }

          var obj={

             a: 2

          }

          foo.call(null);

          var a=1;

          function foo() {

             console.log(this.a); // 1  this => window

          }

          var obj={

             a: 2

          }

          foo.apply(null);

          var a=1;

          function foo() {

             console.log(this.a); // 1  this => window

          }

          var obj={

             a: 2

          }

          var bar = foo.bind(null);

          bar();

          bind 也可以實現函數柯里化:


          function foo(a,b) {

             console.log(a,b); // 2  3

          }

          var bar=foo.bind(null,2);

          bar(3);

          更復雜的例子:


          var foo={

             bar: function() {

                 console.log(this);

             }

          };


          foo.bar(); // foo

          (foo.bar)(); // foo


          (foo.bar=foo.bar)(); // window

          (false||foo.bar)();  // window

          (foo.bar,foo.bar)();  // window

          上述代碼中:


          foo.bar()為對象的方法調用,因此 this 綁定為 foo 對象。


          (foo.bar)() 前一個() 中的內容不計算,因此還是 foo.bar()


          (foo.bar=foo.bar)() 前一個 () 中的內容計算后為 function() { console.log(this); } 所以這里為匿名函數自執行,因此 this 綁定為 全局對象 window


          后面兩個實例同上。


          這樣理解會比較好:


          (foo.bar=foo.bar)  括號中的表達式執行為 先計算,再賦值,再返回值。

          (false||foo.bar)()    括號中的表達式執行為 判斷前者是否為 true ,若為true,不計算后者,若為false,計算后者并返回后者的值。

          (foo.bar,foo.bar)   括號中的表達式之行為分別計算 “,” 操作符兩邊,然后返回  “,” 操作符后面的值。

          箭頭函數中的this

          箭頭函數時ES6新增的語法。


          有兩個作用:


          更簡潔的函數

          本身不綁定this

          代碼格式為:


          // 普通函數

          function foo(a){

             // ......

          }

          //箭頭函數

          var foo = a => {

             // ......

          }


          //如果沒有參數或者參數為多個


          var foo = (a,b,c,d) => {

             // ......

          }

          我們在使用普通函數之前對于函數的this綁定,需要根據這個函數如何被調用來確定其內部this的綁定對象。而且常常因為調用鏈的數量或者是找不到其真正的調用者對 this 的指向模糊不清。在箭頭函數出現后其內部的 this 指向不需要再依靠調用的方式來確定。


          箭頭函數有幾個特點(與普通函數的區別)


          箭頭函數不綁定 this 。它只會從作用域鏈的上一層繼承 this。

          箭頭函數不綁定arguments,使用reset參數來獲取實參的數量。

          箭頭函數是匿名函數,不能作為構造函數。

          箭頭函數沒有prototype屬性。

          不能使用 yield 關鍵字,因此箭頭函數不能作為函數生成器。

          這里我們只討論箭頭函數中的this綁定。


          用一個例子來對比普通函數與箭頭函數中的this綁定:


          var obj={

             foo: function() {

                 console.log(this); // obj

             },

             bar: () => {

                 console.log(this); // window

             }

          }

          obj.foo();

          obj.bar();

          上述代碼中,同樣是通過對象 . 方法調用一個函數,但是函數內部this綁定確是不同,只因一個數普通函數一個是箭頭函數。


          用一句話來總結箭頭函數中的this綁定:


          個人上面說的它會從作用域鏈的上一層繼承 this ,說法并不是很正確。作用域中存放的是這個函數當前執行上下文與所有父級執行上下文的變量對象的集合。因此在作用域鏈中并不存在 this 。應該說是作用域鏈上一層對應的執行上下文中繼承 this 。


          箭頭函數中的this繼承于作用域鏈上一層對應的執行上下文中的this


          var obj={

             foo: function() {

                 console.log(this); // obj

             },

             bar: () => {

                 console.log(this); // window

             }

          }

          obj.bar();

          上述代碼中obj.bar執行時的作用域鏈為:


          scopeChain = [

             obj.bar.AO,

             global.VO

          ]

          根據上面的規則,此時bar函數中的this指向為全局執行上下文中的this,即:window。


          再來看一個例子:


          var obj={

             foo: function() {

                 console.log(this); // obj

                 var bar=() => {

                     console.log(this); // obj

                 }

                 bar();

             }

          }

          obj.foo();

          在普通函數中,bar 執行時內部this被綁定為全局對象,因為它是作為獨立函數調用。但是在箭頭函數中呢,它卻綁定為 obj 。跟父級函數中的 this 綁定為同一對象。


          此時它的作用域鏈為:


          scopeChain = [

              bar.AO,

              obj.foo.AO,

              global.VO

          ]

          這個時候我們就差不多知道了箭頭函數中的this綁定。


          繼續看例子:


          var obj={

             foo: () => {

                 console.log(this); // window

                 var bar=() => {

                     console.log(this); // window

                 }

                 bar();

             }

          }

          obj.foo();

          這個時候怎么又指向了window了呢?


          我們還看當 bar 執行時的作用域鏈:


          scopeChain = [

              bar.AO,

              obj.foo.AO,

              global.VO

          ]

          當我們找bar函數中的this綁定時,就會去找foo函數中的this綁定。因為它是繼承于它的。這時 foo 函數也是箭頭函數,此時foo中的this綁定為window而不是調用它的obj對象。因此 bar函數中的this綁定也為全局對象window。


          我們在回頭看上面關于定時器中的this的例子:


          var name="chen";

          var obj={

             name: "erdong",

             foo: function() {

                 console.log(this.name); // erdong

                 setTimeout(function() {

                     console.log(this); // chen

                 },1000)

             }

          }

          obj.foo();

          這時我們就可以很簡單的讓定時器中的this與foo中的this綁定為同一對象:


          var name="chen";

          var obj={

             name: "erdong",

             foo: function() {

                 // this => obj

                 console.log(this.name); // erdong

                 setTimeout(() =>  {

                     // this => foo中的this => obj

                     console.log(this.name); // erdong

                 },1000)

             }

          }

          obj.foo();

          JavaScript的padStart()和padEnd()格式化字符串使用技巧

          seo達人

          用例

          讓我們從介紹幾種不同的填充用例開始。


          標簽和值

          假設你在同一行上有標簽和值,例如 name:zhangsan 和 Phone Number:(555)-555-1234。如果把他們放在一起看起來會有點奇怪,會是這樣:


          Name: zhangsan

          Phone Number: (555)-555-1234

          你可能想要這個。


          Name:           zhangsan

          Phone Number:   (555)555-1234

          或這個...


                 Name: zhangsan

          Phone Number: (555)555-1234

          金額

          在中國,顯示價格時通常顯示兩位數的角、分。所以代替這個...


          ¥10.1

          你會想要這個。


          ¥10.01

          日期

          對于日期,日期和月份都需要2位數字。所以代替這個...


          2020-5-4

          你會想要這個。


          2020-05-04

          時間

          與上面的日期類似,對于計時器,你需要2位數字表示秒,3位數字表示毫秒。所以代替這個...


          1:1

          你會想要這個。


          01:001

          padstart()

          讓我們從 padStart() 以及標簽和值示例開始。假設我們希望標簽彼此正確對齊,以使值在同一位置開始。


                 Name: zhangsan

          Phone Number: (555)555-1234

          由于 Phone Number 是兩個標簽中較長的一個,因此我們要在 Name 標簽的開頭加上空格。為了將來的需要,我們不要把它專門填充到電話號碼的長度,我們把它填充到長一點,比如說20個字符。這樣一來,如果你在未來使用較長的標簽,這一招仍然有效。


          在填充之前,這是用于顯示此信息的入門代碼。


          const label1 = "Name";

          const label2 = "Phone Number";

          const name = "zhangsan"

          const phoneNumber = "(555)-555-1234";


          console.log(label1 + ": " + name);

          console.log(label2 + ": " + phoneNumber);


          //Name: zhangsan

          //Phone Number: (555)-555-1234

          現在,讓我們填充第一個標簽。要調用 padStart(),你需要傳遞兩個參數:一個用于填充字符串的目標長度,另一個用于你希望填充的字符。在這種情況下,我們希望長度為20,而填充字符為空格。


          const label1 = "Name";

          const label2 = "Phone Number";

          const name = "zhangsan"

          const phoneNumber = "(555)-555-1234";


          console.log(label1.padStart(20, " ") + ": " + name);

          console.log(label2 + ": " + phoneNumber);


          //               Name: zhangsan

          ////Phone Number: (555)-555-1234

          現在填充第二行。


          const label1 = "Name";

          const label2 = "Phone Number";

          const name = "zhangsan"

          const phoneNumber = "(555)-555-1234";


          console.log(label1.padStart(20, " ") + ": " + name);

          console.log(label2.padStart(20, " ") + ": " + phoneNumber);


          //               Name: zhangsan

          ////     Phone Number: (555)-555-1234

          padEnd()

          對于相同的標簽和值示例,讓我們更改填充標簽的方式。讓我們將標簽向左對齊,以便在末尾添加填充。


          初始代碼


          const label1 = "Name";

          const label2 = "Phone Number";

          const name = "zhangsan"

          const phoneNumber = "(555)-555-1234";


          console.log(label1 + ": " + name);

          console.log(label2 + ": " + phoneNumber);


          //Name: zhangsan

          //Phone Number: (555)-555-1234

          現在,讓我們填充第一個標簽,與我們之前所做的類似,但有兩個小區別?,F在,我們使用 padEnd() 而不是padStart(),并且需要在填充之前將冒號與標簽連接起來,這樣我們就能確保冒號在正確的位置。


          const label1 = "Name";

          const label2 = "Phone Number";

          const name = "zhangsan"

          const phoneNumber = "(555)-555-1234";


          console.log((label1 + ': ').padEnd(20, ' ') + name);

          console.log(label2 + ": " + phoneNumber);


          //Name:               zhangsan

          //Phone Number: (555)-555-1234

          現在兩行都已填充。


          const label1 = "Name";

          const label2 = "Phone Number";

          const name = "zhangsan"

          const phoneNumber = "(555)-555-1234";


          console.log((label1 + ': ').padEnd(20, ' ') + name);

          console.log((label2 + ': ').padEnd(20, ' ') + phoneNumber);


          //Name:               zhangsan

          //Phone Number:       (555)-555-1234

          數字(價格、日期、計時器等)呢?

          padding函數是專門針對字符串而不是數字的,所以,我們需要先將數字轉換為字符串。


          價格

          讓我們看一下顯示價格的初始代碼。


          const rmb = 10;

          const cents = 1;

          console.log("¥" + rmb + "." + cents); //¥10.1

          要填充分,我們需要先將其轉換為字符串,然后調用 padStart() 函數,指定長度為1且填充字符為'0';


          const rmb = 10;

          const cents = 1;

          console.log("¥" + rmb + "." + cents.toString().padStart(2,0)); //¥10.01

          日期

          這是顯示日期的初始代碼。


          const month = 2;

          const year = 2020;


          console.log(year + "-" + month); //2020-2

          現在,讓我們填充月份以確保它是兩位數。


          const month = 2;

          const year = 2020;


          console.log(year + "-" + month.toString().padStart(2,"0")); // 2020-02

          計時器

          最后是我們的計時器,我們要格式化兩個不同的數字,即秒和毫秒。盡管有相同的原則。這是初始代碼。


          const seconds = 1;

          const ms = 1;


          console.log(seconds + ":" + ms); //1:1

          現在要填充,我將在單獨的行上進行填充,以便于閱讀。


          const seconds = 1;

          const formattedSeconds = seconds.toString().padStart(2,0);

          const ms = 1;

          const formattedMs = ms.toString().padStart(3,0);


          console.log(formattedSeconds + ":" + formattedMs); // 01:001

          最后

          雖然編寫自己的padding函數并不難,但既然已經內置在JavaScript中,為什么還要自己去做呢?有很多有趣的函數已經內置了。在你自己構建一些東西之前,可能值得先快速搜索一下。

          認識 ESLint 和 Prettier

          seo達人

          ESLint

          先說是什么:ESLint 是一個檢查代碼質量與風格的工具,配置一套規則,他就能檢查出你代碼中不符合規則的地方,部分問題支持自動修復。


          使用這么一套規則有什么用呢?如果單人開發的話倒是沒什么了,但是一個團隊若是存在兩種風格,那格式化之后處理代碼沖突就真的要命了,統一的代碼風格真的很重要!


          (其實以前自己做一個項目的時候,公司電腦和家庭電腦的代碼風格配置不一樣,在家加班的時候也經常順手格式化了,這么循環了幾次不同的風格,導致 diff 極其混亂

          JavaScript中的Event Loop(事件循環)機制

          seo達人

          事件循環

          JavaScript是單線程,非阻塞的

          瀏覽器的事件循環


          執行棧和事件隊列

          宏任務和微任務

          node環境下的事件循環


          和瀏覽器環境有何不同

          事件循環模型

          宏任務和微任務

          經典題目分析

          1. JavaScript是單線程,非阻塞的

          單線程:


          JavaScript的主要用途是與用戶互動,以及操作DOM。如果它是多線程的會有很多復雜的問題要處理,比如有兩個線程同時操作DOM,一個線程刪除了當前的DOM節點,一個線程是要操作當前的DOM階段,最后以哪個線程的操作為準?為了避免這種,所以JS是單線程的。即使H5提出了web worker標準,它有很多限制,受主線程控制,是主線程的子線程。


          非阻塞:通過 event loop 實現。


          2. 瀏覽器的事件循環

          執行棧和事件隊列

          為了更好地理解Event Loop,請看下圖(轉引自Philip Roberts的演講 《Help, I'm stuck in an event-loop》)

          Help, I'm stuck in an event-loop


          執行棧: 同步代碼的執行,按照順序添加到執行棧中


          function a() {

             b();

             console.log('a');

          }

          function b() {

             console.log('b')

          }

          a();

          我們可以通過使用 Loupe(Loupe是一種可視化工具,可以幫助您了解JavaScript的調用堆棧/事件循環/回調隊列如何相互影響)工具來了解上面代碼的執行情況。


          調用情況


          執行函數 a()先入棧

          a()中先執行函數 b() 函數b() 入棧

          執行函數b(), console.log('b') 入棧

          輸出 b, console.log('b')出棧

          函數b() 執行完成,出棧

          console.log('a') 入棧,執行,輸出 a, 出棧

          函數a 執行完成,出棧。

          事件隊列: 異步代碼的執行,遇到異步事件不會等待它返回結果,而是將這個事件掛起,繼續執行執行棧中的其他任務。當異步事件返回結果,將它放到事件隊列中,被放入事件隊列不會立刻執行起回調,而是等待當前執行棧中所有任務都執行完畢,主線程空閑狀態,主線程會去查找事件隊列中是否有任務,如果有,則取出排在第一位的事件,并把這個事件對應的回調放到執行棧中,然后執行其中的同步代碼。


          我們再上面代碼的基礎上添加異步事件,


          function a() {

             b();

             console.log('a');

          }

          function b() {

             console.log('b')

             setTimeout(function() {

                 console.log('c');

             }, 2000)

          }

          a();

          此時的執行過程如下

          img


          我們同時再加上點擊事件看一下運行的過程


          $.on('button', 'click', function onClick() {

             setTimeout(function timer() {

                 console.log('You clicked the button!');    

             }, 2000);

          });


          console.log("Hi!");


          setTimeout(function timeout() {

             console.log("Click the button!");

          }, 5000);


          console.log("Welcome to loupe.");

          img


          簡單用下面的圖進行一下總結


          執行棧和事件隊列


          宏任務和微任務

          為什么要引入微任務,只有一種類型的任務不行么?


          頁面渲染事件,各種IO的完成事件等隨時被添加到任務隊列中,一直會保持先進先出的原則執行,我們不能準確地控制這些事件被添加到任務隊列中的位置。但是這個時候突然有高優先級的任務需要盡快執行,那么一種類型的任務就不合適了,所以引入了微任務隊列。


          不同的異步任務被分為:宏任務和微任務

          宏任務:


          script(整體代碼)

          setTimeout()

          setInterval()

          postMessage

          I/O

          UI交互事件

          微任務:


          new Promise().then(回調)

          MutationObserver(html5 新特性)

          運行機制

          異步任務的返回結果會被放到一個任務隊列中,根據異步事件的類型,這個事件實際上會被放到對應的宏任務和微任務隊列中去。


          在當前執行棧為空時,主線程會查看微任務隊列是否有事件存在


          存在,依次執行隊列中的事件對應的回調,直到微任務隊列為空,然后去宏任務隊列中取出最前面的事件,把當前的回調加到當前指向棧。

          如果不存在,那么再去宏任務隊列中取出一個事件并把對應的回到加入當前執行棧;

          當前執行棧執行完畢后時會立刻處理所有微任務隊列中的事件,然后再去宏任務隊列中取出一個事件。同一次事件循環中,微任務永遠在宏任務之前執行。


          在事件循環中,每進行一次循環操作稱為 tick,每一次 tick 的任務處理模型是比較復雜的,但關鍵步驟如下:


          執行一個宏任務(棧中沒有就從事件隊列中獲?。?

          執行過程中如果遇到微任務,就將它添加到微任務的任務隊列中

          宏任務執行完畢后,立即執行當前微任務隊列中的所有微任務(依次執行)

          當前宏任務執行完畢,開始檢查渲染,然后GUI線程接管渲染

          渲染完畢后,JS線程繼續接管,開始下一個宏任務(從事件隊列中獲?。?

          簡單總結一下執行的順序:

          執行宏任務,然后執行該宏任務產生的微任務,若微任務在執行過程中產生了新的微任務,則繼續執行微任務,微任務執行完畢后,再回到宏任務中進行下一輪循環。


          宏任務和微任務


          深入理解js事件循環機制(瀏覽器篇) 這邊文章中有個特別形象的動畫,大家可以看著理解一下。


          console.log('start')


          setTimeout(function() {

           console.log('setTimeout')

          }, 0)


          Promise.resolve().then(function() {

           console.log('promise1')

          }).then(function() {

           console.log('promise2')

          })


          console.log('end')

          瀏覽器事件循環


          全局代碼壓入執行棧執行,輸出 start

          setTimeout壓入 macrotask隊列,promise.then 回調放入 microtask隊列,最后執行 console.log('end'),輸出 end

          調用棧中的代碼執行完成(全局代碼屬于宏任務),接下來開始執行微任務隊列中的代碼,執行promise回調,輸出 promise1, promise回調函數默認返回 undefined, promise狀態變成 fulfilled ,觸發接下來的 then回調,繼續壓入 microtask隊列,此時產生了新的微任務,會接著把當前的微任務隊列執行完,此時執行第二個 promise.then回調,輸出 promise2

          此時,microtask隊列 已清空,接下來會會執行 UI渲染工作(如果有的話),然后開始下一輪 event loop, 執行 setTimeout的回調,輸出 setTimeout

          最后的執行結果如下


          start

          end

          promise1

          promise2

          setTimeout

          node環境下的事件循環

          和瀏覽器環境有何不同

          表現出的狀態與瀏覽器大致相同。不同的是 node 中有一套自己的模型。node 中事件循環的實現依賴 libuv 引擎。Node的事件循環存在幾個階段。


          如果是node10及其之前版本,microtask會在事件循環的各個階段之間執行,也就是一個階段執行完畢,就會去執行 microtask隊列中的任務。


          node版本更新到11之后,Event Loop運行原理發生了變化,一旦執行一個階段里的一個宏任務(setTimeout,setInterval和setImmediate)就立刻執行微任務隊列,跟瀏覽器趨于一致。下面例子中的代碼是按照的去進行分析的。


          事件循環模型

          ┌───────────────────────┐

          ┌─>│        timers         │

          │  └──────────┬────────────┘

          │  ┌──────────┴────────────┐

          │  │     I/O callbacks     │

          │  └──────────┬────────────┘

          │  ┌──────────┴────────────┐

          │  │     idle, prepare     │

          │  └──────────┬────────────┘      ┌───────────────┐

          │  ┌──────────┴────────────┐      │   incoming:   │

          │  │         poll          │<──connections───     │

          │  └──────────┬────────────┘      │   data, etc.  │

          │  ┌──────────┴────────────┐      └───────────────┘

          │  │        check          │

          │  └──────────┬────────────┘

          │  ┌──────────┴────────────┐

          └──┤    close callbacks    │

            └───────────────────────┘

          事件循環各階段詳解

          node中事件循環的順序


          外部輸入數據 --> 輪詢階段(poll) --> 檢查階段(check) --> 關閉事件回調階段(close callback) --> 定時器檢查階段(timer) --> I/O 事件回調階段(I/O callbacks) --> 閑置階段(idle, prepare) --> 輪詢階段...


          這些階段大致的功能如下:


          定時器檢測階段(timers): 這個階段執行定時器隊列中的回調如 setTimeout() 和 setInterval()。

          I/O事件回調階段(I/O callbacks): 這個階段執行幾乎所有的回調。但是不包括close事件,定時器和setImmediate()的回調。

          閑置階段(idle, prepare): 這個階段僅在內部使用,可以不必理會

          輪詢階段(poll): 等待新的I/O事件,node在一些特殊情況下會阻塞在這里。

          檢查階段(check): setImmediate()的回調會在這個階段執行。

          關閉事件回調階段(close callbacks): 例如socket.on('close', ...)這種close事件的回調

          poll:

          這個階段是輪詢時間,用于等待還未返回的 I/O 事件,比如服務器的回應、用戶移動鼠標等等。

          這個階段的時間會比較長。如果沒有其他異步任務要處理(比如到期的定時器),會一直停留在這個階段,等待 I/O 請求返回結果。

          check:

          該階段執行setImmediate()的回調函數。


          close:

          該階段執行關閉請求的回調函數,比如socket.on('close', ...)。


          timer階段:

          這個是定時器階段,處理setTimeout()和setInterval()的回調函數。進入這個階段后,主線程會檢查一下當前時間,是否滿足定時器的條件。如果滿足就執行回調函數,否則就離開這個階段。


          I/O callback階段:

          除了以下的回調函數,其他都在這個階段執行:


          setTimeout()和setInterval()的回調函數

          setImmediate()的回調函數

          用于關閉請求的回調函數,比如socket.on('close', ...)

          宏任務和微任務

          宏任務:


          setImmediate

          setTimeout

          setInterval

          script(整體代碼)

          I/O 操作等。

          微任務:


          process.nextTick

          new Promise().then(回調)

          Promise.nextTick, setTimeout, setImmediate的使用場景和區別

          Promise.nextTick

          process.nextTick 是一個獨立于 eventLoop 的任務隊列。

          在每一個 eventLoop 階段完成后會去檢查 nextTick 隊列,如果里面有任務,會讓這部分任務優先于微任務執行。

          是所有異步任務中最快執行的。


          setTimeout:

          setTimeout()方法是定義一個回調,并且希望這個回調在我們所指定的時間間隔后第一時間去執行。


          setImmediate:

          setImmediate()方法從意義上將是立刻執行的意思,但是實際上它卻是在一個固定的階段才會執行回調,即poll階段之后。


          經典題目分析

          一. 下面代碼輸出什么

          async function async1() {

             console.log('async1 start');

             await async2();

             console.log('async1 end');

          }

          async function async2() {

             console.log('async2');

          }

          console.log('script start');

          setTimeout(function() {

             console.log('setTimeout');

          }, 0)

          async1();

          new Promise(function(resolve) {

             console.log('promise1');

             resolve();

          }).then(function() {

             console.log('promise2');

          });

          console.log('script end');

          先執行宏任務(當前代碼塊也算是宏任務),然后執行當前宏任務產生的微任務,然后接著執行宏任務


          從上往下執行代碼,先執行同步代碼,輸出 script start

          遇到setTimeout,現把 setTimeout 的代碼放到宏任務隊列中

          執行 async1(),輸出 async1 start, 然后執行 async2(), 輸出 async2,把 async2() 后面的代碼 console.log('async1 end')放到微任務隊列中

          接著往下執行,輸出 promise1,把 .then()放到微任務隊列中;注意Promise本身是同步的立即執行函數,.then是異步執行函數

          接著往下執行, 輸出 script end。同步代碼(同時也是宏任務)執行完成,接下來開始執行剛才放到微任務中的代碼

          依次執行微任務中的代碼,依次輸出 async1 end、 promise2, 微任務中的代碼執行完成后,開始執行宏任務中的代碼,輸出 setTimeout

          最后的執行結果如下


          script start

          async1 start

          async2

          promise1

          script end

          async1 end

          promise2

          setTimeout

          二. 下面代碼輸出什么

          console.log('start');

          setTimeout(() => {

             console.log('children2');

             Promise.resolve().then(() => {

                 console.log('children3');

             })

          }, 0);


          new Promise(function(resolve, reject) {

             console.log('children4');

             setTimeout(function() {

                 console.log('children5');

                 resolve('children6')

             }, 0)

          }).then((res) => {

             console.log('children7');

             setTimeout(() => {

                 console.log(res);

             }, 0)

          })

          這道題跟上面題目不同之處在于,執行代碼會產生很多個宏任務,每個宏任務中又會產生微任務


          從上往下執行代碼,先執行同步代碼,輸出 start

          遇到setTimeout,先把 setTimeout 的代碼放到宏任務隊列①中

          接著往下執行,輸出 children4, 遇到setTimeout,先把 setTimeout 的代碼放到宏任務隊列②中,此時.then并不會被放到微任務隊列中,因為 resolve是放到 setTimeout中執行的

          代碼執行完成之后,會查找微任務隊列中的事件,發現并沒有,于是開始執行宏任務①,即第一個 setTimeout, 輸出 children2,此時,會把 Promise.resolve().then放到微任務隊列中。

          宏任務①中的代碼執行完成后,會查找微任務隊列,于是輸出 children3;然后開始執行宏任務②,即第二個 setTimeout,輸出 children5,此時將.then放到微任務隊列中。

          宏任務②中的代碼執行完成后,會查找微任務隊列,于是輸出 children7,遇到 setTimeout,放到宏任務隊列中。此時微任務執行完成,開始執行宏任務,輸出 children6;

          最后的執行結果如下


          start

          children4

          children2

          children3

          children5

          children7

          children6

          三. 下面代碼輸出什么

          const p = function() {

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

                 const p1 = new Promise((resolve, reject) => {

                     setTimeout(() => {

                         resolve(1)

                     }, 0)

                     resolve(2)

                 })

                 p1.then((res) => {

                     console.log(res);

                 })

                 console.log(3);

                 resolve(4);

             })

          }



          p().then((res) => {

             console.log(res);

          })

          console.log('end');

          執行代碼,Promise本身是同步的立即執行函數,.then是異步執行函數。遇到setTimeout,先把其放入宏任務隊列中,遇到p1.then會先放到微任務隊列中,接著往下執行,輸出 3

          遇到 p().then 會先放到微任務隊列中,接著往下執行,輸出 end

          同步代碼塊執行完成后,開始執行微任務隊列中的任務,首先執行 p1.then,輸出 2, 接著執行p().then, 輸出 4

          微任務執行完成后,開始執行宏任務,setTimeout, resolve(1),但是此時 p1.then已經執行完成,此時 1不會輸出。

          最后的執行結果如下


          3

          end

          2

          4

          你可以將上述代碼中的 resolve(2)注釋掉, 此時 1才會輸出,輸出結果為 3 end 4 1。


          const p = function() {

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

                 const p1 = new Promise((resolve, reject) => {

                     setTimeout(() => {

                         resolve(1)

                     }, 0)

                 })

                 p1.then((res) => {

                     console.log(res);

                 })

                 console.log(3);

                 resolve(4);

             })

          }



          p().then((res) => {

             console.log(res);

          })

          console.log('end');

          3

          end

          4

          1

          最后強烈推薦幾個非常好的講解 event loop 的視頻:


          What the heck is the event loop anyway? | Philip Roberts | JSConf EU

          Jake Archibald: In The Loop - JSConf.Asia

          了不起的 tsconfig.json 指南

          seo達人

          在 TypeScript 開發中,tsconfig.json 是個不可或缺的配置文件,它是我們在 TS 項目中最常見的配置文件,那么你真的了解這個文件嗎?它里面都有哪些優秀配置?如何配置一個合理的 tsconfig.json 文件?本文將全面帶大家一起詳細了解 tsconfig.json 的各項配置。



          本文將從以下幾個方面全面介紹 tsconfig.json 文件:

          了不起的 tsconfig.json 指南.png




          水平有限,歡迎各位大佬指點~~


          一、tsconfig.json 簡介


          1. 什么是 tsconfig.json

          TypeScript 使用 tsconfig.json 文件作為其配置文件,當一個目錄中存在 tsconfig.json 文件,則認為該目錄為 TypeScript 項目的根目錄。

          通常 tsconfig.json 文件主要包含兩部分內容:指定待編譯文件和定義編譯選項。



          從《TypeScript編譯器的配置文件的JSON模式》可知,目前 tsconfig.json 文件有以下幾個頂層屬性:


          compileOnSave

          compilerOptions

          exclude

          extends

          files

          include

          references

          typeAcquisition


          文章后面會詳細介紹一些常用屬性配置。



          2. 為什么使用 tsconfig.json

          通常我們可以使用 tsc 命令來編譯少量 TypeScript 文件:


          /*

           參數介紹:

           --outFile // 編譯后生成的文件名稱

           --target  // 指定ECMAScript目標版本

           --module  // 指定生成哪個模塊系統代碼

           index.ts  // 源文件

          */

          $ tsc --outFile leo.js --target es3 --module amd index.ts

          但如果實際開發的項目,很少是只有單個文件,當我們需要編譯整個項目時,就可以使用 tsconfig.json 文件,將需要使用到的配置都寫進 tsconfig.json 文件,這樣就不用每次編譯都手動輸入配置,另外也方便團隊協作開發。



          二、使用 tsconfig.json

          目前使用 tsconfig.json 有2種操作:


          1. 初始化 tsconfig.json

          在初始化操作,也有 2 種方式:


          手動在項目根目錄(或其他)創建 tsconfig.json 文件并填寫配置;

          通過 tsc --init 初始化 tsconfig.json 文件。


          2. 指定需要編譯的目錄

          在不指定輸入文件的情況下執行 tsc 命令,默認從當前目錄開始編譯,編譯所有 .ts 文件,并且從當前目錄開始查找 tsconfig.json 文件,并逐級向上級目錄搜索。


          $ tsc

          另外也可以為 tsc 命令指定參數 --project 或 -p 指定需要編譯的目錄,該目錄需要包含一個 tsconfig.json 文件,如:


          /*

           文件目錄:

           ├─src/

           │  ├─index.ts

           │  └─tsconfig.json

           ├─package.json

          */

          $ tsc --project src

          注意,tsc 的命令行選項具有優先級,會覆蓋 tsconfig.json 中的同名選項。



          更多 tsc 編譯選項,可查看《編譯選項》章節。



          三、使用示例

          這個章節,我們將通過本地一個小項目 learnTsconfig 來學著實現一個簡單配置。

          當前開發環境:windows / node 10.15.1 / TypeScript3.9



          1. 初始化 learnTsconfig 項目

          執行下面命令:


          $ mkdir learnTsconfig

          $ cd .\learnTsconfig\

          $ mkdir src

          $ new-item index.ts

          并且我們為 index.ts 文件寫一些簡單代碼:


          // 返回當前版本號

          function getVersion(version:string = "1.0.0"): string{

             return version;

          }


          console.log(getVersion("1.0.1"))

          我們將獲得這么一個目錄結構:


           └─src/

              └─index.ts


          2. 初始化 tsconfig.json 文件

          在 learnTsconfig 根目錄執行:


          $ tsc --init


          3. 修改 tsconfig.json 文件

          我們設置幾個常見配置項:


          {

           "compilerOptions": {

             "target": "ES5",             // 目標語言的版本

             "module": "commonjs",        // 指定生成代碼的模板標準

             "noImplicitAny": true,       // 不允許隱式的 any 類型

             "removeComments": true,      // 刪除注釋

             "preserveConstEnums": true,  // 保留 const 和 enum 聲明

             "sourceMap": true            // 生成目標文件的sourceMap文件

           },

           "files": [   // 指定待編譯文件

             "./src/index.ts"  

           ]

          }

          其中需要注意一點:

          files 配置項值是一個數組,用來指定了待編譯文件,即入口文件。

          當入口文件依賴其他文件時,不需要將被依賴文件也指定到 files 中,因為編譯器會自動將所有的依賴文件歸納為編譯對象,即 index.ts 依賴 user.ts 時,不需要在 files 中指定 user.ts , user.ts 會自動納入待編譯文件。



          4. 執行編譯

          配置完成后,我們可以在命令行執行 tsc 命令,執行編譯完成后,我們可以得到一個 index.js 文件和一個 index.js.map 文件,證明我們編譯成功,其中 index.js 文件內容如下:


          function getVersion(version) {

             if (version === void 0) { version = "1.0.0"; }

             return version;

          }

          console.log(getVersion("1.0.1"));

          //# sourceMappingURL=index.js.map

          可以看出,tsconfig.json 中的 removeComments 配置生效了,將我們添加的注釋代碼移除了。



          到這一步,就完成了這個簡單的示例,接下來會基于這個示例代碼,講解《七、常見配置示例》。



          四、tsconfig.json 文件結構介紹


          1. 按頂層屬性分類

          在 tsconfig.json 文件中按照頂層屬性,分為以下幾類:

          tsconfig.json 文件結構(頂層屬性).png


          了不起的 tsconfig.json 指南.png



          2. 按功能分類

          tsconfig.json 文件結構(功能).png




          五、tsconfig.json 配置介紹


          1. compileOnSave

          compileOnSave 屬性作用是設置保存文件的時候自動編譯,但需要編譯器支持。


          {

             // ...

           "compileOnSave": false,

          }


          2. compilerOptions

          compilerOptions 屬性作用是配置編譯選項。

          若 compilerOptions 屬性被忽略,則編譯器會使用默認值,可以查看《官方完整的編譯選項列表》。

          編譯選項配置非常繁雜,有很多配置,這里只列出常用的配置。


          {

           // ...

           "compilerOptions": {

             "incremental": true, // TS編譯器在第一次編譯之后會生成一個存儲編譯信息的文件,第二次編譯會在第一次的基礎上進行增量編譯,可以提高編譯的速度

             "tsBuildInfoFile": "./buildFile", // 增量編譯文件的存儲位置

             "diagnostics": true, // 打印診斷信息

             "target": "ES5", // 目標語言的版本

             "module": "CommonJS", // 生成代碼的模板標準

             "outFile": "./app.js", // 將多個相互依賴的文件生成一個文件,可以用在AMD模塊中,即開啟時應設置"module": "AMD",

             "lib": ["DOM", "ES2015", "ScriptHost", "ES2019.Array"], // TS需要引用的庫,即聲明文件,es5 默認引用dom、es5、scripthost,如需要使用es的高級版本特性,通常都需要配置,如es8的數組新特性需要引入"ES2019.Array",

             "allowJS": true, // 允許編譯器編譯JS,JSX文件

             "checkJs": true, // 允許在JS文件中報錯,通常與allowJS一起使用

             "outDir": "./dist", // 指定輸出目錄

             "rootDir": "./", // 指定輸出文件目錄(用于輸出),用于控制輸出目錄結構

             "declaration": true, // 生成聲明文件,開啟后會自動生成聲明文件

             "declarationDir": "./file", // 指定生成聲明文件存放目錄

             "emitDeclarationOnly": true, // 只生成聲明文件,而不會生成js文件

             "sourceMap": true, // 生成目標文件的sourceMap文件

             "inlineSourceMap": true, // 生成目標文件的inline SourceMap,inline SourceMap會包含在生成的js文件中

             "declarationMap": true, // 為聲明文件生成sourceMap

             "typeRoots": [], // 聲明文件目錄,默認時node_modules/@types

             "types": [], // 加載的聲明文件包

             "removeComments":true, // 刪除注釋

             "noEmit": true, // 不輸出文件,即編譯后不會生成任何js文件

             "noEmitOnError": true, // 發送錯誤時不輸出任何文件

             "noEmitHelpers": true, // 不生成helper函數,減小體積,需要額外安裝,常配合importHelpers一起使用

             "importHelpers": true, // 通過tslib引入helper函數,文件必須是模塊

             "downlevelIteration": true, // 降級遍歷器實現,如果目標源是es3/5,那么遍歷器會有降級的實現

             "strict": true, // 開啟所有嚴格的類型檢查

             "alwaysStrict": true, // 在代碼中注入'use strict'

             "noImplicitAny": true, // 不允許隱式的any類型

             "strictNullChecks": true, // 不允許把null、undefined賦值給其他類型的變量

             "strictFunctionTypes": true, // 不允許函數參數雙向協變

             "strictPropertyInitialization": true, // 類的實例屬性必須初始化

             "strictBindCallApply": true, // 嚴格的bind/call/apply檢查

             "noImplicitThis": true, // 不允許this有隱式的any類型

             "noUnusedLocals": true, // 檢查只聲明、未使用的局部變量(只提示不報錯)

             "noUnusedParameters": true, // 檢查未使用的函數參數(只提示不報錯)

             "noFallthroughCasesInSwitch": true, // 防止switch語句貫穿(即如果沒有break語句后面不會執行)

             "noImplicitReturns": true, //每個分支都會有返回值

             "esModuleInterop": true, // 允許export=導出,由import from 導入

             "allowUmdGlobalAccess": true, // 允許在模塊中全局變量的方式訪問umd模塊

             "moduleResolution": "node", // 模塊解析策略,ts默認用node的解析策略,即相對的方式導入

             "baseUrl": "./", // 解析非相對模塊的基地址,默認是當前目錄

             "paths": { // 路徑映射,相對于baseUrl

               // 如使用jq時不想使用默認版本,而需要手動指定版本,可進行如下配置

               "jquery": ["node_modules/jquery/dist/jquery.min.js"]

             },

             "rootDirs": ["src","out"], // 將多個目錄放在一個虛擬目錄下,用于運行時,即編譯后引入文件的位置可能發生變化,這也設置可以虛擬src和out在同一個目錄下,不用再去改變路徑也不會報錯

             "listEmittedFiles": true, // 打印輸出文件

             "listFiles": true// 打印編譯的文件(包括引用的聲明文件)

           }

          }


          3. exclude

          exclude 屬性作用是指定編譯器需要排除的文件或文件夾。

          默認排除 node_modules 文件夾下文件。


          {

             // ...

           "exclude": [

             "src/lib" // 排除src目錄下的lib文件夾下的文件不會編譯

           ]

          }

          和 include 屬性一樣,支持 glob 通配符:


          * 匹配0或多個字符(不包括目錄分隔符)

          ? 匹配一個任意字符(不包括目錄分隔符)

          **/ 遞歸匹配任意子目錄


          4. extends

          extends 屬性作用是引入其他配置文件,繼承配置。

          默認包含當前目錄和子目錄下所有 TypeScript 文件。


          {

             // ...

           // 把基礎配置抽離成tsconfig.base.json文件,然后引入

             "extends": "./tsconfig.base.json"

          }


          5. files

          files 屬性作用是指定需要編譯的單個文件列表。

          默認包含當前目錄和子目錄下所有 TypeScript 文件。


          {

             // ...

           "files": [

             // 指定編譯文件是src目錄下的leo.ts文件

             "scr/leo.ts"

           ]

          }


          6. include

          include 屬性作用是指定編譯需要編譯的文件或目錄。


          {

             // ...

           "include": [

             // "scr" // 會編譯src目錄下的所有文件,包括子目錄

             // "scr/*" // 只會編譯scr一級目錄下的文件

             "scr/*/*" // 只會編譯scr二級目錄下的文件

           ]

          }


          7. references

          references 屬性作用是指定工程引用依賴。

          在項目開發中,有時候我們為了方便將前端項目和后端node項目放在同一個目錄下開發,兩個項目依賴同一個配置文件和通用文件,但我們希望前后端項目進行靈活的分別打包,那么我們可以進行如下配置:


          {

             // ...

           "references": [ // 指定依賴的工程

              {"path": "./common"}

           ]

          }


          8. typeAcquisition

          typeAcquisition 屬性作用是設置自動引入庫類型定義文件(.d.ts)相關。

          包含 3 個子屬性:


          enable  : 布爾類型,是否開啟自動引入庫類型定義文件(.d.ts),默認為 false;

          include  : 數組類型,允許自動引入的庫名,如:["jquery", "lodash"];

          exculde  : 數組類型,排除的庫名。

          {

             // ...

           "typeAcquisition": {

             "enable": false,

             "exclude": ["jquery"],

             "include": ["jest"]

           }

          }


          六、常見配置示例

          本部分內容中,我們找了幾個實際開發中比較常見的配置,當然,還有很多配置需要自己摸索喲~~



          1. 移除代碼中注釋

          tsconfig.json:


          {

           "compilerOptions": {

             "removeComments": true,

           }

          }

          編譯前:


          // 返回當前版本號

          function getVersion(version:string = "1.0.0"): string{

             return version;

          }

          console.log(getVersion("1.0.1"))

          編譯結果:


          function getVersion(version) {

             if (version === void 0) { version = "1.0.0"; }

             return version;

          }

          console.log(getVersion("1.0.1"));


          2. 開啟null、undefined檢測

          tsconfig.json:


          {

             "compilerOptions": {

                 "strictNullChecks": true

             },

          }

          修改 index.ts 文件內容:


          const leo;

          leo = new Pingan('leo','hello');


          這時候編輯器也會提示錯誤信息,執行 tsc 后,控制臺報錯:


          src/index.ts:9:11 - error TS2304: Cannot find name 'Pingan'.


          9 leo = new Pingan('leo','hello');


          Found 1 error.


          3. 配置復用

          通過 extends 屬性實現配置復用,即一個配置文件可以繼承另一個文件的配置屬性。

          比如,建立一個基礎的配置文件 configs/base.json :


          {

           "compilerOptions": {

             "noImplicitAny": true,

             "strictNullChecks": true

           }

          }

          在tsconfig.json 就可以引用這個文件的配置了:


          {

           "extends": "./configs/base",

           "files": [

             "main.ts",

             "supplemental.ts"

           ]

          }


          4. 生成枚舉的映射代碼

          在默認情況下,使用 const 修飾符后,枚舉不會生成映射代碼。

          如下,我們可以看出:使用 const 修飾符后,編譯器不會生成任何 RequestMethod 枚舉的任何映射代碼,在其他地方使用時,內聯每個成員的值,節省很大開銷。


          const enum RequestMethod {

           Get,

           Post,

           Put,

           Delete

          }


          let methods = [

           RequestMethod.Get,

           RequestMethod.Post

          ]

          編譯結果:


          "use strict";

          let methods = [

             0 /* Get */,

             1 /* Post */

          ];

          當然,我們希望生成映射代碼時,也可以設置 tsconfig.json 中的配置,設置 preserveConstEnums 編譯器選項為 true :


          {

           "compilerOptions": {

             "target": "es5",

             "preserveConstEnums": true

           }

          }


          最后編譯結果變成:


          "use strict";

          var RequestMethod;

          (function (RequestMethod) {

             RequestMethod[RequestMethod["Get"] = 0] = "Get";

             RequestMethod[RequestMethod["Post"] = 1] = "Post";

             RequestMethod[RequestMethod["Put"] = 2] = "Put";

             RequestMethod[RequestMethod["Delete"] = 3] = "Delete";

          })(RequestMethod || (RequestMethod = {}));

          let methods = [

             0 /* Get */,

             1 /* Post */

          ];


          5. 關閉 this 類型注解提示

          通過下面代碼編譯后會報錯:


          const button = document.querySelector("button");

          button?.addEventListener("click", handleClick);

          function handleClick(this) {

          console.log("Clicked!");

          this.removeEventListener("click", handleClick);

          }


          報錯內容:


          src/index.ts:10:22 - error TS7006: Parameter 'this' implicitly has an 'any' type.

          10 function handleClick(this) {

          Found 1 error.


          這是因為 this 隱式具有 any 類型,如果沒有指定類型注解,編譯器會提示“"this" 隱式具有類型 "any",因為它沒有類型注釋?!?。



          解決方法有2種:


          指定 this 類型,如本代碼中為 HTMLElement 類型:

          HTMLElement 接口表示所有的 HTML 元素。一些HTML元素直接實現了 HTMLElement 接口,其它的間接實現HTMLElement接口。

          關于 HTMLElement 可查看詳細。


          使用 --noImplicitThis 配置項:


          在 TS2.0 還增加一個新的編譯選項: --noImplicitThis,表示當 this 表達式值為 any 類型時生成一個錯誤信息。我們設置為 true 后就能正常編譯。


          {

           "compilerOptions": {

             "noImplicitThis": true

           }

          }


          七、Webpack/React 中使用示例


          1. 配置編譯 ES6 代碼,JSX 文件

          創建測試項目 webpack-demo,結構如下:


          webpack-demo/

           |- package.json

           |- tsconfig.json

           |- webpack.config.js

           |- /dist

             |- bundle.js

             |- index.html

           |- /src

             |- index.js

             |- index.ts

           |- /node_modules

          安裝 TypeScript 和 ts-loader:


          $ npm install --save-dev typescript ts-loader

          配置 tsconfig.json,支持 JSX,并將 TypeScript 編譯為 ES5:


          {

           "compilerOptions": {

             "outDir": "./dist/",

             "noImplicitAny": true,

          +   "module": "es6",

          +   "target": "es5",

          +   "jsx": "react",

             "allowJs": true

           }

          }

          還需要配置 webpack.config.js,使其能夠處理 TypeScript 代碼,這里主要在 rules 中添加 ts-loader :


          const path = require('path');


          module.exports = {

           entry: './src/index.ts',

           module: {

             rules: [

               {

                 test: /\.tsx?$/,

                 use: 'ts-loader',

                 exclude: /node_modules/

               }

             ]

           },

           resolve: {

             extensions: [ '.tsx', '.ts', '.js' ]

           },

           output: {

             filename: 'bundle.js',

             path: path.resolve(__dirname, 'dist')

           }

          };


          2. 配置 source map

          想要啟用 source map,我們必須配置 TypeScript,以將內聯的 source map 輸出到編譯后的 JavaScript 文件中。

          只需要在 tsconfig.json 中配置 sourceMap 屬性:


           {

             "compilerOptions": {

               "outDir": "./dist/",

          +     "sourceMap": true,

               "noImplicitAny": true,

               "module": "commonjs",

               "target": "es5",

               "jsx": "react",

               "allowJs": true

             }

           }

          然后配置 webpack.config.js 文件,讓 webpack 提取 source map,并內聯到最終的 bundle 中:


           const path = require('path');


           module.exports = {

             entry: './src/index.ts',

          +   devtool: 'inline-source-map',

             module: {

               rules: [

                 {

                   test: /\.tsx?$/,

                   use: 'ts-loader',

                   exclude: /node_modules/

                 }

               ]

             },

             resolve: {

               extensions: [ '.tsx', '.ts', '.js' ]

             },

             output: {

               filename: 'bundle.js',

               path: path.resolve(__dirname, 'dist')

             }

           };


          八、總結

          本文較全面介紹了 tsconfig.json 文件的知識,從“什么是 tsconfig.js 文件”開始,一步步帶領大家全面認識 tsconfig.json 文件。

          文中通過一個簡單 learnTsconfig 項目,讓大家知道項目中如何使用 tsconfig.json 文件。在后續文章中,我們將這么多的配置項進行分類學習。最后通過幾個常見配置示例,解決我們開發中遇到的幾個常見問題。

          vue.js路由與vuex數據模型設計

          seo達人

          路由設計

          本則路由考慮驗證進入登錄頁面,完成登錄操作進入首頁。


          import Vue from "vue";

          import Router from "vue-router";

          Vue.use(Router);


          import store from "@/store/store";


          // (延遲加載)

          const Login = () => import("@/views/login");

          const Home = () => import("@/views/home");


          const HomeRoute = {

           path: "/",

           name: "首頁",

           component: Home

          };


          export { HomeRoute };


          const router = new Router({

           base: process.env.BASE_URL,

           routes: [

             {

               path: "/login",

               name: "登錄",

               component: Login

             },

             HomeRoute

           ]

          });


          router.beforeEach((to, from, next) => {

           let loginName = store.state.user.loginName;

           if (to.path === "/" && loginName == "") {

             next("/login");

           } else {

             next();

           }

          });


          export default router;

          數據模型

          const state = {

           loginName: ""

          };

          const mutations = {

           SET_LOGINNAME(state, loginName) {

             state.loginName = loginName;

           }

          };

          const actions = {

           login({ commit }, userInfo) {

             return new Promise((res, ret) => {

               commit("SET_LOGINNAME", userInfo);

               res();

             });

           },

           logout({ commit }) {

             return new Promise((res, ret) => {

               commit("SET_LOGINNAME", "");

               res();

             });

           }

          };

          export default {

           namespaced: true,

           state,

           mutations,

           actions

          };

          import Vue from "vue";

          import Vuex from "vuex";

          Vue.use(Vuex);


          import user from "./modules/user";


          const store = new Vuex.Store({

           modules: {

             user

           }

          });


          export default store;

          組件

          <div class="modify">

           <input

             type="text"

             @keydown.enter.prevent="handleKeydown"

             v-model="currentVal"

             placeholder="使用enter鍵切換頻道"

           />

           <button @click="reset" style="margin-left:5px;outline:none;cursor:pointer;">復位</button>

          </div>

          import { mapState, mapMutations, mapActions } from "vuex";

          export default {

           name: "login",

           data() {

             return {

               currentVal: "",

               list: ["咨詢服務", "音悅臺", "體育臺", "財經頻道", "時尚資訊"],

               index: 0

             };

           },

           computed: {

             ...mapState({

               loginName: state => state.user.loginName

             })

           },

           methods: {

             ...mapActions({

               login: "user/login"

             }),

             handleToHome() {

               let userInfo = "user";

               this.login(userInfo);

               this.$router.push({

                 path: "/"

               });

             },

          RN和React路由詳解及對比

          seo達人

          前言

          在平時H5或者RN開發時,我們業務場景中大部分都不是單頁面的需求,那這時我們就能使用路由在進行多頁面的切換。下面會對比一下react路由和RN路由的本質區別和使用方法。


          路由(routing)是指分組從源到目的地時,決定端到端路徑的網絡范圍的進程

          React路由

          簡介

          使用React構建的單頁面應用,要想實現頁面間的跳轉,首先想到的就是使用路由。在React中,常用的有兩個包可以實現這個需求,那就是react-router和react-router-dom。本文主要針對react-router-dom進行說明


          在根組件上配置路由,引用react-router-dom結構{ HashRouter as Router, Route ,Link ,Redirect ,Switch },HashRouter組件是路由的根組件,定義屬性和方法傳遞給子組件。Router組件進行路由,指定每個路由跳轉到相應的組件。Link組件指定跳轉鏈接。Redirect組件路由重定向,不管什么情況下,都會跳轉當前指定的路徑,和switch組件聯合起來一起調用,當路徑匹配到路由,不在往下匹配


          兩類路由

          HashRouter:利用監聽hash變化(有一個事件hashchange)實現路由切換,它是路由容器,

          渲染子組件,并向下層子組件傳遞(Context上下文傳遞)loaction,history等路由信息


          BrowserHistory:利用H5Api實現路由切換,是路由容器,渲染子組件,

          并向子組件傳遞loaction,history等路由信息

          路由配置

          image-20200601110809995


          路由實現原理

          HashRouter只是一個容器,本身并沒有DOM結構

          它渲染的就是它的子組件,并向下層傳遞location

          組件掛載完成之后根據hash改變pathname的值,如果沒有hash值就默認展示根組件

          需要跳轉路由頁面時我們使用link或者push去賦值hash的pathname 如this.props.history.push({ pathname: preview, param: { pic, index } });

          當hash值發生變化的時候會通過hashchange捕獲變化,并給pathname重新賦值

          拿到上下文中傳過來的location,然后取出pathname。再對它的子組件進行遍歷,如果子組件的path屬性和當前上下文中傳過來的pathname屬性相匹配就進行渲染,若不匹配就返回null。

          總結

          React路由是實質就是,根據遍歷識別路由的pathname,來切換router路由容器中component組件的加載渲染。每次更改pathname就都是組件的重新渲染流程,頁面也都會呈現出刷新的效果。


          RN路由

          簡介

          RN把導航和路由都集中到了react-navigation庫里面

          組件使用堆棧式的頁面導航來實現各個頁面跳轉

          構造函數:StackNavigator(RouteConfigs, StackNavigatorConfig)

          RouteConfigs:頁面路由配置

          StackNavigatorConfig:路由參數配置

          路由配置

          image-20200601111333107


          參數詳解

          navigationOptions:配置StackNavigator的一些屬性。


             title:標題,如果設置了這個導航欄和標簽欄的title就會變成一樣的,不推薦使用

             header:可以設置一些導航的屬性,如果隱藏頂部導航欄只要將這個屬性設置為null

             headerTitle:設置導航欄標題,推薦

             headerBackTitle:設置跳轉頁面左側返回箭頭后面的文字,默認是上一個頁面的標題??梢宰远x,也可以設置為null

             headerTruncatedBackTitle:設置當上個頁面標題不符合返回箭頭后的文字時,默認改成"返回"

             headerRight:設置導航條右側??梢允前粹o或者其他視圖控件

             headerLeft:設置導航條左側??梢允前粹o或者其他視圖控件

             headerStyle:設置導航條的樣式。背景色,寬高等

             headerTitleStyle:設置導航欄文字樣式

             headerBackTitleStyle:設置導航欄‘返回’文字樣式

             headerTintColor:設置導航欄顏色

             headerPressColorAndroid:安卓獨有的設置顏色紋理,需要安卓版本大于5.0

             gesturesEnabled:是否支持滑動返回手勢,iOS默認支持,安卓默認關閉



          screen:對應界面名稱,需要填入import之后的頁面


          mode:定義跳轉風格


            card:使用iOS和安卓默認的風格


            modal:iOS獨有的使屏幕從底部畫出。類似iOS的present效果


          headerMode:返回上級頁面時動畫效果


            float:iOS默認的效果


            screen:滑動過程中,整個頁面都會返回


            none:無動畫


          cardStyle:自定義設置跳轉效果


            transitionConfig: 自定義設置滑動返回的配置


            onTransitionStart:當轉換動畫即將開始時被調用的功能


            onTransitionEnd:當轉換動畫完成,將被調用的功能


          path:路由中設置的路徑的覆蓋映射配置


          initialRouteName:設置默認的頁面組件,必須是上面已注冊的頁面組件


          initialRouteParams:初始路由參數

          路由首頁

          react:


          image-20200601111638902


          在react中初始化時沒有指定hash值,route會匹配路由表里面的根組件”/”


          RN:


          image-20200601111722749


          RN 需要在StackNavigatorConfig里面指定首頁


          RN路由使用

          image-20200601112012191


          在入口路由列表注冊完成之后 在導航器中的每一個頁面,都有 navigation 屬性 通過提供的navigate方法來提供跳轉


          navigation

          在導航器中每一個頁面都有navigation屬性,該屬性有以下幾個屬性/方法

          navigate 跳轉到其他頁面 常用參數如下

          routeName 導航器中配置的路由名稱

          params 傳遞到下一個頁面的參數

          state:state 里面包含有傳遞過來的參數 params 、 key 、路由名稱 routeName

          setParams 更改當前頁面路由參數(后面詳細介紹)

          goBack: 回退可穿參數

          navigate



          setParams




          在Vue中創建可重用的 Transition

          seo達人

          原始transition組件和CSS

          定義transition的最簡單方法是使用transition·或transition-group 組件。這需要為transition定義一個name`和一些CSS。


          <template>

           <div id="app">

             <button v-on:click="show = !show">

               Toggle

             </button>

             <transition name="fade">

               <p v-if="show">hello</p>

             </transition>

           </div>

          </template>

          <script>

          export default {

           name: "App",

           data() {

             return {

               show: true

             };

           }

          };

          </script>

          <style>

          .fade-enter-active,

          .fade-leave-active {

           transition: opacity 0.3s;

          }

          .fade-enter,

          .fade-leave-to {

           opacity: 0;

          }

          </style>

          圖片描述


          看起來容易,對吧?然而,這種方法有一個問題。我們不能在另一個項目中真正重用這個transition。


          封裝transition組件

          如果我們將前面的邏輯封裝到一個組件中,并將其用作一個組件,結果會怎樣呢?


          // FadeTransition.vue

          <template>

           <transition name="fade">

             <slot></slot>

           </transition>

          </template>

          <script>

          export default {

           

          };

          </script>

          <style>

          .fade-enter-active,

          .fade-leave-active {

           transition: opacity 0.3s;

          }

          .fade-enter,

          .fade-leave-to {

           opacity: 0;

          }

          </style>


          // App.vue


          <template>

           <div id="app">

             <button v-on:click="show = !show">

               Toggle transition

             </button>

             <fade-transition>

               <div v-if="show" class="box"></div>

             </fade-transition>

           </div>

          </template>

          <script>...</script>

          <style>...</style>

          圖片描述


          通過在transition組件中提供一個slot,我們幾乎可以像使用基本transition組件一樣使用它。這比前面的例子稍微好一點,但是如果我們想要傳遞其他特定于transition的prop,比如mode或者一些hook,該怎么辦呢


          封裝的包裝器transition組件

          幸運的是,Vue 中有一個功能,使我們可以將用戶指定的所有額外props和監聽器傳遞給我們的內部標簽/組件。 如果你還不知道,則可以通過$attrs訪問額外傳遞的 props,并將它們與v-bind結合使用以將它們綁定為props。 這同樣適用于通過$listeners進行的事件,并通過v-on對其進行應用。


          // FadeTransition.vue


          <template>

           <transition name="fade" v-bind="$attrs" v-on="$listeners">

             <slot></slot>

           </transition>

          </template>

          <script>

          export default {};

          </script>

          <style>

          .fade-enter-active,

          .fade-leave-active {

           transition: opacity 0.3s;

          }

          .fade-enter,

          .fade-leave-to {

           opacity: 0;

          }

          </style>


          // App.vue


          ...


          <fade-transition mode="out-in">

           <div key="blue" v-if="show" class="box"></div>

           <div key="red" v-else class="red-box"></div>

          </fade-transition>


          ...

          圖片描述


          完整事例地址:https://codesandbox.io/s/yjl1...


          現在,我們可以傳遞普通transition組件可以接受的任何事件和支持,這使得我們的組件更加可重用。但為什么不更進一步,增加通過 prop 輕松定制持續時間的可能性。


          顯式持續時間 prop

          Vue 為transition組件提供了一個duration prop,然而,它是為更復雜的動畫鏈接而設計的,它幫助 Vue 正確地將它們鏈接在一起。


          在我們的案例中,我們真正需要的是通過組件prop控制CSS animation/transition。 我們可以通過不在CSS中指定顯式的CSS動畫持續時間,而是將其作為樣式來實現。 我們可以借助transition hook來做到這一點,該transition hook與組件生命周期 hook 非常相似,但是它們在過渡所需元素之前和之后被調用。 讓我們看看效果如何。


          // FadeTransition.vue


          <template>

           <transition name="fade"

                       enter-active-class="fadeIn"

                       leave-active-class="fadeOut"

                       v-bind="$attrs"

                       v-on="hooks">

               <slot></slot>

           </transition>

          </template>

          <script>

          export default {

           props: {

             duration: {

               type: Number,

               default: 300

             }

           },

           computed: {

             hooks() {

               return {

                 beforeEnter: this.setDuration,

                 afterEnter: this.cleanUpDuration,

                 beforeLeave: this.setDuration,

                 afterLeave: this.cleanUpDuration,

                 ...this.$listeners

               };

             }

           },

           methods: {

             setDuration(el) {

               el.style.animationDuration = `${this.duration}ms`;

             },

             cleanUpDuration(el) {

               el.style.animationDuration = "";

             }

           }

          };

          </script>

          <style>

          @keyframes fadeIn {

           from {

             opacity: 0;

           }

           to {

             opacity: 1;

           }

          }

          .fadeIn {

           animation-name: fadeIn;

          }

          @keyframes fadeOut {

           from {

             opacity: 1;

           }

           to {

             opacity: 0;

           }

          }

          .fadeOut {

           animation-name: fadeOut;

          }

          </style>

          圖片描述


          完整事例地址:https://codesandbox.io/s/j4qn...


          現在,我們可以控制實際的可見過渡時間,這使我們可重用的過渡變得靈活且易于使用。 但是,如何過渡多個元素(如列表項)呢?


          Transition group 支持

          你想到的最直接的方法可能是創建一個新組件,比如fade-transition-group,然后將當前transition標簽替換為transition-group標簽,以實現 group transition。如果我們可以在相同的組件中這樣做,并公開一個將切換到transition-group實現的group prop,那會怎么樣呢?幸運的是,我們可以通過render函數或component和is屬性來實現這一點。


          // FadeTransition.vue


          <template>

           <component :is="type"

                      :tag="tag"

                      enter-active-class="fadeIn"

                      leave-active-class="fadeOut"

                      move-class="fade-move"

                      v-bind="$attrs"

                      v-on="hooks">

               <slot></slot>

           </component>

          </template>

          <script>

          export default {

           props: {

             duration: {

               type: Number,

               default: 300

             },

             group: {

               type: Boolean,

               default: false

             },

             tag: {

               type: String,

               default: "div"

             }

           },

           computed: {

             type() {

               return this.group ? "transition-group" : "transition";

             },

             hooks() {

               return {

                 beforeEnter: this.setDuration,

                 afterEnter: this.cleanUpDuration,

                 beforeLeave: this.setDuration,

                 afterLeave: this.cleanUpDuration,

                 leave: this.setAbsolutePosition,

                 ...this.$listeners

               };

             }

           },

           methods: {

             setDuration(el) {

               el.style.animationDuration = `${this.duration}ms`;

             },

             cleanUpDuration(el) {

               el.style.animationDuration = "";

             },

             setAbsolutePosition(el) {

               if (this.group) {

                 el.style.position = "absolute";

               }

             }

           }

          };

          </script>

          <style>

          @keyframes fadeIn {

           from {

             opacity: 0;

           }

           to {

             opacity: 1;

           }

          }

          .fadeIn {

           animation-name: fadeIn;

          }

          @keyframes fadeOut {

           from {

             opacity: 1;

           }

           to {

             opacity: 0;

           }

          }

          .fadeOut {

           animation-name: fadeOut;

          }

          .fade-move {

           transition: transform 0.3s ease-out;

          }

          </style>


          // App.vue


          ...


          <div class="box-wrapper">

           <fade-transition group :duration="300">

             <div class="box"

                  v-for="(item, index) in list"

                  @click="remove(index)"

                  :key="item"

              >

             </div>

           </fade-transition>

          </div>


          ...

          圖片描述


          完整事例地址:https://codesandbox.io/s/pk9r...


          文檔中介紹了一個帶有transition-group元素的警告。 我們基本上必須在元素離開時將每個項目的定位設置為absolute,以實現其他項目的平滑移動動畫。 我們也必須添加一個move-class并手動指定過渡持續時間,因為沒有用于移動的 JS hook。我們將這些調整添加到我們的上一個示例中。


          再做一些調整,通過在mixin中提取 JS 邏輯,我們可以將其應用于輕松創建新的transition組件,只需將其放入下一個項目中即可。


          Vue Transition

          在此之前描述的所有內容基本上都是這個小型 transition 集合所包含的內容。它有 10 個封裝的transition組件,每個約1kb(縮小)。我認為它非常方便,可以輕松地在不同的項目中使用。你可以試一試:)


          總結

          我們從一個基本的過渡示例開始,并最終通過可調整的持續時間和transition-group支持來創建可重用的過渡組件。 我們可以使用這些技巧根據并根據自身的需求創建自己的過渡組件。 希望讀者從本文中學到了一些知識,并且可以幫助你們建立功能更好的過渡組件。

          web安全之XSS實例解析

          seo達人

          XSS

          跨站腳本攻擊(Cross Site Script),本來縮寫是 CSS, 但是為了和層疊樣式表(Cascading Style Sheet, CSS)有所區分,所以安全領域叫做 “XSS”;


          XSS攻擊,通常是指攻擊者通過 “HTML注入”篡改了網頁,插入了惡意的腳本,從而在用戶瀏覽網頁時,對用戶的瀏覽器進行控制或者獲取用戶的敏感信息(Cookie, SessionID等)的一種攻擊方式。


          頁面被注入了惡意JavaScript腳本,瀏覽器無法判斷區分這些腳本是被惡意注入的,還是正常的頁面內容,所以惡意注入Javascript腳本也擁有了所有的腳本權限。如果頁面被注入了惡意 JavaScript腳本,它可以做哪些事情呢?


          可以竊取 cookie信息。惡意 JavaScript可以通過 ”doccument.cookie“獲取cookie信息,然后通過 XMLHttpRequest或者Fetch加上CORS功能將數據發送給惡意服務器;惡意服務器拿到用戶的cookie信息之后,就可以在其他電腦上模擬用戶的登陸,然后進行轉賬操作。

          可以監聽用戶行為。惡意JavaScript可以使用 "addEventListener"接口來監聽鍵盤事件,比如可以獲取用戶輸入的銀行卡等信息,又可以做很多違法的事情。

          可以修改DOM 偽造假的登陸窗口,用來欺騙用戶輸入用戶名和密碼等信息。

          還可以在頁面內生成浮窗廣告,這些廣告會嚴重影響用戶體驗。

          XSS攻擊可以分為三類:反射型,存儲型,基于DOM型(DOM based XSS)


          反射型

          惡意腳本作為網絡請求的一部分。


          const Koa = require("koa");

          const app = new Koa();


          app.use(async ctx => {

             // ctx.body 即服務端響應的數據

             ctx.body = '<script>alert("反射型 XSS 攻擊")</script>';

          })


          app.listen(3000, () => {

             console.log('啟動成功');

          });

          訪問 http://127.0.0.1:3000/ 可以看到 alert執行


          反射型XSS1


          舉一個常見的場景,我們通過頁面的url的一個參數來控制頁面的展示內容,比如我們把上面的一部分代碼改成下面這樣


          app.use(async ctx => {

             // ctx.body 即服務端響應的數據

             ctx.body = ctx.query.userName;

          })

          此時訪問 http://127.0.0.1:3000?userName=xiaoming 可以看到頁面上展示了xiaoming,此時我們訪問 http://127.0.0.1:3000?userName=<script>alert("反射型 XSS 攻擊")</script>, 可以看到頁面彈出 alert。


          反射型XSS2


          通過這個操作,我們會發現用戶將一段含有惡意代碼的請求提交給服務器,服務器在接收到請求時,又將惡意代碼反射給瀏覽器端,這就是反射型XSS攻擊。另外一點需要注意的是,Web 服務器不會存儲反射型 XSS 攻擊的惡意腳本,這是和存儲型 XSS 攻擊不同的地方。


          在實際的開發過程中,我們會碰到這樣的場景,在頁面A中點擊某個操作,這個按鈕操作是需要登錄權限的,所以需要跳轉到登錄頁面,登錄完成之后再跳轉會A頁面,我們是這么處理的,跳轉登錄頁面的時候,會加一個參數 returnUrl,表示登錄完成之后需要跳轉到哪個頁面,即這個地址是這樣的 http://xxx.com/login?returnUrl=http://xxx.com/A,假如這個時候把returnUrl改成一個script腳本,而你在登錄完成之后,如果沒有對returnUrl進行合法性判斷,而直接通過window.location.href=returnUrl,這個時候這個惡意腳本就會執行。


          存儲型

          存儲型會把用戶輸入的數據“存儲”在服務器。


          比較常見的一個場景就是,攻擊者在社區或論壇寫下一篇包含惡意 JavaScript代碼的博客文章或評論,文章或評論發表后,所有訪問該博客文章或評論的用戶,都會在他們的瀏覽器中執行這段惡意的JavaScript代碼。


          存儲型攻擊大致需要經歷以下幾個步驟


          首先攻擊者利用站點漏洞將一段惡意JavaScript代碼提交到網站數據庫中

          然后用戶向網站請求包含了惡意 JavaScript腳本的頁面

          當用戶瀏覽該頁面的時候,惡意腳本就會將用戶的cookie信息等數據上傳到服務器

          存儲型XSS


          舉一個簡單的例子,一個登陸頁面,點擊登陸的時候,把數據存儲在后端,登陸完成之后跳轉到首頁,首頁請求一個接口將當前的用戶名顯示到頁面


          客戶端代碼


          <!DOCTYPE html>

          <html lang="en">


          <head>

             <meta charset="UTF-8">

             <meta name="viewport" content="width=device-width, initial-scale=1.0">

             <meta http-equiv="X-UA-Compatible" content="ie=edge">

             <title>XSS-demo</title>

             <style>

                 .login-wrap {

                     height: 180px;

                     width: 300px;

                     border: 1px solid #ccc;

                     padding: 20px;

                     margin-bottom: 20px;

                 }

                 input {

                     width: 300px;

                 }

             </style>

          </head>


          <body>

             <div class="login-wrap">

                 <input type="text" placeholder="用戶名" class="userName">

                 <br>

                 <input type="password" placeholder="密碼" class="password">

                 <br>

                 <br>

                 <button class="btn">登陸</button>

             </div>

          </body>

          <script>

             var btn = document.querySelector('.btn');

             

             btn.onclick = function () {

                 var userName = document.querySelector('.userName').value;

                 var password = document.querySelector('.password').value;

                 

                 fetch('http://localhost:3200/login', {

                     method: 'POST',

                     body: JSON.stringify({

                         userName,

                         password

                     }),

                     headers:{

                         'Content-Type': 'application/json'

                     },

                     mode: 'cors'

                 })

                     .then(function (response) {

                         return response.json();

                     })

                     .then(function (res) {

                         alert(res.msg);

                         window.location.href= "http://localhost:3200/home";

                     })

                     .catch(err => {

                         message.error(`本地測試錯誤 ${err.message}`);

                         console.error('本地測試錯誤', err);

                     });

             }

          </script>


          </html>

          服務端代碼


          const Koa = require("koa");

          const app = new Koa();

          const route = require('koa-route');

          var bodyParser = require('koa-bodyparser');

          const cors = require('@koa/cors');


          // 臨時用一個變量來存儲,實際應該存在數據庫中

          let currentUserName = '';


          app.use(bodyParser()); // 處理post請求的參數


          const login = ctx => {

             const req = ctx.request.body;

             const userName = req.userName;

             currentUserName = userName;


             ctx.response.body = {

                 msg: '登陸成功'

             };

          }


          const home = ctx => {

             ctx.body = currentUserName;

          }

          app.use(cors());

          app.use(route.post('/login', login));

          app.use(route.get('/home', home));

          app.listen(3200, () => {

             console.log('啟動成功');

          });

          點擊登陸將輸入信息提交大服務端,服務端使用變量 currentUserName來存儲當前的輸入內容,登陸成功后,跳轉到 首頁, 服務端會返回當前的用戶名。如果用戶輸入了惡意腳本內容,則惡意腳本就會在瀏覽器端執行。


          在用戶名的輸入框輸入 <script>alert('存儲型 XSS 攻擊')</script>,執行結果如下


          存儲型XSS


          基于DOM(DOM based XSS)

          通過惡意腳本修改頁面的DOM節點,是發生在前端的攻擊


          基于DOM攻擊大致需要經歷以下幾個步驟


          攻擊者構造出特殊的URL,其中包含惡意代碼

          用戶打開帶有惡意代碼的URL

          用戶瀏覽器接受到響應后執行解析,前端JavaScript取出URL中的惡意代碼并執行

          惡意代碼竊取用戶數據并發送到攻擊者的網站,冒充用戶行為,調用目標網站接口執行攻擊者指定的操作。

          舉個例子:


          <body>

             <div class="login-wrap">

                 <input type="text" placeholder="輸入url" class="url">

                 <br>

                 <br>

                 <button class="btn">提交</button>

                 <div class="content"></div>

             </div>

          </body>

          <script>

             var btn = document.querySelector('.btn');

             var content = document.querySelector('.content');

             

             btn.onclick = function () {

                 var url = document.querySelector('.url').value;

                 content.innerHTML = `<a href=${url}>跳轉到輸入的url</a>`

             }

          </script>

          點擊提交按鈕,會在當前頁面插入一個超鏈接,其地址為文本框的內容。


          在輸入框輸入 如下內容


          '' onclick=alert('哈哈,你被攻擊了')

          執行結果如下


          基于DOM型XSS


          首先用兩個單引號閉合調 href屬性,然后插入一個onclick事件。點擊這個新生成的鏈接,腳本將被執行。


          上面的代碼是通過執行 執行 alert來演示的攻擊類型,同樣你可以把上面的腳本代碼修改為任何你想執行的代碼,比如獲取 用戶的 cookie等信息,<script>alert(document.cookie)</script>,同樣也是可以的.

          防御XSS

          HttpOnly

          由于很多XSS攻擊都是來盜用Cookie的,因此可以通過 使用HttpOnly屬性來防止直接通過 document.cookie 來獲取 cookie。


          一個Cookie的使用過程如下


          瀏覽器向服務器發起請求,這時候沒有 Cookie

          服務器返回時設置 Set-Cookie 頭,向客戶端瀏覽器寫入Cookie

          在該 Cookie 到期前,瀏覽器訪問該域下的所有頁面,都將發送該Cookie

          HttpOnly是在 Set-Cookie時標記的:


          通常服務器可以將某些 Cookie 設置為 HttpOnly 標志,HttpOnly 是服務器通過 HTTP 響應頭來設置的。


          const login = ctx => {

             // 簡單設置一個cookie

             ctx.cookies.set(

                 'cid',

                 'hello world',

                 {

                   domain: 'localhost',  // 寫cookie所在的域名

                   path: '/home',       // 寫cookie所在的路徑

                   maxAge: 10 * 60 * 1000, // cookie有效時長

                   expires: new Date('2021-02-15'),  // cookie失效時間

                   httpOnly: true,  // 是否只用于http請求中獲取

                   overwrite: false  // 是否允許重寫

                 }

               )

          }

          HttpOnly


          需要注意的一點是:HttpOnly 并非阻止 XSS 攻擊,而是能阻止 XSS 攻擊后的 Cookie 劫持攻擊。


          輸入和輸出的檢查

          永遠不要相信用戶的輸入。


          輸入檢查一般是檢查用戶輸入的數據是都包含一些特殊字符,如 <、>, '及"等。如果發現特殊字符,則將這些字符過濾或編碼。這種可以稱為 “XSS Filter”。


          安全的編碼函數


          針對HTML代碼的編碼方式是 HtmlEncode(是一種函數實現,將字符串轉成 HTMLEntrities)


          & --> &amp;

          < --> &lt;

          > --> &gt;

          " --> &quot;

          相應的, JavaScript的編碼方式可以使用 JavascriptEncode。


          假如說用戶輸入了 <script>alert("你被攻擊了")</script>,我們要對用戶輸入的內容進行過濾(如果包含了 <script> 等敏感字符,就過濾掉)或者對其編碼,如果是惡意的腳本,則會變成下面這樣


          &lt;script&gt;alert("你被攻擊了");&lt;/script&gt;

          經過轉碼之后的內容,如 <script>標簽被轉換為 &lt;script&gt;,即使這段腳本返回給頁面,頁面也不會指向這段代碼。


          防御 DOM Based XSS

          我們可以回看一下上面的例子


          btn.onclick = function () {

             var url = document.querySelector('.url').value;

             content.innerHTML = `<a href=${url}>跳轉到輸入的url</a>`

          }

          事實上,DOM Based XSS 是從 JavaScript中輸出數據到HTML頁面里。


          用戶輸入 '' onclick=alert('哈哈,你被攻擊了'),然后通過 innerHTML 修改DOM的內容,就變成了 <a href='' onclick=alert('哈哈,你被攻擊了')>跳轉到輸入的url</a>, XSS因此產生。


          那么正確的防御方法是什么呢?

          從JavaScript輸出到HTML頁面,相當于一次 XSS輸出的過程,需要根據不同場景進行不同的編碼處理


          變量輸出到 <script>,執行一次 JavascriptEncode

          通過JS輸出到HTML頁面


          輸出事件或者腳本,做 JavascriptEncode 處理

          輸出 HTML內容或者屬性,做 HtmlEncode 處理

          會觸發 DOM Based XSS的地方有很多,比如


          xxx.interHTML

          xxx.outerHTML

          document.write

          頁面中所有的inputs框

          XMLHttpRequest返回的數據

          ...


          項目中如果用到,一定要避免在字符串中拼接不可信的數據。


          利用CSP

          CSP (Content Security Policy) 即內容安全策略,是一種可信白名單機制,可以在服務端配置瀏覽器哪些外部資源可以加載和執行。我們只需要配置規則,如何攔截是由瀏覽器自己實現的。我們可以通過這種方式來盡量減少 XSS 攻擊。


          通常可以通過兩種方式來開啟 CSP:


          設置 HTTP Header 的 Content-Security-Policy

          Content-Security-Policy: default-src 'self'; // 只允許加載本站資源

          Content-Security-Policy: img-src https://*  // 只允許加載 HTTPS 協議圖片

          Content-Security-Policy: child-src 'none'    // 允許加載任何來源框架

          設置 meta 標簽的方式

          <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://*; child-src 'none';">

          這些 CSS 偽類,你可能還不知道,可以用起來了!

          seo達人

          css 偽類是用于向某些選擇器添加特殊的效果,是動態的,指當前元素所處的狀態或者特性。只有一個元素達到一個特定狀態時,它可能得到一個偽類的樣式;當狀態改變時,它又會失去這個樣式。


          這篇文章在一定程度上鼓勵你在構建UI時使用更簡單的CSS和更少的 JS。熟悉 CSS 所提供的一切是實現這一目標的一種方法,另一種方法是實現最佳實踐并盡可能多地重用代碼。


          接下介紹一些大家可能還不熟悉的一些偽類及其用例,希望對大家日后有所幫助。


          ::first-line | 選擇文本的第一行

          ::first-line 偽元素在某塊級元素的第一行應用樣式。第一行的長度取決于很多因素,包括元素寬度,文檔寬度和文本的文字大小。


          ::first-line 偽元素只能在塊容器中,所以,::first-line偽元素只能在一個display值為block, inline-block, table-cell 或者 table-caption中有用。在其他的類型中,::first-line 是不起作用的。


          用法如下:


          p:first-line {

           color: lightcoral;

          }

          ::first-letter | 選擇這一行的第一字

          CSS 偽元素 ::first-letter會選中某塊級元素第一行的第一個字母。用法如下:


          <style>

             p::first-letter{

               color: red;

               font-size: 2em;

             }

          </style>


          <p>前端小智,不斷努,終身學習者!</p>

          clipboard.png


          ::selection| 被用戶高亮的部分

          ::selection 偽元素應用于文檔中被用戶高亮的部分(比如使用鼠標或其他選擇設備選中的部分)。


          div::selection {

               color: #409EFF;

          }

          clipboard.png


          :root | 根元素

          :root 偽類匹配文檔樹的根元素。對于 HTML 來說,:root 表示 <html> 元素,除了優先級更高之外,與 html 選擇器相同。


          在聲明全局 CSS 變量時 :root 會很有用:


          :root {

           --main-color: hotpink;

           --pane-padding: 5px 42px;

          }

          :empty | 僅當子項為空時才有作用

          :empty 偽類代表沒有子元素的元素。子元素只可以是元素節點或文本(包括空格),注釋或處理指令都不會產生影響。


          div:empty {

           border: 2px solid orange;

           margin-bottom: 10px;

          }


          <div></div>

          <div></div>

          <div>

          </div>

          clipboard.png


          只有第一個和第二個div有作用,因為它們確實是空的,第三個 div 沒有作用,因為它有一個換行。


          :only-child | 只有一個子元素才有作用

          :only-child 匹配沒有任何兄弟元素的元素.等效的選擇器還可以寫成 :first-child:last-child或者:nth-child(1):nth-last-child(1),當然,前者的權重會低一點。


          p:only-child{

           background: #409EFF;

          }


          <div>

           <p>第一個沒有任何兄弟元素的元素</p>

          </div>

          <div>

           <p>第二個</p>

           <p>第二個</p>

          </div>

          clipboard.png


          :first-of-type | 選擇指定類型的第一個子元素

          :first-of-type表示一組兄弟元素中其類型的第一個元素。


          .innerDiv p:first-of-type {

           color: orangered;

          }

          上面表示將 .innerDiv 內的第一個元素為 p 的顏色設置為橘色。


          <div class="innerDiv">

             <div>Div1</div>

             <p>These are the necessary steps</p>

             <p>hiya</p>

             

             <p>

                 Do <em>not</em> push the brake at the same time as the accelerator.

             </p>

             <div>Div2</div>

          </div>

          clipboard.png


          :last-of-type | 選擇指定類型的最后一個子元素

          :last-of-type CSS 偽類 表示了在(它父元素的)子元素列表中,最后一個給定類型的元素。當代碼類似Parent tagName:last-of-type的作用區域包含父元素的所有子元素中的最后一個選定元素,也包括子元素的最后一個子元素并以此類推。


          .innerDiv p:last-of-type {

             color: orangered;

          }

          上面表示將 .innerDiv 內的的最后一個元素為 p 的顏色設置為橘色。


          clipboard.png


          nth-of-type() | 選擇指定類型的子元素

          :nth-of-type() 這個 CSS 偽類是針對具有一組兄弟節點的標簽, 用 n 來篩選出在一組兄弟節點的位置。


          .innerDiv p:nth-of-type(1) {

             color: orangered;

          }


          <div class="innerDiv">

           <div>Div1</div>

           <p>These are the necessary steps</p>

           <p>hiya</p>

           

           <p>

               Do <em>not</em> push the brake at the same time as the accelerator.

           </p>

           <div>Div2</div>

          </div>

          clipboard.png


          :nth-last-of-type() | 在列表末尾選擇類型的子元素

          :nth-last-of-type(an+b) 這個 CSS 偽類 匹配那些在它之后有 an+b-1 個相同類型兄弟節點的元素,其中 n 為正值或零值。它基本上和 :nth-of-type 一樣,只是它從結尾處反序計數,而不是從開頭處。


          .innerDiv p:nth-last-of-type(1) {

             color: orangered;

          }

          這會選擇innerDiv元素中包含的類型為p元素的列表中的最后一個子元素。


          <div class="innerDiv">

             <p>These are the necessary steps</p>

             <p>hiya</p>

             <div>Div1</div>

             <p>

                 Do the same.

             </p>

             <div>Div2</div>

          </div>

          clipboard.png


          :link | 選擇一個未訪問的超鏈接

          :link偽類選擇器是用來選中元素當中的鏈接。它將會選中所有尚未訪問的鏈接,包括那些已經給定了其他偽類選擇器的鏈接(例如:hover選擇器,:active選擇器,:visited選擇器)。


          為了可以正確地渲染鏈接元素的樣式,:link偽類選擇器應當放在其他偽類選擇器的前面,并且遵循LVHA的先后順序,即::link — :visited — :hover — :active。:focus偽類選擇器常伴隨在:hover偽類選擇器左右,需要根據你想要實現的效果確定它們的順序。


          a:link {

             color: orangered;

          }

          <a href="/login">Login<a>

          clipboard.png


          :checked | 選擇一個選中的復選框

          :checked CSS 偽類選擇器表示任何處于選中狀態的radio(<input type="radio">), checkbox (<input type="checkbox">) 或("select") 元素中的option HTML元素("option")。


          input:checked {

           box-shadow: 0 0 0 3px hotpink;

          }


          <input type="checkbox" />

          clipboard.png


          大家都說簡歷沒項目寫,我就幫大家找了一個項目,還附贈【搭建教程】。


          :valid | 選擇一個有效的元素

          :valid CSS 偽類表示內容驗證正確的<input> 或其他 <form> 元素。這能簡單地將校驗字段展示為一種能讓用戶辨別出其輸入數據的正確性的樣式。


          input:valid {

           box-shadow: 0 0 0 3px hotpink;

          }

          clipboard.png


          :invalid | 選擇一個無效的元素

          :invalid CSS 偽類 表示任意內容未通過驗證的 <input> 或其他 <form> 元素。


          input[type="text"]:invalid {

             border-color: red;

          }

          :lang() | 通過指定的lang值選擇一個元素

          :lang() CSS 偽類基于元素語言來匹配頁面元素。


          /* 選取任意的英文(en)段落 */

          p:lang(en) {

           quotes: '\201C' '\201D' '\2018' '\2019';

          }

          :not() | 用來匹配不符合一組選擇器的元素

          CSS 偽類 :not() 用來匹配不符合一組選擇器的元素。由于它的作用是防止特定的元素被選中,它也被稱為反選偽類(negation pseudo-class)。


          來看一個例子:


          .innerDiv :not(p) {

             color: lightcoral;

          }

          <div class="innerDiv">

             <p>Paragraph 1</p>

             <p>Paragraph 2</p>

             <div>Div 1</div>

             <p>Paragraph 3</p>

             <div>Div 2</div>

          </div>

          clipboard.png


          Div 1 和 Div 2會被選中,p 不會被選 中。


          原文:https://blog.bitsrc.io/css-ps...


          代碼部署后可能存在的BUG沒法實時知道,事后為了解決這些BUG,花了大量的時間進行log 調試,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug。



          日歷

          鏈接

          個人資料

          藍藍設計的小編 http://www.syprn.cn

          存檔

          亚洲va欧美va天堂v国产综合