ECMAScript 6 学习笔记(六):函数的扩展

栏目: JavaScript · 发布时间: 6年前

内容简介:ES6 之前,想要为函数指定默认值只能采用如下的办法:ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。参数默认值可以与解构赋值的默认值,结合起来使用。

函数参数的默认值

ES6 之前,想要为函数指定默认值只能采用如下的办法:

function f(a, b) {
  a = a || 0; // 方法1
  b = typeof b == "undefined" ? 0 : b; // 方法2
  return a + b;
}

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 = 0}) {
  console.log(x, y);
}

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

上面代码没有使用函数参数的默认值,如果函数 foo 调用时没提供参数,变量 x 和 y 就不会生成,从而报错。通过提供函数参数的默认值,就可以避免这种情况:

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

foo() // 0 0
foo({}) // 0 0
foo({x: 1}) // 1 0
foo({x: 1, y: 2}) // 1 2

但是要注意,使用解构赋值默认值时要注意默认值的位置:

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

foo() // 0 0
foo({}) // undefined undefined
foo({x: 1}) // 1 undefined
foo({x: 1, y: 2}) // 1 2

如上所示,第一种情况下,当函数没有指定参数时,默认值是空对象“{}”,参数为“{}”时,默认值就是“{x = 0, y = 0}”;而第二种情况,函数没有指定参数时,默认值是对象“{x = 0, y = 0}”,参数为“{}”时,默认值是“{x, y}”,相当于没有指定默认值,因此会出现 undefined。

除此之外,还需注意,尽量把有默认值的参数作为函数的尾参数,因为如果非尾部的参数设置默认值,这个参数是没法省略的:

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

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

函数的 length 属性

指定了默认值以后,函数的 length 属性,将返回没有指定默认值的参数个数,即函数至少需要传入的参数个数。因此某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。

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

作用域

一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。

var x = 1;

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

f(2) // 2

如果变量 x 本身没有定义,则指向外层的全局变量 x。函数调用时,函数体内部的局部变量 x 影响不到默认值变量 x。如果此时,全局变量 x 不存在,就会报错。

let x = 1;

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

f() // 1

rest 参数

ES6 引入 rest 参数(形式为…变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。需要注意的是,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。同时,函数的 length 属性,不包括 rest 参数。

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

add(2, 5, 3) // 10

一个例子:

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

// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();

严格模式

从 ES5 开始,函数内部可以设定为严格模式。ES2016 做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。这样规定的原因是,函数内部的严格模式,同时适用于函数体和函数参数。但是,函数执行的时候,先执行函数参数,然后再执行函数体。这样就有一个不合理的地方,只有从函数体之中,才能知道参数是否应该以严格模式执行,但是参数却应该先于函数体执行。

两种方法可以规避这种限制。第一种是设定全局性的严格模式,这是合法的。第二种是把函数包在一个无参数的立即执行函数里面。

const doSomething = (function () {
  'use strict';
  return function(value = 42) {
    return value;
  };
}());

name 属性

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

var f = function () {};

// ES5
f.name // ""

// ES6
f.name // "f"

Function 构造函数返回的函数实例,name 属性的值为 anonymous。bind 返回的函数,name 属性值会加上 bound 前缀。

(new Function).name // "anonymous"

(function(){}).bind({}).name // "bound "

箭头函数

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; }

let getTempItem = id => { id: id, name: "Temp" };   // 报错
let getTempItem = id => ({ id: id, name: "Temp" }); // 不报错

如果箭头函数只有一行语句,且不需要返回值,可以采用下面的写法。

let fn = () => void doesNotReturn();

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

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

注意点

  • 函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象。
  • 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
  • 不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
  • 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

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

箭头函数的这种特性很有利于封装回调函数。

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,所以也就不能用作构造函数。所以,箭头函数转成 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);
}

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

不适用场合

第一个场合是定义函数的方法,且该方法内部包括 this。

const cat = {
  lives: 9,
  jumps: () => {
    this.lives--; // 这里的 this 指向全局对象
  }
}

第二个场合是需要动态 this 的时候,也不应使用箭头函数。

var button = document.getElementById('press');
button.addEventListener('click', () => {
  this.classList.toggle('on'); // 这里的 this 指向全局对象
});

双冒号运算符

箭头函数可以绑定 this 对象,大大减少了显式绑定 this 对象的写法(call、apply、bind)。但是,箭头函数并不适用于所有场合,所以现在有一个提案,提出了“函数绑定”(function bind)运算符,用来取代call、apply、bind调用。函数绑定运算符是并排的两个冒号(::),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即this对象),绑定到右边的函数上面。

foo::bar;
// 等同于
bar.bind(foo);

foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);

const hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn(obj, key) {
  return obj::hasOwnProperty(key);
}

如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面。

var method = obj::obj.foo;
// 等同于
var method = ::obj.foo;

let log = ::console.log;
// 等同于
var log = console.log.bind(console);

如果双冒号运算符的运算结果,还是一个对象,就可以采用链式写法。

import { map, takeWhile, forEach } from "iterlib";

getPlayers()
::map(x => x.character())
::takeWhile(x => x.strength > 100)
::forEach(x => console.log(x));

尾调用优化

尾调用(Tail Call)是函数式编程的一个重要概念,指某个函数的最后一步是调用另一个函数。“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。

function f() {
  let m = 1;
  let n = 2;
  return g(m + n);
}
f();

// 等同于
function f() {
  return g(3);
}
f();

// 等同于
g(3);

对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。下面是一个阶乘函数的普通写法和尾递归写法:

function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

factorial(5) // 空间复杂度 O(n)
function factorial(n, total) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5, 1) // 空间复杂度 O(1)

递归函数的改写

尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。

注意:ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。

  • func.arguments:返回调用时函数的参数。
  • func.caller:返回调用当前函数的那个函数。

方法一是在尾递归函数之外,再提供一个正常形式的函数:

function tailFactorial(n, total) {
  if (n === 1) return total;
  return tailFactorial(n - 1, n * total);
}

function factorial(n) {
  return tailFactorial(n, 1);
}

factorial(5) // 120

函数式编程有一个概念,叫做柯里化(Currying),意思是将多参数的函数转换成单参数的形式。这里也可以使用柯里化。

function currying(fn, n) {
  return function (m) {
    return fn.call(this, m, n);
  };
}

function tailFactorial(n, total) {
  if (n === 1) return total;
  return tailFactorial(n - 1, n * total);
}

const factorial = currying(tailFactorial, 1);

factorial(5) // 120

第二种方法是采用 ES6 的函数默认值:

function factorial(n, total = 1) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5) // 120

尾递归优化的实现

递归之所以需要优化,原因是调用栈太多,造成溢出,那么只要减少调用栈,就不会溢出。怎么做可以减少调用栈呢?就是采用“循环”换掉“递归”。

function tco(f) {
  var value;
  var active = false;
  var accumulated = [];

  return function accumulator() {
    accumulated.push(arguments);
    if (!active) {
      active = true;
      while (accumulated.length) {
        value = f.apply(this, accumulated.shift());
      }
      active = false;
      return value;
    }
  };
}

var sum = tco(function(x, y) {
  if (y > 0) {
    return sum(x + 1, y - 1)
  }
  else {
    return x
  }
});

sum(1, 100000)

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

PHP项目开发全程实录

PHP项目开发全程实录

清华大学出版社 / 2008 / 56.00元

《软件项目开发全程实录丛书•PHP项目开发全程实录:DVD17小时语音视频讲解(附光盘1张)》主要特色: (1)12-32小时全程语音同步视频讲解,目前市场上唯一的“全程语音视频教学”的案例类 图书,培训数千元容,尽在一盘中! (2)10套“应用系统”并公开全部“源代码”,誓将案例学习进行到底! (3)丛书总计80个应用系统300个应用模块。 (4)含5000页SQL se......一起来看看 《PHP项目开发全程实录》 这本书的介绍吧!

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具