目录
ES6对Function做一些扩展,引入很多新特征,下面来逐个介绍
1.函数参数默认值
函数定义时,可以指定默认参数,在未传入参数或参数为undefined
时,会使用默认的参数
function Obj(id = 107, name = 'jack'){
this.id = id;
this.name = 'jack'
}
const obj = new Obj() // {id: 107, name: 'jack'}
function add(x, y = 1){
return x + y
}
add(1) // 2
add(1, 3) // 4
但是要小心,参数变量是默认声明的,不能用let或const再次声明,因为其中调用时会发生隐式解构
,即let [arg1,...,arg2] = [].prototype.slice.call(arguments,0)
function fn(x = 5){
// 隐式解构:let [x=5] = [].prototype.slice.call(arguments,0)
let x = 1 // error
}
// 下面这个可以
function fn(x=7) {
if (x) {
let x = 1 // 不在一个语句块里
console.log(x) // 1
}
console.log(x) // 7
}
惰性求值
let x = 99
function foo(p = x + 1){
alert(p)
}
foo() // 100
x = 100
foo() // 101
指定了默认值后,函数的length
属性会失真,因为length属性含义为函数预期传入的参数个数
(function (a, b, c = 5) {}).length // 2
(function (a, b = 1, c) {}).length // 1 只记录默认值前面的
2.rest参数
rest参数形式为...args
,会将获取函数的多余参数并将其放进数组中
function(a,...b){}
// 等价于
function(){
let args = [...arguments]
let a = args.shift()
let b = args
}
function add(n, ...values) {
let sum = 0
for (let val of values) {
sum += val
}
return n * sum
}
add(10, 1, 2, 3) // 60
3.name属性
函数的name属性返回该函数的函数名
function fn(){ ... }
fn.name // 'fn'
var bar = function foo(){ ... }
bar.name // 'foo'
// 特殊的
var fn = function(){ ... }
fn.name // 'fn'
// bind()返回的函数会加上‘bound’前缀
function fn(){ ... }
fn.bind({}).name // 'bound fn'
☆4.箭头函数
4.1 箭头函数特征
箭头函数是ES6对函数进行的最大扩展
var fn = n => n+1
// 等价于
var fn = function(n){
return n+1
}
var fn = (a,b)=>a+b
var fn = (a,b)=>{
return a+b
}
若直接返回一个对象有必要时需要加个括号
var getId = id =>({id: id,name:'jack'})
若箭头函数只有一行语句,且不需要返回值,可以采用void(慎用,会被同事特殊关怀滴~)
var fn = arr => void arr.push(1)
var arr = [3,2]
fn(arr) // [3,2,1]
4.2 箭头函数和参数解构结合
var obj ={
id: 107,
name: 'jack',
age: 18
}
var getIdAndName = ({id, name}) =>name + ':'+ id
getIdAndName(obj) // jack:107
4.3 箭头函数与rest参数结合
let numbersToArr = ...num => num
numbersToArr(1,2,3) // [1,2,3]
let fn = (head,...end) =>[head, end]
fn(1,2,3) //[1,[2,3]]
☆4.4 箭头函数注意点
作用域里无this,this实际是来自于作用域链访问到的非箭头函数作用域中的this
不可以new
不存在arguments对象
不可以yield
详细说明一下箭头函数的this
// 用Babel编译ES6代码来解释this
// ES6
function fn() {
setTimeout(() => {
console.log('id:', this.id)
}, 100)
}
// ES5
function fn() {
var _this = this
setTimeout(function () {
console.log('id:', _this.id)
}, 100)
}
由于箭头函数作用域中没有自己的this,故call、apply、bind
对其无效
function fn() {
return () => {
return () => {
return () => {
console.log('id:', this.id)
};
};
};
}
var f = fn.call({id: 1}) // f为return 的箭头函数,这时候this为上文所说的_this,故箭头函数没有真正意义上的'this',也意味着bind、call、apply对它无效!
var t1 = f.call({id: 2})()() // id: 1
var t2 = f().call({id: 3})() // id: 1
var t3 = f()().call({id: 4}) // id: 1
再举个栗子
function fn() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
var id = 21
fn() // 21
fn.call({ id: 42 }) // 42
function Timer() {
this.s1 = 0
this.s2 = 0
// 箭头函数
setInterval(() => this.s1++, 1000)
// 普通函数
setInterval(function(){this.s2++}, 1000)
}
var timer = new Timer()
setTimeout(() => console.log(timer.s1), 3100) // 3 this绑定timer
setTimeout(() => console.log(timer.s2), 3100) // 0 this帮定window
下面的例子能帮助你很好的理解箭头函数里的this
var obj = {
name: 'jack',
sayName:()=>alert(this.name)
}
obj.sayName() // undefined, WTF???
// 冷静详细分析一下
// 箭头函数的this实际上是__this Babel转译
obj = {
__this:this // 这里的this指向的是window
sayName:function(){
alert(__this.name)
}
}
// 在全局执行obj.sayName()
// 1.创建全局上下文gContext
gContext = {
VO:{
obj:{ ... }, // 因为obj是gContext中访问的,this是顺作用域链查询为window
this: window
}
Scope:[VO],
}
// 2.objContext
objContext = {
VO:{
__this: this,
name: 'jack',
sayName:function(){ alert(_this.name) }
},
Scope: [gContext.VO,VO] // 在全局时Scope加入gContext.VO
}
// 3.sayNameReference
sayNameReference = {
base: obj,
name: sayNameReference
}
sayName.[[scope]] = objContext.Scope
// 4.sayName执行时创建sayNameContext
sayNameContext = {
VO:{
arguments:{
length:0
}
},
Scope: [gContext.VO,objContext.VO,VO]
}
// 卧槽,终于分析完了,这时候函数执行alert(__this.name),就会查找_this
// 就会顺着sayNameContext.Scope作用域链查询,由于作用域链是栈结构,后进先出,故从数组屁股开始找
// sayNameContext.VO --> objContext.VO 找到_this,而_this保存的又是this,继续查找
// --> gContext.VO 找到this为window,TMD,终于给老子找到了!!!
考你一下有没有认真听课
var button = document.getElementById('press')
button.addEventListener('click', () => {
this.classList.toggle('on')
});
// babel转译
button = {
__this: this, // 这里的_this指向什么?
onClick:function(){
__this.classList.toggle('on')
}
}
5.尾调用优化
尾调用(tail call)
是函数式编程的一个重要概念,是指函数的最后一步
是调用另一个函数
function fn(x){
return g(x)
}
函数调用会在内存形成一个调用记录
,又称调用帧(call frame)
,保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧,等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个调用栈(call stack)
。
而由于尾调用是函数的最后一步,故不需保留外层函数的调用帧,直接访问内层函数的调用帧即可。
这就是尾调用优化,即必要时(闭包除外),删除外部函数的调用帧,只保留内层函数的调用帧。
function f() {
let m = 1
let n = 2
return g(m + n)
}
f()
// 等同于
function f() {
return g(3)
}
f()
// 等同于
g(3)
5.1 尾递归
在尾部调用自身即为尾递归
,由于递归需要同时保存成千上百个调用帧,很耗费内存,很容易栈溢出(stack overflow)
,而尾递归由于只会存在下一个内部函数的调用帧,所以不会栈溢出
function factorial(n) {
if (n === 1) return 1
return n * factorial(n - 1) // 每次都还保留n这个变量,故调用帧保存下来
}
factorial(5) // 120
最多(即递归到最后一次函数执行时,调用帧叠加最多)需要保存n个调用帧,因为外层调用帧不能删除优化,还要用到n这个外部变量,空间复杂度O(n),现在做尾调优化
function factorial(n, total) {
if (n === 1) return total
return factorial(n - 1, n * total) // 不保留其他变量,调用帧在尾调开始时就结束了
}
factorial(5, 1) // 120
这时候,每次递归执行时只需保存当前的调用帧即可,外部函数的调用帧已经优化删除了,空间复杂度O(1)
5.2 尾递归优化改写
头发还多的话去找找看吧