2

最近因为参与小程序的开发,本身就支持ES6的语法,所以也是对ES6一次不错的实践机会,接下去会逐一的将学习过程中涉及的常用语法和注意事项罗列出来,加深印象。

函数参数默认值

ES6之前,不能直接给函数参数指定默认值,只能用变通的方法,例如:

function log(x, y) {
  y = y || 'word';
  console.log(x, y); 
}

log('hello'); // hello, word
log('hello', 'clearlove07'); // hello clearlove07

但上例中y对应的值如果是false的话,该赋值就不起作用了,例如y''空字符串:

log('hello', ''); // hello word

为了避免这个问题,通常还需要对y进行判断,看是否有赋值,如果没有再使用默认值。

ES6允许为函数的参数设置默认值,直接写在参数定义后面,例如:

function foo(x, y = 'word') {
  console.log(x, y);
}

log('hello'); // hello word
log('hello', 'clearlove07'); // hello clearlove07
log('hello', ''); // hello

除了简约,这种写法还有两个好处:

  1. 代码自说明,阅读代码的人通过参数就可以一目了然的清楚哪些参数是非必填的,不需要查看文档就可以了解。

  2. 可扩展性强,调用函数方哪怕不传这个参数值,也不会影响函数的执行。

注意事项:

  • 参数变量一旦指定默认值,函数体内不能再次声明

function foo(x = 3) {
  let x = 4; // error
  const x = 5; // error
}
  • 参数有默认值是,函数参数不能同名

// 不会报错
function foo(x, x, y) {
 ...
}

// 报错
function foo(x, x, y = 1) {
 ...
}
// SyntaxError: Duplicate parameter name not allowed in this context

与解构赋值默认值结合使用

参数默认值可以与解构赋值的默认值,结合起来使用。例如:

function fetch(url, { body = '', method = 'GET', headers = {} }){
  console.log(method);
}

fetch('https://segmentfault.com/u/clearlove07', {});
// 'GET'

fetch('https://segmentfault.com/u/clearlove07');
// Uncaught TypeError: Cannot match against 'undefined' or 'null'

上面代码中,fetch的第二个参数是个对象的话,那么就可以个这个对象赋值默认值,但这种写法不能省略第二个参数,否则会报错。那如果希望第二个参数也有默认值,则需要双重赋值:

function fetch(url, {body = '', method = 'GET', headers = {}} = {} ){
  console.log(method);
}

fetch('https://segmentfault.com/u/clearlove07'); // 'GET'

还有一个比较有意思的写法:

// 写法一
function foo1({x = 0, y = 0} = {}) {
  console.log([x, y]);
}

// 写法二
function foo2({x, y} = {x:0, y:0}) {
  console.log([x, y]);
}

// 函数没有参数情况下
foo1(); // [0, 0]
foo2(); // [0, 0]

// 参数x, y都有值的情况下
foo1({x: 1, y: 2}); // [1, 2]
foo2({x: 1, y: 2}); // [1, 2]

// x有值, y无值的情况下
foo1({x: 1}); // [1, 0]
foo2({x: 1}); // [1, undefined]

// x和y都无值的情况下
foo1({}); // [0, 0]
foo2({}); // [undefined, undefined]

foo1({z: 1}); // [0, 0] 
foo2({z: 1}); // [undefined, undefined]    

上面两种写法共同点是: 有默认值
区别在于:
写法一: 默认值是个空对象,但是设置了对象解构赋值的默认值
写法二: 默认值是个有具体属性的对象,但是没有设置对象解构赋值的默认值

参数默认值的位置

通常情况下,定义了函数的默认值的参数,应该放在参数列表的后面,这样比较清晰那些参数可以省略的。如果是在非尾部的参数设置默认值,实际上这个参数是没办法省略的。

function foo(x = 1, y) {
 return [x, y];
}

foo(); // [1, undefined]
foo(2); // [2, undefined]
foo(, 2); // Uncaught SyntaxError: Unexpected token ,
foo(undefined, 2); // [undefined, 2]

如果传入undefined,则会触发默认值的行为, 但是null则不会

function foo(x = 4, y) {
  return [x, y];
}

foo(undefined, null); // [4, null]

函数length属性

函数声明/函数表达式都具有length属性,这个属性返回函数参数长度。
指定函数参数默认之后, 函数的length属性值将不包含指定默认值的参数个数。

let foo = function(x, y = 1) {}
foo.length // 1

let foo = function(x = 1, y) {}
foo.length // 0

let foo = function(x, y = 1, z) {}
foo.length // 1

rest参数

用于获取函数剩余参数,替代arguments对象,形式为...args。rest变量是数组形式。

function add(...values) {
  let sum = 0;
  for(let val for values) {
    sum += val;
  }
  return sum 
}

add(1, 2, 3);  // 6

add()为求和函数,可以传递任意长度的参数进行求和。

使用rest参数替代arguments例子:

function numSort() {
  return Array.prototype.slice.call(arguments).sort();
}

// 替代方案
const numSort = (...nums) => nums.sort()

后面这种方法比较简洁,清晰明了。
rest参数中的变量代表一个数组,所以所有数组的方法都可以用于这个参数,例如:

function push(arr, ...nums) {
  nums.forEach(num => {
    arr.push(num);
  })
  return arr;
}

let arr = []
push(arr, 1, 2, 3, 4); // [1, 2, 3, 4]

注意:

  • rest参数后面不能有其他参数,即它为最后一个参数,否则会报错。

  • 函数的length属性, 不包含rest参数

箭头函数

如果 return 值就只有一行表达式,可以省去 return,默认表示该行是返回值,否则需要加一个大括号和 return。

let foo = (x) => x * x

// 等价于
let  foo = function(x) { return x * x; }

let foo = (x, y) => { 
  let total = x + y;  
  return total;     
} 

由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。

// 报错
let obj = () => { id: '1', age: 20 } 

// 正常
let obj = () => ({id: '1', age: 20})

箭头函数的一个用处是简化回调函数。

[1, 2, 3].map(function(x) {
  return x * x
}) 

// 简化
[1, 2, 3].map(x => x * x)

rest 参数与箭头函数结合的例子。

let concatArr = (num, ....items) => [num, items]
concatArr(1, 2, 3, 4) // [1, [2, 3, 4]]

使用注意事项:

  1. 函数体内this指向是定义时所在对象,而非执行时所在对象。
    普通函数的this是可变的,我们通常把函数归为两种状态: 定义时/执行时。函数中的this始终指向执行时所在的对象。比如全局函数执行时,this指向的是window。对象的方法执行时,this指向的是该对象,这就是函数this的可变性,但箭头函数中的this是固定不变的。看下面的例子:

    function obj() {
      setTimeout(() => console.log(this.id), 1000)
    }
    
    let id = 1
    obj.call({id: 2}); // 2
  2. 箭头函数里并没有存储this,将箭头函数转义成ES5:

    // ES6
    function obj() {
      setTimeout( () => console.log(this.id), 1000)
    }
    
    // ES5
    function obj() {
      var _this = this
      setTimeout(function() {
        console.log(_this.id)
      }, 1000)
    }

    由于箭头函数没有自己的this,所以当然也不能使用call()apply()bind()等方法来改变this的指向。例如:

    (function() {
      return [
        (() => this.id).bind({ id: 'inner' })()
      ];
    }).call({ id: 'outer' })
    // outer

    上面代码中箭头函数没有自己的this,所以bind无效。内部的this指向外部的this

  3. 在对象中使用箭头函数,this执行的是window

    let obj = {
      id: 1,
      foo: () =>{
        console.log(this.id)
      }
    }
    
    let id = 2
    obj.foo() // 2 
  4. 不可以当做构造函数,也就是不能使用new,否则会报错。

  5. 不可以使用arguments对象,该对象在函数内不存在。可以用rest参数代替。

  6. 不可以使用yield命令,因为箭头函数不能当做Generator函数。

箭头函数可以让this指向固化,这种特性有利于与封装回调函数。下面例子中,DOM时间的回调函数封装在一个对象里面。

let handler = {
  id: 'handler',
  init: function() {
    document.addEventListener('click', event => this.doSomething(event.type), false)
  },
  doSomething: function(type) {
    console.log('Handling ': type + ' id ' + this.id)
  } 
}

上面代码的init方法中,使用了箭头函数,这就导致了这个箭头函数里面的this总是指向handler对象。否则回调函数允许时,this.doSomething这一行就好报错,因为this指向的是window对象。
this指向的固化,并不是因为箭头函数内容有绑定this的机制,实际原因是因为箭头函数内部根本就没有this,而是一直指向外层代码块的this。正因为箭头函数没有this,所以也不能作为构造函数。


Clearlove
1.2k 声望53 粉丝

专注做好一件事