ES6語法——函數、數組、對象

http://es6.ruanyifeng.com/阮一峯ES6

目錄

七、函數的擴展

    1、參數的默認值    2、rest參數    3、擴展運算符...    4、箭頭函數    5、尾調用    6、函數參數的尾逗號

八、數組的擴展

    1、Array.from(),Array.of()    2、fill()    3、entries(),keys(),values()

    4、find(),findIndex(),includes()    5、copyWithin()    6、flat(),flatMap()

九、對象的擴展

    1、屬性的簡潔表示法    2、屬性名表達式    3、Object.is()

    4、Object.assign()和對象的擴展運算符    5、Object.getOwnPropertyDescriptors()

    6、__proto__屬性,Object.setPrototypeOf(),Object.getPrototypeOf()

    7、Object.keys(),Object.values(),Object.entries()    8、Object.fromEntries()

七、函數的擴展

1、參數的默認值

ES6允許爲函數的參數設置默認值,直接將默認值寫在參數定義的後面,便於閱讀和修改。

有默認值的參數可以省略,但解構賦值的默認值與參數默認值不同。

function log(x,y="World"){
  console.log(x,y);
}
log("Hello"); // "Hello World"
log("Hello","Hsg"); // "Hello Hsg"

function foo({x,y=5}={}){ // 參數默認值
  console.log(x,y);
}
foo() // undefined 5
function foo({x,y=5}){ // 解構賦值默認值
  console.log(x,y);
}
foo() // TypeError: Cannot read property 'x' of undefined

(1)參數變量是默認聲明的,所以不能用let或const再次聲明。使用參數默認值時,函數不能有同名參數。

function foo(x=5){
  let x=1; // error
  const x=2; // error
}

function foo(x,x,y){} // 不報錯
function foo(x,x,y=1){} // SyntaxError: Duplicate parameter name not allowed in this context

(2)參數默認值不是傳值的,而是每次都重新計算默認值表達式的值。

let x=99;
function foo(p=x){
  console.log(p);
}
foo(); // 99
x=100;
foo(); //100

(3)參數默認值的位置

只有尾部的參數設置默認值,纔可以省略該參數。除非顯式輸入undefined,才能省略該參數。

function foo(x=1,y){
  console.log(x,y);
}
foo(); // "1 undefined"
foo(2); // "2 undefined"
foo(,2); // 報錯
foo(undefined,2); // "1 2"

(4)函數的length屬性

length屬性返回沒有指定默認值的參數個數,不包括rest參數,如果不是尾參數,也不包括默認值之後的參數。

(5)作用域

一旦設置了參數的默認值,函數進行聲明初始化時,參數會形成一個單獨的作用域,與函數內部作用域不是同一個。等到初始化結束,這個作用域就會消失。

let x="outer";
function foo(x,y=x){
  console.log(x,y);
}
foo("para"); // "para para"

2、rest參數

rest參數(...變量名),將函數的多餘參數放入數組中,可以使用數組的方法,不需要使用arguments對象了。

rest參數之後不能再有其它參數。函數的length屬性不包括rest參數。

function test(...args){
  for(let value of args){
    console.log(value); // "1 2 3 4 5"
  }
}
test(1,2,3,4,5);

3、擴展運算符...

擴展運算符(...),可理解爲rest參數的逆運算,將一個數組轉爲用逗號分隔的參數序列。該運算符主要用於函數調用。

console.log(...[1,2,3]) // 1 2 3
// 擴展運算符與正常的函數參數可以結合使用
function f(v,w,x,y,z){}
const args=[0,1];
f(-1,...args,2,...[3]);
// 擴展運算符後面還可以放置表達式
const arr=[...(x>0?['a']:[]),'b'];

// 擴展運算符如果放在括號中,JavaScript引擎就會認爲這是函數調用,否則就會報錯。
(...[1,2]) // Uncaught SyntaxError: Unexpected number
console.log((...[1,2])) // Uncaught SyntaxError: Unexpected number

(1)替代apply函數

// Math.max,求一個數組最大元素
Math.max.apply(null,[14,3,77]);// ES5
Math.max(...[14,3,77]); // ES6
// push,將一個數組添加到另一個數組的尾部
Array.prototype.push.apply(arr1,arr2); // ES5
arr1.push(...arr2); // ES6

(2)應用

// 複製數組,而不僅是複製指向數組的指針
const a2=a1.concat(); // ES5
const a2=[...a1]; // ES6方法1
const [...a2]=a1; // ES6方法2,解構賦值

// 合併數組,下面兩種方式均是淺拷貝
a1.concat(a2,a3); // ES5
[...a1,...a2,...a3]; // ES6

// 將字符串轉爲真正的數組,能夠正確識別四個字節的Unicode字符
[...'hello'] // [ "h", "e", "l", "l", "o" ]
[...'x\uD83D\uDE80y'].length // 3

// 任何Iterator接口的對象,都可以用擴展運算符轉爲真正的數組(Map和Set結構,Generator函數)
let nodeList=document.querySelectorAll('div');
let array=[...nodeList];

4、箭頭函數

使用“箭頭”(=>)定義函數,左邊是參數,右邊是返回值。

沒有參數或多個參數,使用一個圓括號代表參數部分。

代碼塊部分多於一條語句,要使用大括號將它們括起來,並且使用return語句返回。如果直接返回一個對象,必須在對象外面加上括號(),否則會報錯。

var f=v=>v;
var f=()=>5;
var sum=(num1,num2)=>num1+num2;
var sum=(num1,num2)=>{return num1+num2;};
let getTempItem=id=({id:id,name:"Temp"}); // 直接返回一個對象
let f=()=>void doesNotReturn(); // 只有一行語句,且不需要返回值

// 箭頭函數與變量解構
const full=({first,last})=>first+' '+last;
// 等同於
function full(person){
  return person.first+' '+person.last;
}

箭頭函數有幾個使用注意點。

(1)函數體內的this對象是固定的,就是定義時所在的對象,而不是使用時所在的對象。

(2)不可以當作構造函數使用new命令(沒有自己的this),否則會拋出一個錯誤。

(3)不可以使用arguments對象,該對象在函數體內不存在,可以用rest參數代替。

(4)不可以使用yield命令,不能用作Generator函數。

this指向的固定化,是因爲箭頭函數沒有自己的this,導致內部的this就是外層代碼塊的this。

指向外層函數的對應變量arguments、super、new.target在箭頭函數中也不存在。

function foo(){ // ES6
  setTimeout(()=>{
    console.log('id:',this.id);
  },100);
}
var id=21;
foo.call({id:42}); // id: 42

function foo2(){ // ES5
  var _this=this;
  setTimeout(function(){
    console.log('id:', _this.id);
  },100);
}
foo2.call({id:42}); // id: 42

不應該使用箭頭函數:定義函數的方法,且該方法內部包括this;需要動態this;如果函數體很複雜,有許多行,或有大量的讀寫操作,不單純是爲了計算值(爲了提高代碼可讀性)。 

“函數綁定”運算符(::)爲箭頭函數綁定this對象,大大減少了顯式綁定this對象的寫法(call、apply、bind)。雙冒號左邊是一個對象,右邊是一個函數,自動將左邊的對象作爲this對象綁定到右邊的函數上。如果雙冒號左邊爲空,右邊是一個對象的方法,則等於將該方法綁定在該對象上面。

foo::bar; // 等同於bar.bind(foo);
let log=::console.log; // 等同於var log=console.log.bind(console);

5、尾調用

尾調指某個函數的最後一步是調用另一個函數。

function f(x){ // 尾調用
  return g(x);
}
function f(x){ // 調用後還有賦值操作
  let y=g(x);
  return y;
}
function f(x){ // 調用後還有操作
  return g(x)+1;
}
function f(x){ // 調用後還有return undefined;
  g(x);
}

尾調用優化:只保留內層函數的調用幀,不需要保留外層函數的調用幀(調用位置、內部變量等信息),調用幀只有一項將大大節省內存。注意只有不再用到外層函數的內部變量,內層函數的調用幀纔會取代外層函數的調用幀,否則就無法進行“尾調用優化”。

一旦使用遞歸,最好使用尾遞歸。

function f(){
  let m=1;
  let n=2;
  return g(m+n);
}
f(); // 等同於g(3);

function addOne(a){
  var one=1;
  function inner(b){
    return b+one; // 無法進行尾調用優化
  }
  return inner(a);
}

6、函數參數的尾逗號

ES2017允許函數的最後一個參數有尾逗號。使得函數參數與數組和對象的尾逗號規則保持一致。

將參數寫成多行,以後增加參數或者調整參數的次序,勢必要在原來最後一個參數後面添加一個逗號。這對於版本管理系統來說,就會顯示添加逗號的那一行也發生了變動。這看上去有點冗餘,因此新的語法允許定義和調用時,尾部直接有一個逗號。

八、數組的擴展

1、Array.from(),Array.of()

Array.from方法用於將兩類對象轉爲真正的數組:類數組的對象(array-like object)和可遍歷(iterable)的對象(包括ES6新增的數據結構Set和Map)。如果參數是一個真正的數組,返回一個一模一樣的新數組。

擴展運算符(...)也可以將某些數據結構轉爲數組。擴展運算符背後調用遍歷器接口(Symbol.iterator),如果一個對象沒有部署這個接口,就無法轉換。

/* 類數組對象 */

// 1. NodeList對象,DOM操作返回的NodeList集合
let ps=document.querySelectorAll('p');
Array.from(ps).filter();  // [...document.querySelectorAll('div')].filter();

// 2. 函數內部的arguments對象
function foo(){
  var args=Array.from(arguments); // const args=[...arguments];
}

// 3. 類數組的對象,必須有length屬性,可以通過Array.from方法轉爲數組,而此時擴展運算符無法轉換。
let arrayLike={
    '0':'a',
    '1':'b',
    '2':'c',
    length:3
};
var arr1=[].slice.call(arrayLike); // ['a', 'b', 'c'] ES5的寫法
let arr2=Array.from(arrayLike); // ['a', 'b', 'c'] ES6的寫法

/* 部署了Iterator接口的數據結構 */

// 1. 字符串
Array.from('hello') // ['h', 'e', 'l', 'l', 'o']

// 2. Set結構
let namesSet=new Set(['a', 'b'])
Array.from(namesSet) // ['a', 'b']

Array.from還可以接受第二個參數,作用類似於數組的map方法,用來對每個元素進行處理,將處理後的值放入返回的數組。

如果map函數裏用到了this關鍵字,還可以傳入Array.from的第三個參數,用來綁定this。 

Array.from(arrayLike,x=>x*x);
Array.from(arrayLike,function(item){return item*item});
// 等同於
Array.from(arrayLike).map(x=>x*x);

Array.of方法用於將一組值,轉換爲數組。如果沒有參數,就返回一個空數組。基本上可以用來替代Array()或new Array(),避免參數個數只有一個時是指定數組的長度,使行爲統一。

Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array(3) // [, , ,]

2、fill()

fill方法使用給定值,填充一個數組,數組中已有的元素,會被全部抹去。可選的第二、三個參數,用於指定填充的起始位置和結束位置。

如果填充的類型爲對象,那麼被賦值的是同一個內存地址的對象,而不是深拷貝對象。

new Array(3).fill(7) 
['a','b','c'].fill(7) // [7, 7, 7]
['a','b','c'].fill(7,1,2) // ['a', 7, 'c']// [7, 7, 7]

let arr=new Array(3).fill({name:"Mike"});
arr[0].name="Ben";
arr // [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}]

3、entries(),keys(),values()

entries(),keys(),values()是新的用於遍歷數組的方法,它們都返回一個遍歷器對象,可以用for...of循環進行遍歷,唯一的區別是keys()是對鍵名的遍歷、values()是對鍵值的遍歷,entries()是對鍵值對的遍歷。

如果不使用for...of循環,可以手動調用遍歷器對象的next方法進行遍歷。

for(let index of ['a','b'].keys()){
  console.log(index); // 0 1
}
for(let item of ['a','b'].values()){
  console.log(item); // 'a' 'b'
}
for(let [index,item] of ['a','b'].entries()){
  console.log(index,item); // 0 'a' 1 'b'
}

4、find(),findIndex(),includes()

find方法,找出第一個符合條件的數組成員,如果沒有符合條件的成員,則返回undefined。findIndex方法返回第一個符合條件的數組成員的位置,如果所有成員都不符合條件,則返回-1。

二者的參數是一個回調函數,所有數組成員依次執行該回調函數,直到找出第一個返回值爲true的成員或成員位置,然後返回該成員或成員位置。第二個參數,綁定回調函數的this對象。

indexOf方法無法識別數組的NaN成員,但是findIndex方法可以藉助Object.is方法做到。

[1,4,-5,10].find((n)=>n<0) // -5
[1,5,10,15].find(function(value,index,arr){
  return value>9;
}) // 10
[NaN].findIndex(y=>Object.is(NaN,y)) // 0

includes方法返回一個布爾值,表示某個數組是否包含給定的值。第二個參數表示搜索的起始位置,默認爲0,如果爲負數,則表示倒數的位置,如果這時它大於數組長度,則會重置爲從0開始。

indexOf不夠語義化,且它內部使用嚴格相等運算符(===)進行判斷,會導致對NaN的誤判。

console.log([1,2,NaN,3].includes(NaN)); // true 
console.log([1,2,NaN,3].includes(NaN,-1)); // false
console.log([1,2,NaN,3].indexOf(NaN)); // -1

5、copyWithin()

copyWithin方法,在當前數組內部,將指定位置的成員複製到其它位置(會覆蓋原有成員),然後返回當前數組(修改後)。第一個參數是必須的,從該位置開始替換數據。第二個參數是可選的,從該位置開始讀取數據,默認爲 0。第三個參數是可選的,到該位置前停止讀取數據,默認等於數組長度。三個參數爲負值,表示倒數。

[1,2,3,4,5].copyWithin(0,3) // [4, 5, 3, 4, 5]
[1,2,3,4,5].copyWithin(0,3,4) // [4, 2, 3, 4, 5]
[1,2,3,4,5].copyWithin(0,-2,-1) // [4, 2, 3, 4, 5]
[].copyWithin.call({length:5,3:1},0,3) // {0: 1, 3: 1, length: 5}

6、flat(),flatMap()

如果數組的成員還是數組,flat()用於將嵌套的數組“拉平”,返回一個新數組,對原數據沒有影響。參數是一個整數,默認爲1,只會“拉平”一層,如果參數爲Infinity,不管有多少層嵌套,都可轉成一維數組。如果原數組有空位,會跳過空位。

flatMap方法對原數組的每個成員執行一個函數(相當於執行map(value,index,arr)),然後對返回值組成的數組執行flat()方法,返回一個新數組。第二個參數,用來綁定遍歷函數裏面的this。只能展開一層數組。

console.log([1,2,[3,[4,5]]].flat()); // [1, 2, 3, [4, 5]]
console.log([1,2,[3,[4,5]]].flat(Infinity)); // [1, 2, 3, 4, 5]
console.log([1,2,,,3].flat()); // [1, 2, 3]
console.log([2,3,4].flatMap((x)=>[x,x*2])); // [2, 4, 3, 6, 4, 8]

九、對象的擴展

1、屬性的簡潔表示法

ES6允許在對象之中,直接寫變量,此時,屬性名和變量名相同,屬性值爲變量的值。方法也可以簡寫。

簡潔寫法的屬性名總是字符串,會導致一些看上去比較奇怪的結果。如class可以作爲屬性名,不會報錯。

const foo="bar";
// baz={foo:"bar"}
const baz={foo}; // ES6
const baz={foo:foo}; // ES5

const o={ // ES6
  method(){
    return "hello";
  }
};
const o={ // ES5
  method:function(){
    return "hello";
  }
}

2、屬性名表達式

JavaScript定義對象的屬性,有兩種方法,分別是使用標識符或表達式(或變量)作爲屬性名。如果使用{}定義對象,在ES5中只能使用標識符定義屬性。

ES6允許{}定義對象時,用表達式作爲對象的屬性名,把表達式放在方括號內。表達式還可以用於定義方法名。

屬性名表達式如果是一個對象,默認情況下會自動將對象轉爲字符串"[object Object]"。

屬性名表達式與簡潔表示法,不能同時使用,會報錯。

obj.abc=123; // 標識符作屬性名
obj['a'+'bc']=123; // 表達式作屬性名
var s="a"+"bc"; // 變量作屬性名
obj[s]=123;

var obj={ // ES5
  abc:123
};
let propKey='foo'; 
let obj={ // ES6
  [propKey]:true,
  ['a'+'bc']:123
};

3、Object.is()

Object.is()用來比較兩個值是否嚴格相等,與===的行爲基本一致。不同之處只有兩個:一是+0不等於-0,二是NaN等於自身。

Object.is('foo','foo') // true
Object.is({}, {}) // false

+0===-0 // true
NaN===NaN // false
Object.is(+0,-0) // false
Object.is(NaN,NaN) // true

4、Object.assign()和對象的擴展運算符

Object.assign方法用於對象的合併,將源對象的所有可枚舉屬性,複製到目標對象。第一個參數是目標對象,後面的參數都是源對象。如果目標對象與源對象或多個源對象有同名屬性,則後面的屬性會覆蓋前面的屬性。

如果只有一個參數,Object.assign會直接返回該參數。如果該參數不是對象,則會先轉成對象,然後返回。由於undefined和null無法轉成對象,所以如果它們作爲參數,就會報錯。

如果非對象參數非首參數,那麼處理規則有所不同。這些參數都會轉成對象,如果無法轉成對象,就會跳過。這意味着,如果undefined和null不在首參數,就不會報錯。數值、字符串和布爾值,除了字符串會以數組形式,拷貝入目標對象,其它值都不會產生效果。

只拷貝源對象的自身屬性,不拷貝繼承屬性,也不拷貝不可枚舉的屬性。

淺拷貝,而不是深拷貝,如果源對象某個屬性的值是對象,那麼目標對象拷貝得到的是這個對象的引用。

只能進行值的複製,如果要複製的值是一個取值函數,那麼將求值後再複製。

const target={a:1,b:1};
const source1={b:2,c:2};
const source2={c:3};
Object.assign(target,source1,source2);
target // {a:1, b:2, c:3}

Object.assign([1,2,3],[4,5]) // [4, 5, 3]將數組視爲對象

const obj={a:1};
Object.assign(obj)===obj // true
typeof Object.assign(2) // "object"

const v1='abc';
const v2=true;
const v3=10;
const obj=Object.assign({},v1,v2,v3);
console.log(obj); // { "0": "a", "1": "b", "2": "c" }

對象的擴展運算符(...)用於取出參數對象的所有可遍歷屬性,拷貝到當前對象之中,等同於使用Object.assign()方法。後面的同名屬性會覆蓋前面的屬性。

let z={a:3,b:4};
let n={...z}; // { a: 3, b: 4 }
// 等同於
let n=Object.assign({},z);

let aWithOverrides={...a,x:1,y:2}; // 後面的自定義屬性覆蓋前面同名屬性

let foo={...['a','b','c']}; // {0: "a", 1: "b", 2: "c"} 將數組視爲對象

上面的例子只是拷貝了對象實例的屬性,如果想完整克隆一個對象,還拷貝對象原型的屬性,可以採用下面的寫法。寫法一的__proto__屬性在非瀏覽器的環境不一定部署,因此推薦使用寫法二和寫法三。 

const clone1={ // 寫法一
  __proto__:Object.getPrototypeOf(obj),
  ...obj
};

const clone2=Object.assign( // 寫法二
  Object.create(Object.getPrototypeOf(obj)),
  obj
);

const clone3=Object.create( // 寫法三
  Object.getPrototypeOf(obj),
  Object.getOwnPropertyDescriptors(obj)
)

5、Object.getOwnPropertyDescriptors()

ES5的Object.getOwnPropertyDescriptor()方法會返回某個對象屬性的描述對象。ES2017引入了Object.getOwnPropertyDescriptors()方法,返回指定對象所有自身屬性(非繼承屬性)的描述對象。

const obj={
  foo:123,
  get bar(){return 'abc'}
};
Object.getOwnPropertyDescriptors(obj)
// { foo:
//    { value: 123,
//      writable: true,
//      enumerable: true,
//      configurable: true },
//   bar:
//    { get: [Function: get bar],
//      set: undefined,
//      enumerable: true,
//      configurable: true } }

該方法的用法:解決Object.assign()無法正確拷貝get屬性和set屬性的問題,配合Object.defineProperties()方法,就可以實現正確拷貝。配合Object.create()方法,將對象屬性克隆到一個新對象,這屬於淺拷貝。實現一個對象繼承另一個對象。實現 Mixin(混入)模式。

const target2={};
Object.defineProperties(target2,Object.getOwnPropertyDescriptors(source));

6、__proto__屬性,Object.setPrototypeOf(),Object.getPrototypeOf()

JavaScript語言的對象繼承是通過原型鏈實現的。ES6提供了更多原型對象的操作方法。

__proto__屬性(前後各兩個下劃線),用來讀取或設置當前對象的prototype對象。只有瀏覽器必須部署這個屬性,其他運行環境不一定需要部署,而且新的代碼最好認爲這個屬性是不存在的(最好不使用)。

const obj={
  method:function(){...}
};
obj.__proto__=someOtherObj;

Object.setPrototypeOf方法的作用與__proto__相同,用來設置一個對象的prototype對象,返回參數對象本身。它是ES6正式推薦的設置原型對象的方法。

Object.getPrototypeOf()用於讀取一個對象的原型對象。

如果(第一個)參數不是對象,會自動轉爲對象。如果(第一個)參數是undefined或null,無法轉爲對象,會報錯。

let proto={};
let obj={x:10};
Object.setPrototypeOf(obj,proto);

Object.getPrototypeOf(obj);

// 如果第一個參數不是對象,會自動轉爲對象。由於返回的還是第一個參數,所以這個操作不會產生任何效果。
Object.setPrototypeOf(1,{})===1 // true
Object.setPrototypeOf('foo',{})==='foo' // true
Object.setPrototypeOf(true,{})===true // true

7、Object.keys(),Object.values(),Object.entries()

Object.keys()方法,ES5,返回一個數組,成員是參數對象自身的(不含繼承的)所有可遍歷屬性的鍵名。

Object.values()方法,ES2017,返回一個數組,成員是參數對象自身的(不含繼承的)所有可遍歷屬性的鍵值。

Object.entries()方法,ES2017,返回一個數組,成員是參數對象自身的(不含繼承的)所有可遍歷屬性的鍵值對數組。

供for...of循環遍歷對象使用。

只返回對象自身的可遍歷屬性。會過濾屬性名爲Symbol值的屬性。

let {keys, values, entries}=Object;
let obj={a:1,b:2,c:3};
for(let key of keys(obj)){
  console.log(key); // 'a', 'b', 'c'
}
for(let value of values(obj)){
  console.log(value); // 1, 2, 3
}
for(let [key,value] of entries(obj)){
  console.log([key,value]); // ['a', 1], ['b', 2], ['c', 3]
}

8、Object.fromEntries()

Object.fromEntries()方法是Object.entries()的逆操作,用於將一個鍵值對數組轉爲對象。

Object.fromEntries([
  ['foo','bar'],
  ['baz',42]
]) // { foo: "bar", baz: 42 }

 

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