Skip to content

Latest commit

 

History

History
354 lines (247 loc) · 11.7 KB

7-函数的扩展.md

File metadata and controls

354 lines (247 loc) · 11.7 KB

1.函数参数的默认值
在ES6之前,不能直接为函数的参数指定默认值,只能采用变通的方法。

  function log(x, y) {
    y = y || 'World';
    console.log(x, y);
  }
  
  log('Hello') // Hello World
  log('Hello', 'China') // Hello China
  log('Hello', '') // Hello World

上面代码检查函数log的参数y有没有赋值,如果没有,则指定默认值为World。

这种写法的缺点在于,如果参数y赋值了,但是对应的布尔值为false,则该赋值不起作用。

就像上面代码的最后一行,参数y等于空字符,结果被改为默认值。

为了避免这个问题,通常需要先判断一下参数y是否被赋值,如果没有,再等于默认值。

if (typeof y === 'undefined') {
  y = 'World';
}

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

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

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello

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

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

foo({}) // undefined, 5
foo({x: 1}) // 1, 5
foo({x: 1, y: 2}) // 1, 2
foo() // TypeError: Cannot read property 'x' of undefined

参数默认值的位置
通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。

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

f() // [1, undefined]
f(2) // [2, undefined])
f(, 1) // 报错
f(undefined, 1) // [1, 1]

// 例二
function f(x, y = 5, z) {
  return [x, y, z];
}

f() // [undefined, 5, undefined]
f(1) // [1, 5, undefined]
f(1, ,2) // 报错
f(1, undefined, 2) // [1, 5, 2]

rest参数

ES6引入rest参数(形式为“...变量名”),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

function add(...values) {
  let sum = 0;

  for (var val of values) {
    sum += val;
  }

  return sum;
}

add(2, 5, 3) // 10

注意,rest参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。

        // 报错
        function f(a, ...b, c) {
          // ...
        }

函数的length属性,不包括rest参数。

  (function(a) {}).length  // 1
  (function(...a) {}).length  // 0
  (function(a, ...b) {}).length  // 1    

扩展运算符 ...

扩展运算符(spread)是三个点(...)。它好比rest参数的逆运算,将一个数组转为用逗号分隔的参数序列。

  console.log(...[1, 2, 3])
  // 1 2 3
  
  console.log(1, ...[2, 3, 4], 5)
  // 1 2 3 4 5
  
  [...document.querySelectorAll('div')]
  // [<div>, <div>, <div>]

函数的name属性

函数的name属性,返回该函数的函数名。
  function foo() {}
  foo.name // "foo"

这个属性早就被浏览器广泛支持,但是直到ES6,才将其写入了标准。

需要注意的是,ES6对这个属性的行为做出了一些修改。如果将一个匿名函数赋值给一个变量,ES5的name属性,会返回空字符串,而ES6的name属性会返回实际的函数名。

  var func1 = function () {};
  
  // ES5
  func1.name // ""
  
  // ES6
  func1.name // "func1"

上面代码中,变量func1等于一个匿名函数,ES5和ES6的name属性返回的值不一样。

如果将一个具名函数赋值给一个变量,则ES5和ES6的name属性都返回这个具名函数原本的名字。

  const bar = function baz() {};
  
  // ES5
  bar.name // "baz"
  
  // ES6
  bar.name // "baz"

Function构造函数返回的函数实例,name属性的值为“anonymous”。

  (new Function).name // "anonymous"

bind返回的函数,name属性值会加上“bound ”前缀。

  function foo() {};
  foo.bind({}).name // "bound foo"
  
  (function(){}).bind({}).name // "bound "
箭头函数
基本用法
ES6允许使用“箭头”(=>)定义函数。
var f = v => v;

上面的箭头函数等同于:

  var f = function(v) {
    return v;
  };

如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。

  var f = () => 5;
  // 等同于
  var f = function () { return 5 };
  
  var sum = (num1, num2) => num1 + num2;
  // 等同于
  var sum = function(num1, num2) {
    return num1 + num2;
  };

如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。

  var sum = (num1, num2) => { return num1 + num2; }

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

  var getTempItem = id => ({ id: id, name: "Temp" });

箭头函数可以与变量解构结合使用。

  const full = ({ first, last }) => first + ' ' + last;
  
  // 等同于
  function full(person) {
    return person.first + ' ' + person.last;
  }

箭头函数使得表达更加简洁。

  const isEven = n => n % 2 == 0;
  const square = n => n * n;

上面代码只用了两行,就定义了两个简单的工具函数。如果不用箭头函数,可能就要占用多行,而且还不如现在这样写醒目。

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

  // 正常函数写法
  [1,2,3].map(function (x) {
    return x * x;
  });
  
  // 箭头函数写法
  [1,2,3].map(x => x * x);

另一个例子是

  // 正常函数写法
  var values=['1','3','2','9','6','5'];
  var result = values.sort(function (a, b) {
    return a - b;
  });
  
  // 箭头函数写法
  var values=['1','3','2','9','6','5'];
  var result = values.sort((a, b) => a - b);
  
  下面是rest参数与箭头函数结合的例子。
  
  const numbers = (...nums) => nums;
  
  numbers(1, 2, 3, 4, 5)
  // [1,2,3,4,5]
  
  const headAndTail = (head, ...tail) => [head, tail];
  
  headAndTail(1, 2, 3, 4, 5)
  // [1,[2,3,4,5]]

使用注意点 箭头函数有几个使用注意点。

(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用Rest参数代替。

(4)不可以使用yield命令,因此箭头函数不能用作Generator函数。

上面四点中,第一点尤其值得注意。this对象的指向是可变的,但是在箭头函数中,它是固定的。

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

上面代码中,setTimeout的参数是一个箭头函数,这个箭头函数的定义生效是在foo函数生成时,而它的真正执行要等到100毫秒后。如果是普通函数,执行时this应该指向全局对象window,这时应该输出21。但是,箭头函数导致this总是指向函数定义生效时所在的对象(本例是{id: 42}),所以输出的是42。

箭头函数可以让setTimeout里面的this,绑定定义时所在的作用域,而不是指向运行时所在的作用域。下面是另一个例子。

  function Timer() {
    this.s1 = 0;
    this.s2 = 0;
    // 箭头函数
    setInterval(() => this.s1++, 1000);
    // 普通函数
    setInterval(function () {
      this.s2++;
    }, 1000);
  }
  
  var timer = new Timer();
  
  setTimeout(() => console.log('s1: ', timer.s1), 3100);
  setTimeout(() => console.log('s2: ', timer.s2), 3100);
  // s1: 3
  // s2: 0

上面代码中,Timer函数内部设置了两个定时器,分别使用了箭头函数和普通函数。前者的this绑定定义时所在的作用域(即Timer函数),后者的this指向运行时所在的作用域(即全局对象)。所以,3100毫秒之后,timer.s1被更新了3次,而timer.s2一次都没更新。

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

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

上面代码的init方法中,使用了箭头函数,这导致这个箭头函数里面的this,总是指向handler对象。否则,回调函数运行时,this.doSomething这一行会报错,因为此时this指向document对象。

this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。

所以,箭头函数转成ES5的代码如下。

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

上面代码中,转换后的ES5版本清楚地说明了,箭头函数里面根本没有自己的this,而是引用外层的this。

请问下面的代码之中有几个this?

  function foo() {
    return () => {
      return () => {
        return () => {
          console.log('id:', this.id);
        };
      };
    };
  }
  
  var f = foo.call({id: 1});
  
  var t1 = f.call({id: 2})()(); // id: 1
  var t2 = f().call({id: 3})(); // id: 1
  var t3 = f()().call({id: 4}); // id: 1

上面代码之中,只有一个this,就是函数foo的this,所以t1、t2、t3都输出同样的结果。因为所有的内层函数都是箭头函数,都没有自己的this,它们的this其实都是最外层foo函数的this。

除了this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments、super、new.target。

  function foo() {
    setTimeout(() => {
      console.log('args:', arguments);
    }, 100);
  }
  
  foo(2, 4, 6, 8)
  // args: [2, 4, 6, 8]

上面代码中,箭头函数内部的变量arguments,其实是函数foo的arguments变量。

另外,由于箭头函数没有自己的this,所以当然也就不能用call()、apply()、bind()这些方法去改变this的指向。

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

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

长期以来,JavaScript语言的this对象一直是一个令人头痛的问题,在对象方法中使用this,必须非常小心。箭头函数”绑定”this,很大程度上解决了这个困扰。