深入理解ES6 003【學習筆記】

函式

參數預設值,以及一些關於arguments物件,如何使用表達式作為參數、參數的暫時性死區。

以前設定預設值總是利用在含有邏輯或運算子(logical OR operator)的表達式中,前一個值是false時,總是返回後面一個的值,但如果我們給參數傳入0時,就有些麻煩。需要去驗證一下型別。

function makeRequest(url,timeout,callback){
  timeout = timeout || 2000;
  callback = callback || function(){};
  // 邏輯
}
// 解決傳0問題
function makeRequest(url,timeout,callback){
  timeout = typeof timeout !=='undefined'?timeout: 2000;
  callback = typeof callback !=='undefined'?callback : function(){};
  // 邏輯
}

預設參數

不傳入或傳入undefined。如果傳入null不會使用預設值,會認為是一個合法的值。預設參數對arguments物件的影響。之前的參數傳遞會體現在arguments物件上。 在ES5非嚴格模式下,命名參數的變化會同步更新到arguments物件上,但在ES5的嚴格模式下,取消了這個行為,無論參數如何變化,arguments物件不再隨之改變。如下:

function mixArgs(first,second){
  "use strict"

  console.log(first === arguments[0]);
  console.log(second === arguments[1]);

  first = 'c';
  second = 'd'

 console.log(first === arguments[0]);
 console.log(second === arguments[1]);
}
mixArgs('a','b')
// true
// true
// false
// false

ES6中,如果一個函式使用了預設參數值,則無論是否明確定義了嚴格模式,arguments物件的行為都將與ES5嚴格模式下保持一致。

預設參數表達式

初次解析函式宣告時不會呼叫getValue()方法,只有當呼叫add()函式且不傳入第二個參數時才會呼叫。

let value = 5;
function getValue(){
  return value++;
}
function add(first,second=getValue()){
  return first+second;
})
console.log(add(1,1)) // 2
console.log(add(1)) // 6
console.log(add(1)) //7

先定義的參數可以作為後定義參數的預設值,反過來不成立(first=second,second),因為second比first晚定義,因此其不能作為first的預設值。為了幫助你理解背後的原理,我們重溫一下暫時性死區(Temporal Dead Zone, TDZ)的概念。與let宣告類似,定義參數時會為每個參數建立一個新的識別符號繫結,該繫結在初始化之前不可被引用,如果試圖存取會導致程式拋出錯誤。
函式參數有自己的作用域和暫時性死區,其與函式體的作用域是各自獨立的,也就是說參數的預設值不可存取函式體內的變數。
無論是否使用不定參數,arguments物件總是包含所有傳入函式的參數。

處理無命名參數--不定參數

// 函式的length屬性統計的是函式命名參數的數量,不定參數的加入不會影響length屬性的值。
function pick(object, ...keys){
  let result = Object.create(null);
  for(let i=0,len=keys.length;i<len;i++){
    result[keys[i]] = object[keys[i]]
  }
  return result;
}
  • 不定參數(當然在函式參數中使用)
  • 每個函式最多只能宣告一個不定參數
  • 而且一定要放在所有參數的末尾
  • 不允許在setter中使用不定參數,是因為物件字面值(object literal)setter的參數有且只能有一個(在不定參數的定義中,參數的數量可以無限多,所以在當前上下文中允許使用不定參數)。
let object = {
  // 語法錯誤 不可以在Setter中使用不定參數
  set name(...value){
    // ...
  }
}

無論是否使用不定參數,arguments物件總是包含所有傳入函式的參數。

增強的Function建構函式

可以在動態建立函式時使用預設值及不定參數,唯一需要做的是在參數名後添加一個等號和一個預設值,定義不定參數,只需在最後一個參數前添加...。如下:

var add = new Function('first','second=first','return first+second')
console.log(add(1,1)) // 2
console.log(add(1)) // 2
var pickFirst = new Function("...args",'return args[0]')
console.log(pickFirst(1,2)) // 1

展開運算子

展開運算子(Spread Operator)可以讓你指定一個陣列,將它們打散後作為各自獨立的參數傳入函式。JavaScript內建的Math.max()方法可以接受任意數量的參數並返回值最大的那個,這是一個簡單的用例。

自己理解,注意,前面那個是定義時用的,這個是呼叫時使用(參數是分開的,而你有一個陣列)。

如下,之前陣列求最大值的做法:

let values = [25,50,75,100]
console.log(Math.max.apply(Math,values)) // 100;
// max方法不允許傳入一個陣列
console.log(Math.max(...values))
// 展開運算子後面可以繼續添加參數
console.log(Math.max(...values,110)) //110

函式的name屬性

宣告函式的.name屬性是它宣告的函式名,函式表達式如果函式是匿名的則就是其變數的名稱,如果不是則是其宣告的名字,

它只是協助偵錯用的額外資訊,所以不能使用name屬性的值來獲取對於函式的引用。

function somethingDoing(){
  // 空函式
}
var doSomeThing = function doSomethingElse() {
  // 空函式
}
var person = {
  get firstName(){
    return 'Nicholas';
  },
  sayName:function(){
    console.log(this.name)
  }
}
console.log(somethingDoing.name) // somethingDoing
console.log(doSomeThing.name) // doSomethingElse 函式表達式有個名字,這個名字比要賦值的變數權重高
console.log(person.sayName.name) // sayName
console.log(person.firstName.name) // get firstName
// 兩個特例
// 透過bind()建立的函式會帶前綴bind
var doSomthing = function(){}
console.log(doSomthing.bind().name) // "bound doSomething"
console.log(new Function().name) // anonymous

明確函式的多重用途

ES5當使用new時,函式內的this值指向一個新物件,函式最終返回一個新物件(沒有明確返回一個物件時)。

function Person(name){
  this.name = name;
}
var person = new Person('Nicholas');
var notPerson = Person('Nicholas'); 
console.log(person) // '[Object object]'
console.log(notPerson) // undefined

上例中,在ES6中函式有兩個不同的內部存取[[Call]][[Construct]]。當透過new關鍵字呼叫函式時,執行的是[[Construct]]函式,它負責建立一個通常被稱作實例的新物件,然後執行函式體,將this繫結到實例上。而不使用new關鍵字時,則呼叫[[Call]]執行函式體,具有[[Construct]]方法的函式統稱為建構函式。切記不是所有的函式都有[[Construct]]方法 如:箭頭函式

ES5判斷函式是否被new呼叫

function Person(name){
  if(this instanceof Person){
    this.name = name
  }else {
    throw new Error('必須透過new關鍵字來呼叫Person')
  }
}

上面的方式在一定程度上可以避免,但不要忘了還有call和apply呢……

var person = new Person('Nicholas');
var person =  Person('Nicholas'); // 報錯
var person =  Person.call(person,'Michael'); // 通過

無屬性new.target

為瞭解決判斷函式是否透過new關鍵字呼叫的問題,ES6引入了new.target這個元屬性(meta-property),元屬性是指非物件的屬性,其可以提供非物件目標的補充資訊。當呼叫函式[[Construct]]方法時,new.target上被賦予new的操作目標,通常是新建立的物件實例,也就是函式體內this的建構函式;如果呼叫[[Call]]方法,則new.target值為undefined,有了這個元屬性,可以透過檢查new.target是否被定義過來安全地偵測是否透過new關鍵字呼叫的。

function Person(name){
  if(typeof new.target!=='undefined'){
    this.name = name;
  }else {
    throw new Error('必須透過new關鍵字來呼叫Person')
  }
}
var person = new Person('Nicholas'); // 通過
var person =  Person('Nicholas'); // 報錯
var person =  Person.call(person,'Michael'); // 報錯

也可能透過new.target是否被某個特定的建構函式所呼叫。如下:

function Person(name){
  // if(typeof new.target!=='undefined'){
    if(new.target===Person)
    this.name = name;
  }else {
    throw new Error('必須透過new關鍵字來呼叫Person')
  }
}

function AnotherPerson(name){
  Person.call(this,name)
}
var person = new Person('Nicholas');
var otherPerson = new AnotherPerson('Michael') // 拋出錯誤

區塊級函式

ES5的嚴格格式下,我們在程式碼區塊中宣告函式是不被允許的,而在ES6中將doSomething()函式視作一個區塊級宣告,從而可以在定義該函式的程式碼內存取和呼叫它:

"use strict"

if(true){
  console.log(typeof doSomething) // "function"
  function doSomething(){
    // 空函式
  }
  doSomething() //
}
typeof doSomething // undefined

和let的使用差不多,唯一區別是區塊級函式會將宣告提到程式碼區塊的頂部。注意函式表達式宣告的區別。在ES6中,即使處於非嚴格模式下,也可以宣告區塊級函式,但其行為與嚴格模式下稍有不同。這些函式不再提升至程式碼區塊的頂部,而是提升至外圍函式或全域作用域的頂部,如下:

// ECMAScript 6中的行為

if(true){
  console.log(typeof doSomething) // "function"
  function doSomething(){
    // 空函式
  }
  doSomething() //
}
typeof doSomething // function

箭頭函式

let PageHandler = {
  id:"123456",
  init: function(){
    document.addEventListener("click",function(event){
      this.doSomething(event.type); // 拋出錯誤
    },false)
  },
  doSomething: function(type){
    console.log("Handling "+ type + " for "+this.id)
  }
}

上面的程式碼並沒有如預期的正常執行。因為this的繫結是事件目標物件的引用(這裡是指document),而沒有繫結在PageHandler,且由於this.doSomething()在目標document中不存在,所以無法正常執行,之前的做法是透過bind()方法明確地將this繫結到PageHandler函式上來修正這個問題。如下:

let PageHandler = {
  id:"123456",
  init: function(){
    document.addEventListener("click",(function(event){
      this.doSomething(event.type); 
    }).bind(this),false)
  },
  doSomething: function(type){
    console.log("Handling "+ type + " for "+this.id)
  }
}

呼叫bind(this)後事實上建立了一個新函式,它的this被繫結到目前的this即PageHandler。為了避免建立一個額外的函式,我們可以透過一個更好的方式來修正這段程式碼,使用箭頭函式。箭頭函式中沒有this繫結,必須透過查找作用域鏈來決定其值。

let PageHandler = {
  id:"123456",
  init: function(){
    document.addEventListener("click",(event)=>{
      this.doSomething(event.type); 
    },false)
  },
  doSomething: function(type){
    console.log("Handling "+ type + " for "+this.id)
  }
}
  • 沒有this、super、arguments和new.target繫結
  • 不能透過new關鍵字呼叫 箭頭函式沒有[[Construct]]方法,所以不能被用作建構函式
  • 沒有原型
  • 不可以改變this的繫結
  • 不支援arguments物件
  • 不支援重複的命名參數

箭頭函式也同樣有一個name屬性,這與其他函式的規則相同。

this繫結

如果箭頭函式被非箭頭函式包含,則this繫結的是最近一層非箭頭函式的this;否則,this的值會被設定為undefined。

箭頭函式和陣列

箭頭函式的語法簡潔,非常適用於陣列處理。舉例來說,比如給陣列排序,通常是:

var result = values.sort(function(a,b){
  return a - b;
})

// 我們可以將其簡寫至如下:
var result = values.sort((a,b)=>a-b)

諸如sort()map()reduce()這些可以接受回呼函式(callback function)的陣列方法,

尾端呼叫最佳化

函式作為另一個函式的最後一條語句被呼叫。

function doSomething(){
  return doSomethingElse() // 尾端呼叫;
}

在ES5引擎中,尾端呼叫的實現與其他函式呼叫的實現類似:建立一個新的堆疊幀(stack frame),將其推入呼叫堆疊(call stack)來表示函式呼叫。也就是說,在循環呼叫中,每一個未用完的堆疊幀都會被保存在記憶體中,當呼叫堆疊變得過大時會造成程式問題。

為了縮減嚴格模式下尾端呼叫堆疊的大小(非嚴格格式不受影響),如果滿足以下條件,尾端呼叫不再建立新的堆疊幀,而是清除並重用目前的堆疊幀:

  • 尾端呼叫不存取目前堆疊幀的變數(也就是說函式不是一個閉包)
  • 在函式內部,尾端呼叫是最後一條語句
  • 尾端呼叫的結果作為函式的返回值
"use strict"

function doSomething(){
  return doSomethingElse() // 尾端呼叫最佳化;
}

// 注意以上3個條件

如何利用尾端呼叫最佳化


function factorial(n){
  if(n<=1){
    return 1
  } else {
    // 無法最佳化
    return n * factorial(n-1)
  }
}

// 可以利用傳遞第二個參數來保存每次階乘的結果(並且預設結果是1)
function factorial(n,p=1){
  if(n<=1){
    return 1*p
  }else {
    let result = n*p;
    //最佳化後
    return factorial(n-1,result)
  }
}

主題測試文章,只做測試使用。發佈者:Walker,轉轉請注明出處:https://walker-learn.xyz/archives/4310

(0)
Walker的頭像Walker
上一篇 2025年3月8日 12:39
下一篇 2025年3月8日 10:59

相關推薦

  • Go工程師體系課 protobuf_guide【學習筆記】

    Protocol Buffers 入門指南 1. 簡介 Protocol Buffers(簡稱 protobuf)是 Google 開發的一種語言無關、平台無關、可擴充的結構化資料序列化機制。與 JSON、XML 等序列化方式相比,protobuf 更小、更快、更簡單。 專案首頁:https://github.com/protocolbuffers/prot…

    個人 2025年11月25日
    1.2K00
  • 深入理解ES6 006【學習筆記】

    Symbol 與 Symbol 屬性 第6種原始資料型別:Symbol。私有名稱原本是為了讓開發者們建立非字串屬性名稱而設計的,但是一般的技術無法偵測這些屬性的私有名稱 建立 Symbol let firstName = Symbol(); let person = {} person[firstName] = "Nicholas"; cons…

    個人 2025年3月8日
    1.2K00
  • TS珠峰 002【學習筆記】

    泛型 /* * @Author: error: error: git config user.name & please set dead value or install git && error: git config user.email & please set dead value or install git &a…

    個人 2025年3月27日
    1.5K00
  • Go工程師體系課 018【學習筆記】

    API 閘道與持續部署入門(Kong & Jenkins) 對應資料目錄《第 2 章 Jenkins 入門》《第 3 章 透過 Jenkins 部署服務》,整理 Kong 與 Jenkins 在企業級持續交付中的實戰路徑。即使零基礎,也能順著步驟建立出自己的閘道 + 持續部署管線。 課前導覽:什麼是 API 閘道 API 閘道位於用戶端與後端微服務…

    個人 2025年11月25日
    19000
  • 深入理解ES6 008【學習筆記】

    迭代器(Iterator)和產生器(Generator)這項新特性對於高效的資料處理而言是不可或缺的,你也會發現在語言的其他特性中也都有迭代器的蹤影:新的 for-of 迴圈、展開運算子 (...)、甚至連非同步程式設計都可以使用迭代器。 迭代器是一種特殊的物件,它具有一些專門為迭代過程設計的專有介面,所有的迭代器物件都有一個 next() 方法,每次呼叫都傳回一個結果對…

    個人 2025年3月8日
    1.1K00
簡體中文 繁體中文 English
歡迎🌹 Coding never stops, keep learning! 💡💻 光臨🌹