# 6、Function 的扩展


# 一、函数参数的默认值

1. 基本用法

ES6 之前不能指定函数参数默认值,只能使用变通的方式

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

当第三个参数传空字符串时,仍然改为了默认值,所以为避免还需要判断是否赋值了

function log(x, y) {
  if (typeof y === "undefined") {
    y = y || "Word";
  }
  console.log(x, y);
}

现在 ES6 支持直接在参数上面赋默认值,并且时默认声明,在函数体内不能用 let 或 const 再次声明。并且注意用参数默认值时,不能有同名参数,否则报错,如果参数默认值是表达式,每次都要重新计算表达式

let x = 99;
function foo(p = x + 1) {
  console.log(p);
}

foo(); // 100

x = 100;
foo(); // 101

2. 作用域

设置参数默认值后,函数声明进行初始化的时候,参数会形成单独的作用域,初始化结束后,作用域就会消失。

var x = 1;
function f(x, y = x) {
  console.log(y);
}

f(2); //2

参数 x 指向了第一个 x,形成了单独的作用域,所以指向了参数 x 而不是全局 x,所以输出 2,如果没有参数传入,x 为 undefined,输出 undefined。

let x = 1;
function f(y = x) {
  let x = 2;
  console.log(y);
}
f(); // 1

以上参数 y=x 形成了单独作用域,在这个作用域中,变量 x 本身没有定义,所以指向外层的全局变量 x。函数调用时,函数体内的局部变量 x 影响不到默认值 x,如果全局 x 不存在,则会报错

var x = 1;
function foo(x = x) {
  // 实际执行的是let x =x 暂时性死区,会报x未定义
  // ...
}
foo(); // ReferenceError: x is not defined

# 二、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 参数代替 arguments 变量的例子。

// arguments变量的写法
function sortNumbers() {
  return Array.prototype.slice.call(arguments).sort();
}

// const sortNumbers = (...numbers) => numbers.sort();

因为 arguments 对象不是数组,而是一个类似数组的对象。所以为了使用数组的方法,必须使用 Array.prototype.slice.call 先讲其转为数组。rest 参数是一个真正的数组,可以直接使用数组的方法。

注意:

  • rest 参数后面不能再跟参数
  • 函数 length 属性,不包括 rest 参数
(function(a, ...b) {}.length); // 1

# 三、严格模式

ES5 开始函数内部可以设为严格模式。

function doSomething(a, b) {
  "use strict";
  // code
}

ES2016 开始,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显示设定为严格模式,否则报错,原因是函数先执行函数参数再执行函数体,然后才能知道参数是否按严格模式执行。

function doSomething(a, b = a) {
  // 报错
  "use strict";
}
function doSomething({ a, b }) {
  // 报错
  "use strict";
}
function doSomething(...a) {
  // 报错
  "use strict";
}

解决方式

  • 设定全局性的严格模式

  • 第二种是函数包在无参数的立即执行函数里面。

# 四、name 属性

函数 name 属性,返回该函数的函数名

function foo(){}
foo.name. // 'foo'

ES5 和 ES6 的区别

var f = function() {};
// ES5
f.name; // ""
// ES6
f.nsme; // 'f'

# 五、箭头函数

箭头函数不会在函数体内重新定义 this 的值,这使得在回调中的行为更容易预测,并且避免了 this 在回调中潜存的 bug

ES6 允许使用箭头定义函数(=>)

var f = v => v;
// 等同于
var f = function(v) {
  return v;
};

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

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

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

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

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

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

简化回调函数

// 正常写法
[123].map(function(x){
    return x*X
})
var result = values.sort(function (a,b){
   return a-b
})
// 箭头函数
[1,2,3].map(x=>x*x);
var result = values.sort((a,b)=>a-b);

rest 参数与箭头函数结合

const numbers = (...nums) => nums;
numbers(1, 2, 3, 4, 5);
// [1,2,3,4,5]

注意

(1)函数体内的 this 对象,就是定义时所在的对象,而不是运行时所在的对象。this 对象指向是固定的。并不是因为内部绑定了 this,是因为箭头函数根本没有自己的 this,导致内部的 this 是环境

(2)不可以当作构造函数,不能 new 实例

(3)不能用 arguments 对象,可以用 rest 代替

(4)不可以使用 yield 命令,不能用做 Generator 函数

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

箭头函数可以让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);
  }
};

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

// ES6
function foo() {
  setTimeout(() => {
    console.log("id", this.id);
  }, 100);
}

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

箭头函数内没有 this,所以在对象方法中使用 this 需要非常小心,箭头函数绑定 this,很大程度解决了这个困扰。

不适合用箭头函数的三种场景

  • (1)定义对象方法并使用内部 this
const cat = {
  lives: 9,
  jumps: () => {
    this.lives--;
  }
};

cat.jumps()方法是一个箭头函数,如果普通函数调用时 this 指向 cat,但是箭头函数不会有预期效果,因为对象不构成单独的作用域,导致 jumps 箭头函数定义时的作用域就是全局作用域。

  • (2)需要动态使用 this 时
var button = document.getElementById("press");
button.addEventListener("clice", () => {
  this.classList.toggle("on");
});

上面代码运行报错,因为如果普通函数 this 指向的就是点击按钮的对象,但是此时箭头函数里的 this 是全局对象

  • (3)函数很复杂,内部大浪读写操作,不单纯为了值时,用普通函数可以提高代码的可读性。
  • 性能 着想,避免在 render 中使用大量箭头函数

2020.3.9