JavaScript的每日一問

這篇文章是對一些常用的JS方法進行補充說明,或者是一些好玩的例子,或者是一些採坑的情況,總之就是JS各種小知識的混合吧~~不定期更新

1. Set 使用方法

1.1 基本用法

ES6 提供了新的數據結構 Set,它類似於數組,但是成員的值都是唯一的,沒有重複的值。
因爲這種作用,現在去重最常用的方法就是這個,如下面代碼

const a = [1, 2, 5, 3, 2, 7, 5]
const arr = [...new Set(a)]
console.log(arr) // [ 1, 2, 5, 3, 7 ]

1.2 補充說明

關於Set的用法,很多文章都有說,ES6的官方文檔更有詳細說明,就不多交代,這部分主要討論一些不一樣的情況
如下面代碼:

const mySet = new Set([{ a: 1 }, { a: 1 }])
const result = [...mySet]
console.log(result)

結果: [ { a: 1 }, { a: 1 } ]

這個例子就不是如我們預料的可以去重,這是因爲:
雖然這兩個值具有相同的鍵值對,但是set是對內存(memory)中不同對象進行引用,這就和{a:1} === {a:1}爲false是一樣的原因。

const obj = { a: 1 }
console.log(...new Set([obj, obj]))
結果: { a: 1 }

但是要換用上面的方法就可以實現去重,這是因爲兩個obj都是指向內存中同一個對象

2.sort使用方法

2.1 基本用法

sort()是用來進行排序,並且會改變原先數據,常用方法:

const arr1 = ['a', 'b', 'c']
const arr2 = ['b', 'c', 'a']
console.log(arr2.sort())
console.log(arr2)
結果: [ 'a', 'b', 'c' ]
      [ 'a', 'b', 'c' ]

2.2 補充說明

先看個代碼:

const arr1 = ['a', 'b', 'c']
const arr2 = ['b', 'c', 'a']
console.log(arr1.sort() === arr1, arr2.sort() === arr2, arr1.sort() === arr2.sort())

結果:true true false

爲什麼會是這種結果呢?
因爲在進行===比較時候,sort排序不會對比較產生影響,無論是arr1還是arr1.sort()都是指向內存中相同的對象arr1,所以結果爲true,arr2的判斷也是同理,雖然arr2.sort()排序之後的數組會發生變化,但是他們指向都是內存中arr2,所以也是true,而arr1.sort()arr2.sort()雖然轉換後結果相同,但是指向的數組不同,所以爲false

3.擴展運算符(spread)

3.1 基本用法

擴展運算符(spread)是三個點(…),將一個數組轉爲用逗號分隔的參數序列。

console.log(1, ...[2, 3, 4], 5)

結果: [ 1, 2, 5, 3, 7 ]

我們在開發時候經常會使用這種用法,確實方便

3.2 補充說明

但是有種用法還是要注意

const arr3 = [{ firstName: 'James' }]
const arr4 = [...arr3]
arr4[0].firstName = 'John'
console.log(arr3)
console.log(arr4)

結果: [ { firstName: 'John' } ]
      [ { firstName: 'John' } ] 

從上面代碼可以看出,使用spread符號 ,然後對新的對象中值進行修改,會對原先數組的對象產生影響。
這是因爲spread擴展符號是對arr3進行的淺複製(shallow copy),所以產生的新數組arr4實際指向和arr3相同的內存地址,這樣當修改arr4就會影響arr3

4.var和Let

4.1 例子

先看一個例子:

for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 1);
  }
  // 結果:3 3 3
  
  for (let i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 1);
  }
  // 結果:0 1 2

兩個例子只是分別使用var和Let進行變量聲明,結果就是不同的。

4.2 原因

因爲在時間循環中(event loop)中,setTimeout的執行順序是在整個循環調用結束之後。
在第一個第一個例子中,變量i使用var進行的變量聲明,i成爲了全局變量。所以在進行For循環時候,變量1在進行i++的處理,直到爲3,循環結束後,開始進行setTimeout的執行順序,此時輸出就是3 3 3。對於這種情況,以前最常用的方法便是使用閉包來解決這個問題,不過這裏不過多說明。
接着看第二個例子,在這裏變量i使用let進行聲明,let和Const都是塊作用域,也就是作用域是{}之間,這樣每次進行循環時候,i都是一個新值。

5. 箭頭函數

5.1 例子

  const shape = {
    radius: 10,
    diameter() {
      return this.radius * 2;
    },
    perimeter: () => 2 * this.radius,
  };
  
  console.log(shape.diameter()); //20
  console.log(shape.perimeter()); //NaN

兩個例子第一個是我們最常用的“正常”的函數,第二個是箭頭函數,兩個結果截然不同。

5.2 原因

箭頭函數的this指向上下文函數this的值。在這個例子裏,上下文沒有函數對象,就默認爲window,而window裏面沒有radius這個屬性,就返回爲NaN。

6. 賦值

6.1 例子

let c = { greeting: 'Hey!' };
let d;

d = c;
c.greeting = 'Hello';
console.log(d.greeting);// Hello

6.2 原因

其實這個原因和上面2以及3例子的原因很像,因爲d和c都是指向了同一個內存,修改一個內存值時候,另一個也會改變

7. new Number

7.1 例子

let a = 3;
let b = new Number(3);

console.log(a == b); //true
console.log(a === b); //false
console.log(typeof(b));//object

7.2 原因

new Number()儘管看着像是一個Number,但其實它的類型是Object,所以和真正的number還是不同的

8. Class中的static繼承

8.1 class基本語法

class Chameleon {
    colorChange(newColor) {
      this.newColor = newColor;
      return this.newColor;
    }
  
    constructor({ newColor = 'green' } = {}) {
      this.newColor = newColor;
    }
  }
  
  const freddie = new Chameleon({ newColor: 'purple' });
  console.log(freddie.colorChange('orange'));

這個代碼的運行結果是什麼?

答案是:orange

這裏簡單介紹一下class,詳細說明請看ES6官網class基本語法

JavaScript 語言中,生成實例對象的傳統方法是通過構造函數,但是ES6 提供了更接近傳統語言的寫法,引入了 Class(類)這個概念,作爲對象的模板,通過class關鍵字,可以定義類。

在class裏面有一個constructor方法,這就是構造方法,而this關鍵字則代表實例對象。constructor方法是類的默認方法,通過new命令生成對象實例時,自動調用該方法。一個類必須有constructor方法,如果沒有顯式定義,一個空的constructor方法會被默認添加。

此外,例子中不只有構造方法,還有一個方法colorChange。

ES6 的類,完全可以看作構造函數的另一種寫法,類的數據類型就是函數,類本身就指向構造函數。使用的時候,也是直接對類使用new命令,跟構造函數的用法完全一致。

8.2 static靜態方法

接着再看一個例子:

class Chameleon {
    static colorChange(newColor) {
      this.newColor = newColor;
      return this.newColor;
    }
  
    constructor({ newColor = 'green' } = {}) {
      this.newColor = newColor;
    }
  }
  
  const freddie = new Chameleon({ newColor: 'purple' });
  console.log(freddie.colorChange('orange'));

猜猜這個結果是什麼?

答案是:TypeError: freddie.colorChange is not a function

看這個報錯信息,是freddie的實例沒有colorChange的方法,那就說明一下出現這種情況的原因:

類相當於實例的原型,所有在類中定義的方法,都會被實例繼承。如果在一個方法前,加上static關鍵字,就表示該方法不會被實例繼承,而是直接通過類來調用,這就稱爲“靜態方法”。

通過了解static的使用條件,就明白這個結果的原因,上面代碼中,Chameleon 類的colorChange方法前有static關鍵字,表明該方法是一個靜態方法,可以直接在Chameleon 類上調用(Chameleon .colorChange()),但不能在Chameleon 類的實例上調用。如果在實例上調用靜態方法,會拋出一個錯誤,表示不存在該方法。

8.3 extends對於static的繼承

前面兩個例子已經瞭解之後,再看一個新例子:

class Chameleon {
    static colorChange(newColor) {
      this.newColor = newColor;
      return this.newColor;
    }
  
    constructor({ newColor = 'green' } = {}) {
      this.newColor = newColor;
    }
  }
  
  class freddie extends Chameleon {}
  console.log(freddie.colorChange('orange'))

這個例子也是有static,那麼結果是什麼呢?

答案是:orange

接下來說一下原因:
這和Class的繼承有關,父類的靜態方法,也會被子類繼承
上面代碼中,colorChange()是Chameleon 類的靜態方法,freddie 繼承Chameleon ,也繼承了Chameleon 的靜態方法。所以結果不是TypeError,而是Orange

9. new一個實例

new 運算符創建一個用戶定義的對象類型的實例或具有構造函數的內置對象的實例。

9.1 例子1

function Person(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
  
  const member = new Person('Lydia', 'Hallie');
  const member1 = Person('Sarah', 'Smith');
  
  console.log(member);
  console.log(member1);

結果是什麼?

答案是:
Person { firstName: ‘Lydia’, lastName: ‘Hallie’ }
undefined

爲什麼會是這種結果?這和new一個新實例的流程有關。那麼當new一個實例時,會經歷什麼流程?

答:new 關鍵字會進行如下的4步操作:

  1. 創建一個新的對象:var obj = new Object()
  2. 繼承對象類型的原型:obj._proto_=Person.prototype
  3. 將Person中的this指向obj,並實行Person函數體:var result = Person.call(obj)
  4. 判斷Person的返回值類型,如果是值類型,返回obj。如果是引用類型,就返回這個引用類型的對象
    if (typeof(result) == "object") person = result; else person = obj;

現在回答上面問題,爲什麼是這個結果?
對於第一個例子,由於使用了new,首先創建了一個新對象member,然後使person中的this指向了member,所以變成了merber.firstName = 'Lydia'
對於第二個例子,沒有使用new,那麼Person裏面的this便指向了全局global,所以變成了global.firstName = 'Sarah',但是member1卻沒有值,所以爲Undefined

9.2 例子2

function Person(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
  
  const member = new Person('Lydia', 'Hallie');
  Person.getFullName = function() {
    return `${this.firstName} ${this.lastName}`;
  };
  
   console.log(member.getFullName());

這個結果會是什麼?

答案是:TypeError: member.getFullName is not a function

原因:我們不能像常規對象那樣直接給構造函數添加屬性,這樣添加的屬性無法被new的新實例進行使用。

9.3 例子3

function Person(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
  
  const member = new Person('Lydia', 'Hallie');
  Person.prototype.getFullName = function() {
    return `${this.firstName} ${this.lastName}`;
  };
  
  console.log(member.getFullName());

看看這個結果是什麼?

答案:Lydia Hallie

原因:爲什麼這個代碼就可以運行呢?這是因爲我們把這個getFullName的方法掛到了prototype上面,通過上面new實例第2個步驟中可以知道,新構建的實例member也可以使用getFullName這個方法

10. function的一個有趣例子

10.1 例子

function bark() {
  console.log('Woof!');
}

bark.animal = 'dog';

大家覺得這個例子會出現什麼結果?一切正常還是會報錯?

答案是:一切正常,不會報錯

10.2 原因

這是因爲function的類型是object!function其實是具有屬性的,這個屬性可以調用。其實這個和9.2實例的情況相同

11. 事件機制

11.1 JavaScript 事件觸發的階段

JavaScript 事件觸發分爲三個階段:

  1. Capturing :捕獲階段
  2. Target:目標階段
  3. Bubbling:冒泡階段

DOM 的事件在傳播時,會從根節點開始往下傳遞到 target ,若註冊了事件監聽器,則監聽器處於捕獲階段

target 就是觸發事件的具體對象,這時註冊在 target 上的事件監聽器處於目標階段

最後,事件再往上從 target 一路逆向傳遞到根節點,若註冊了事件監聽器,則監聽器處於冒泡階段

整個流程 捕獲 --> 目標 --> 冒泡

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章