ES6类型扩展-函数扩展

形参默认值

JS中的函数,无论在函数定义时声明了多少形参,在函数调用时都可以传入任意数量的参数。

通常定义函数时会为可选的参数定义默认值,这样可以更方便的针对参数数量添加处理逻辑。

ES6为函数形参定义默认值很简单,直接在形参后面添加默认值即可

function foo(url, timeout = 3000, callback = function(){}) {
	// doSomething
}

触发默认值

除了不传参数可以触发默认值外,当参数值是undefined时也可以触发默认值,但是null没有这个效果。

function foo(url, timeout = 3000, callback = function(){}) {
	console.log(timeout)
}

foo('/test') // 3000
foo('/test', undefined) // 3000
foo('/test', null) // null
foo('/test', 5000) // 5000

注意: 每次调用函数,默认参数值都会重新计算

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

foo() // 2
n = 11
foo() // 12

length属性

形参指定默认值后,函数的length属性返回没有指定默认值的参数个数。

(function (a, b, c = 3) {}).length // 2

rest 参数也不会计入length属性

(function (a, b, ...args) {}).length // 2

如果默认值的参数不是尾参数,那么length属性不再计入后面的参数

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

arguments

ES6中,如果函数使用了参数默认值,arguments的对象行为同ES5严格模式下保持一致,即arguments对象中保存的是函数调用时传入的参数值。

// ES5 非严格模式
function foo(a) {
  console.log(arguments[0]) // 1
  a=2
  console.log(arguments[0]) // 2
}
foo(1)

// ES5 严格模式
function foo2(a) {
  'use strict'
  console.log(arguments[0]) // 1
  a=2
  console.log(arguments[0]) // 1
}
foo2(1)

// ES6默认参
function foo3(a = 1) {
  console.log(arguments[0]) // 1
  a=2
  console.log(arguments[0]) // 1
}
foo3(1)

// ES6默认参
function foo4(a = 1) {
  console.log(arguments.length) // 0
  a=2
  console.log(arguments[0]) // undefined
}
foo4()

arguments.length等于传入参数的数量,所以foo4函数的arguments.length等于0,arguments[0]等于undefined

默认参数表达式

默认参数值可以是一个函数调用,参数值等于函数执行的返回值

function test() {
  return 1
}

function foo(a, b=test()) {
  console.log(a + b)
}

foo(1,3) // 4
foo(1) // 2

注意: 当第一次调用foo函数时,由于传入了两个值,所以不会触发test函数的执行。

临时死区

function foo(a = b, b) {
  console.log(a + b)
}

foo(1,1) // 2
foo(undefined, 1) // b is not defined

在这个示例中,调用foo(undefined,1)函数,由于a初始化时b尚未初始化,所以会导致程序抛出错误,此时b尚处于临时死区中,所有引用临时死区中绑定的行为都会报错

不定参数

不定参数也称剩余参数或者rest参数,它的表示形式是在命名参数前加三个点...,这个参数在函数内部是一个数组,可以通过数组名访问里面的参数。

function foo(a,b,...c) {
  console.log(c)
}
foo(1,2,3,4,5) // [3, 4, 5]
function pick(object, ...keys) {
    let result = Object.create(null);
    for (let i = 0, len = keys.length; i < len; i++) {
        result[keys[i]] = object[keys[i]];
    }
    return result;
}


let person = {
  name: 'wmui',
  age: 10,
  sex: 'boy'
}
console.log(pick(person,'name','sex')) // {name: "wmui", sex: "boy"}

使用限制

  1. 每个函数最多声明一个不定参数,并且必须放到所有参数的末尾。
function foo(a,b,...c,d) {
  console.log(c)
}
foo(1,2,3,4,5) // Uncaught SyntaxError: Rest parameter must be last formal parameter
  1. 不定参数不能在对象字面量的setter属性中使用
let o = {
  set name(...val) {
   // doSomething
  }
}
// Uncaught SyntaxError: Setter function argument must not be a rest parameter

有这条限制也很好理解,因为本身对象字面量中setter的参数有且只能有一个,而不定参数的定义中,参数的数量可以无限多,所以在当前上下文中不允许使用不定参数

arguments

虽然有了不定参数,但是在ES6中arguments对象也是可以正常使用的,它并没有被不定参数取代。

function foo(a,b,...c) {
  console.log(c.length) // 3
  console.log(arguments.length) // 5
}
foo(1,2,3,4,5)

应用

由于不定参数是一个数组,所以数组特有的方法都可以应用于该变量

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

// 不定参数写法
let sortNumbers = (...args) => args.sort()

展开运算符

展开运算符和不定参数很相似。展开运算符可以把指定的数组,打散成各自独立的参数,然后传入函数;而不定参数是把各自独立的参数,整合成一个数组,然后在函数内部被访问。

let arr = [1,2,3]
console.log(...arr) // 1 2 3

展开运算符通常用于需要传入多个独立参数的函数,比如用Math.max()方法获取一组数的最大值。

Math.max()方法不能直接获取数组中的最大值,所有参数要以独立参数的形式传入

Math.max(3,2,1) // 3

// 利用apply()改变this,获取数组中元素最大值
let arr = [1,2,3]
Math.max.apply(Math, arr) // 3

虽然可以借助apply()方法实现获取数组中元素最大值,但是第一眼很难看懂代码的真正意图,而利用展开运算符就要好很多。

let arr = [1,2,3]
Math.max(...arr) // 3

展开运算符还可以和正常传入的参数混合使用,比如设置Math.max()返回值最小为0

let arr = [-1,-2,-3]
Math.max(...arr, 0) // 0

展开运算符可以简化使用数组给函数传参的编码过程,在大多数需要使用apply()方法的情况下展开运算符可能是一个更合适的方案

严格模式

从ES5开始,函数内部可以设置为严格模式。ES7对严格模式做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错

之所以这样规定,是因为函数内部的严格模式同时适用于函数参数和函数体,但函数执行的顺序是先执行函数参数再执行函数体,这样就会导致可能你的函数参数是不符合严格模式的,但要到执行函数体时才能被检测到,这无疑是不合理的。

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

严格模式下是不允许用前缀0代表八进制的,如果ES7不修改严格模式,那么JS引擎会先成功执行value = 070,然后进入函数体内部,发现需要用严格模式执行,这时才会报错

参数尾逗号

ES8允许函数的最后一个参数有尾逗号(trailing comma)。

function fn(
  param1,
  param2,
) { /* ... */ }

fn(
  'foo',
  'bar',
);

这样的规定使得函数参数与数组和对象的尾逗号规则保持一致

name属性

JS中有多种定义函数的方式,因而辨别函数就是一项具有挑战性的任务,ES6为所有函数新增了name属性,方便开发者们追踪函数调用记录

// 示例1  函数名字是声明时函数的名称
function foo(){}
console.log(foo.name) // foo

// 示例2  函数名字是匿名函数变量的名称
let foo2 = function(){}
console.log(foo2.name) // foo2

// 示例3  函数名字是函数表达式自身的名称
let foo3 = function test(){}
console.log(foo3.name) // test

// 示例4  函数名字带有bound前缀
let foo4 = function (){}
console.log(foo4.bind().name) // bound foo4

// 示例5  函数名字带有anonymous前缀
let foo5 = new Function()
console.log(foo5.name) // anonymous

// 示例6
let obj = {
  get getName() {
   return 'wmui'
  },
  set setName(v) {
   this.name = v
  },
  sayName() {
   return this.name
  }
}

let descriptor = Object.getOwnPropertyDescriptor(obj, 'getName');
let descriptor2 = Object.getOwnPropertyDescriptor(obj, 'setName');
// getter函数带有get前缀,setter函数带有set前缀
console.log(obj.sayName.name) // sayName
console.log(descriptor.get.name) // get getName
console.log(descriptor2.set.name) // set getName

判断调用

JS函数内部有两个内部方法:[[call]]和[[construct]]

当通过new关键字调用函数时,执行的是[[construct]]函数,它会创建一个新的实例对象,函数体执行时会把this绑定到实例上。

如果不使用new关键字调用函数,则执行[[call]]函数,直接执行代码中的函数体

注意: 不是所有函数都有[[construct]]方法,所以不是所有函数都可以通过new来调用。具有[[construct]]方法的函数被统称为构造函数

ES5判断函数调用

在ES5中判断一个函数是否通过new关键字调用,最常用的方法是使用instanceof操作符

function Person(name) {
  if(this instanceof Person) {
    this.name = name
  } else {
    throw new Error('You must use new with Person')
  }
}

let p1 = new Person('wmui')
let p2 = Person('wmui') // 报错

这种做法是正确的,但是并不完全靠得住,因为当使用call()或apply()方法强制把this绑定到Person实例上时,它是检测不会出来的。

function Person(name) {
  if(this instanceof Person) {
    this.name = name
  } else {
    throw new Error('You must use new with Person')
  }
}

let instance = new Person()
let p3 = Person.call(instance, 'wmui') // 不报错

ES6判断函数调用

ES6引入了new.target这个元属性解决判断函数是否通过new关键字调用的问题。元属性就是非对象的属性。当使用new关键字调用函数时,执行的是[[construct]]函数,new.target被赋值为新创建的实例对象;如果不通过new关键字调用,new.target的值为undefined。

function Person(name) {
  if(typeof new.target !== "undefined") {
    this.name = name
  } else {
    throw new Error('You must use new with Person')
  }
}

let p1 = new Person('wmui')
let p2 = Person('wmui') // 报错

let instance = new Person()
let p3 = Person.call(instance, 'wmui') // 报错

块级函数

在代码块中声明的函数就是块级函数。在ES6之前定义块级函数严格来说是一个语法错误,虽然浏览器也支持,但是表现行为不完全一致。而ES6会把函数视为一个块级声明,从而可以在代码块中声明和访问该函数。

if(true) {
  function foo() {
    console.log('hello')
  }
  foo()
}

foo()
// 'hello'
// 'hello'

非严格模式下,代码块内定义的函数,在代码块外仍然可以访问到,这是因为函数声明被提升到了外围函数或全局作用域的顶部

严格模式下,代码块内定义的函数,在代码块外访问不到,这是因为if语句代码块结束执行后,语句内的函数也不存在了。

'use strict';
if(true) {
  function foo() {
   console.log('hello')
  }
  foo()
}

foo()
// 'hello'
// Uncaught ReferenceError: foo is not defined

箭头函数

箭头函数(=>)是一种使用箭头定义函数的新语法,它与传统的JS函数有一些不同:

  1. 没有this、super、arguments、new.target
    箭头函数中的这些值由外围最近一层的非箭头函数决定

  2. 不能通过new关键字调用
    因为箭头函数没有[[construct]]方法

  3. 没有原型
    由于不能通过new关键字调用箭头函数,因而没有构建原型的需求,所以没有prototype属性

  4. 不能改变this绑定
    箭头函数内部的this值不可以被改变,在函数声明周期内始终保持一致

  5. 不支持arguments对象
    箭头函数没有arguments对象,必须通过命名参数和不定参数这两种形式来访问其参数

  6. 不支持重复的命名参数
    无论在严格还是非严格模式下,箭头函数都不支持重复的命名参数;而传统函数只有在严格模式下,才不能有重复的命名参数

语法

箭头函数有多种不同的表现形式,但都有参数、箭头和函数体组成。

let foo = num => num + 1;

// 有效等价于
let foo = function(num) {
  return num + 1;
}

如果只有一个参数,可以直接写参数名,然后是箭头,箭头右侧的表达式被求值后会立即返回。

如果有两个或两个以上参数,要在参数两侧加上一对小括号。

let foo = (num1, num2) => num1 + num2;

// 有效等价于
let foo = function(num1, num2) {
  return num1 + num2;
}

如果函数没有参数,要在声明的时候写一组没有内容的小括号

let foo = () => 1;

// 有效等价于
let foo = function() {
  return 1;
}

如果希望为函数编写由多个表达式组成的更传统的函数体,那么需要用花括号包裹函数体,并显式地定义一个返回值

let foo = (num1, num2) => {
  return num1 + num2;
}

// 有效等价于
let foo = function(num1, num2) {
  return num1 + num2;
}

this

箭头函数中没有this绑定,必须通过查找作用城链来决定其值。

如果箭头函数被非箭头函数包含,则this绑定的是最近一层非箭头函数的this;否则,this的值会被设置为undefined

如果对象的方法中包含了另外一个函数,并且这个函数引用了this,为了不让this指向window对象,我们通常会使用bind()显示的为函数绑定this值

let obj = {
  name: 'wmui',
  init: function() {
   document.addEventListener('click', (function(e){
     // 方法内部的函数引用了this,使用bind()改变this指向
     this.test(e.type)
   }).bind(this), false)
  },
  test: function(type) {
   console.log(this.name,type)
  }
}

obj.init() // wmui click

如果用箭头函数重写上面的示例,不仅使代码更精简,而且更加容易理解

let obj = {
  name: 'wmui',
  init: function() {
   document.addEventListener('click', (e) => {
     this.test(e.type)
   }, false)
  },
  test: function(type) {
   console.log(this.name,type)
  }
}

obj.init() // wmui click

辨识方法

尽管箭头函数与传统函数的语法不同,但它同样可以被识别出来

let foo = (num1, num2) => num1 - num2;
console.log(typeof foo); // "function"
console.log(foo instanceof Function); // true

箭头函数上可以调用call()、apply()及bind()方法,但箭头函数的this值不会受这些方法的影响

let foo = (num1, num2) => num1 + num2;
console.log(foo.call(null, 1, 2)); // 3
console.log(foo.apply(null, [1, 2])); // 3

let boundFoo = foo.bind(null, 1, 2);
console.log(boundFoo()); // 3

函数柯里化

柯里化是一种可以把多参函数转变成单参函数,并且调用后返回一个新函数的技术,这个新函数可以接收剩余参数而且有返回结果

使用ES5的语法写一个柯里化函数

function foo(x) {
  return function (y) {
   return y + x
  }
}
foo(1)(2) // 3

使用ES6的语法写一个柯里化函数

let foo (x) => (y) => y + x;

foo(1)(2) // 3

一般来说,出现连续地箭头函数调用的情况,就是在使用函数柯里化的技术

尾调用优化

尾调用是指函数作为另一个函数的最后一条语句被调用。

关于函数的调用这里简单说一下,函数调用会在内存中形成一个调用记录,称作“调用帧”(call frame),用来保存调用位置和内部变量等信息。如果在函数A的内部调用了函数B,那么在A的调用帧上方就会形成一个B的调用帧,等到函数B运行结束并且将结果返回到A,B的调用帧才会消失。同理,如果函数B的内部调用了函数C,那么在B的调用帧上方会有一个C的调用帧,以此类推,所有的调用帧就会形成一个调用栈(call stack)

尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,于是JS引擎就可以在背后对尾调用进行优化。如果所有函数都是尾调用,将会大大节省内存开销。

ES6缩减了严格模式下尾调用栈的大小,如果满足以下三个条件,尾调用不再创建新的栈帧,并且可以被JS引擎自动优化:

  1. 尾调用不是闭包
  2. 尾调用是函数内部的最后一条语句
  3. 尾调用的结果作为返回值被返回
'use strict';
function foo() {
  // 被优化
  return foo2()
}

下面这几种情况不会被优化:

// 示例1  缺少return语句
'use strict';
function foo() {
  foo2()
}

// 示例2  尾调用返回后执行其他操作
'use strict';
function foo() {
  return foo2() + 1
}

// 示例3  不是尾调用
'use strict';
function foo() {
  let t = foo2()
  return t
}

// 示例4  尾调用是闭包
'use strict';
function foo() {
  let num = 1
  let t = () => num
  return t()
}

应用

尾调用优化常被用于递归函数

应用一:计算阶乘

function factorial(n) {
  if (n <= 1) {
      return 1;
    } else {
      // 未被优化
      return n * factorial(n - 1);
  }
}
function factorial(n, p = 1) {
  if (n <= 1) {
    return 1 * p;
  } else {
    let result = n * p;
    // 被优化
    return factorial(n - 1, result);
  }
}

应用二:计算Fibonacci数列

function Fibonacci (n) {
  if ( n <= 1 ) {return 1};
  return Fibonacci(n - 1) + Fibonacci(n - 2);
}

Fibonacci(10) // 89
Fibonacci(100) // 堆栈溢出
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
  if( n <= 1 ) {return ac2};
  return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}

Fibonacci2(100) // 573147844013817200000

ES6 明确规定,所有 ECMAScript 的实现,都必须部署“尾调用优化”。这就是说,ES6 中只要使用尾递归,就不会发生栈溢出,相对节省内存

posted @ 2021-09-29 11:37  wmui  阅读(43)  评论(0编辑  收藏  举报