ES6语法——函数、数组、对象

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/yaocong1993/article/details/84932980

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 }

猜你喜欢

转载自blog.csdn.net/yaocong1993/article/details/84932980
今日推荐