函数声明,函数表达式

声明

1.直接声明

可以放在代码的任意位置,其他地方都可以调用

// 函数声明
function f(a, b) {
  return a + b
}

2.构造函数语法 new Function

new Function(arg0, arg1, /* …, */ argN, functionBody)

  • 可以动态地由字符串创建函数,需要从服务器获取代码或者动态地从模板编译函数时才会使用
  • 使用 new Function 创建的函数,它的 [[Environment]] 并不指向当前的词法环境,而是指向全局环境(因此,此类函数无法访问外部变量,只能访问全局变量,也就没有闭包)

因为动态创建的函数可能无法知道里面的内容,如果能直接访问外层变量是不安全的,所以这是合理的

表达式

在执行到达时被创建,并且仅从那一刻起可用

// 函数表达式
let sum = function(a, b) {
	return a + b
}

立即执行函数表达式(IIFE,Immediately Invoked Function Expression)

函数表达式立即调用就形成了 IIFE

(function a() {
  console.log('x')
})()

Tip

历史用法:以前只有 var 声明变量,可以用这种方法产生块级作用域和私有变量。现在已经基本不再需要,用块级作用域代替即可

其他 IIFE 变体

括号放里面

(function a() {
  console.log('x')
}())

一元运算符

+function() {
  console.log("This is an IIFE using a unary operator!")
}();
 
!function() {
  console.log("This is another IIFE using a unary operator!")
}();

void

void function() {
  console.log("This is an IIFE using void operator!")
}()

传参

(function(name) {
  console.log(`Hello, ${name}!`)
})("John")

new

let user = new function() {
  this.name = "John";
 
  // 其他创建对象的代码...
}

匿名函数

没有名称标识符的函数,用在临时的不需要再次调用的地方

function() {
}

命名函数表达式(NFE,Named Function Expression)

带有名字的函数表达式

  • 它允许函数在内部引用自己
  • 它在函数外是不可见的
// 函数有2个名字 sayHi,fn
let sayHi = function fn(who) {
  console.log(`Hello, ${who}`)
}

Tip

为什么要再加个名字,不直接用 sayHi ?
因为 sayHi 是外部变量,随时可能被改变,所以在 fn 内部要用 fn 表示自身

参数

JS 允许传入任意个参数而不影响调用,传入参数多了会忽略多余的,传入参数少了会赋值 undefined

arguments

  • 普通函数内部有一个叫 arguments类数组对象,key 是实参的位置,value 是实参值
  • 改变 arguments 会影响对应的函数参数
  • 有一个 key 叫 length,表示实参的个数
  • 有一个 key 叫 callee,表示 arguments 所属的当前正在执行的函数(已弃用)
function f() {
  console.log( arguments )
}
f(1, 2, 3)
 
// arguments 对象:
// {
//   0: 1,
//   1: 2,
//   2: 3,
//   length: 3
// }

剩余参数(Rest 参数)ES6

  • ...xxx 表示多余的参数(没有形参对应的实参)
  • 必须放在最后
  • 它是一个真正的 Array 实例
// 将多余的参数求和
function f(a, b, ...rest) {
  let sum = 0
  for(let i = 0; i < rest.length; i++) {
      sum += rest[i]
  }
  return sum
}
 
f(1, 2, 3, 4, 5) // 返回 3+4+5=12

arguments 和 Rest 的区别:

区别 1区别 2区别 3区别 4
arguments是对象所有实参有额外属性箭头函数不能用
Rest是数组没有对应形参的实参箭头函数能用

Rest 是用来替代 arguments 的

默认参数ES6

当值严格等于 undefined 时默认值才会生效,而且参数默认值是惰性求值的

// 给形参赋值就指定了默认参数
function f (a = 1) {
  console.log(a)
}

参数尾逗号ES2017

允许函数的最后一个参数有尾逗号


Note

参数是值传递还是引用传递?

  • 对基本类型:传值调用
  • 对引用类型:共享调用:传给函数的参数是对象的引用的拷贝(即对象变量指针的拷贝)。如果重新给参数赋值不会影响原对象,但是改变参数对象内部的值会影响原对象

属性

函数也是对象,也有 key

name

函数的 name 属性的值会根据上下文推断:如果函数自己没有提供,那么在赋值中,会根据上下文来推测一个

// 下面所有的函数的 name 都是 f
 
function f() {}
 
let f = function() {}
 
function f( f = function() {} ) {}
 
let user = {
  f() {},
}
 
let user = {
  f: function() {}
}

length

返回函数期望的参数数量(即形参的个数)

  • 不包括剩余参数(Rest)
  • 只包括在第一个默认参数之前的参数

自定义属性

函数也是对象,也可以添加属性。函数上的属性通过 func.prop 来访问

this

被自动定义在函数的作用域中,是在函数 <运行时> 绑定的,取决于函数调用时的各种条件。简单说,JS 中的函数具有动态的 this,它取决于调用上下文。(this 对象引用的是函数执行的环境上下文对象)

绑定规则

绑定优先级:new绑定 > 显示绑定 > 隐式绑定 > 默认绑定

优先级绑定方式this
1new绑定使用 new 操作符调用函数时,this 绑定到新创建的对象上
2显式绑定使用 applycallbind 方法,它们的第一个参数就是 this 要绑定的对象
3隐式绑定以对象的形式调用:obj.foo(),那么 this 指向调用它的对象 obj。注意如果有多重对象的调用,则只有最后一个对象起作用
4默认绑定单独调用函数时,this 指向 undefined(严格模式),或者 全局对象 window(非严格模式)

例外情况:箭头函数根据外层词法环境来决定 this

NOTE

隐式绑定 为什么会有隐式丢失问题?
obj.method() 调用运行的本质:

  • 首先,点 ’.’ 取了属性 obj.method 的值
  • 接着 () 执行了它

为确保 obj.method() 调用正常运行,JS 的点 ’.’ 返回的不是一个函数,而是一个特殊的 Reference Type 的值,他会记住调用对象的完整信息,从而设置正确的 this。
如果将调用分开,就找不到调用对象的完整信息,就会丢失 this:

obj.f() // (1) 正常调用,指向 obj 对象
;(obj.f)() // (2) 正常调用,指向 obj 对象(括号不改变运行顺序)
;(method = obj.f)() // (3) undefined 调用分开了
// 相当于:
// method = obj.f
// method()
;(obj.f || obj.fn)() // (4) undefined 这是表达式调用:(expression)(),同上也相当于调用分开了

总结:括号里不是表达式就不影响 this,是表达式就会丢失 this

apply,call

可以设置函数体内 this 对象的值,并立即执行函数

  • apply:只接受 类数组 方式传参:f.apply(obj [, argsArray])
  • call:按顺序单个传参:f.call(obj [, arg1, arg2, ...]),也可以用 展开语法 传入可迭代对象

f.apply(obj, args) 等价于 f.call(obj, ...args)
apply 性能会好点

方法借用

有一个对象是可迭代对象又是类数组对象(如:arguments),但它并不是真正的数组。如果这时候想调用一些关于数组的方法,可以这样做:
[].xxx.call( arguments )
当然,xxx 方法的内部实现需要取的是 this 的值来操作

bind

返回一个新的绑定函数,并拥有指定的 this 和 初始参数,返回的函数可以继续接受后续的参数
let newf = f.bind(obj [, arg1, arg2, ...])

一个函数不能被重绑定(re-bound)
bind 后的新函数是另一个函数,不具有原函数中的自定义属性

偏函数(Partial Function)

通过在 bind 调用时传入部分参数,后续对新函数的调用就自动带一部分参数,只用传入后面的参数即可
在频繁传入重复相同的参数时使用

柯里化(Currying)

是一种转换,将 f(a,b,c) f(a)(b)(c) 的形式进行调用(将多个参数的调用转换成依次调用单个参数)

function curry(func) {
 
  return function curried(...args) {
    if (args.length >= func.length) {
      return func.apply(this, args)
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2))
      }
    }
  }
 
}

柯里化要求函数必须具有固定数量的参数

箭头函数ES6

特点

箭头函数是更短的函数,通常也叫 lambda 函数

(x, y) => {
    if(x > y) return 1
    if(x < y) return -1
}
// 相当于
function (x, y) {
    if(x > y) return 1
    if(x < y) return -1
}
 
// 如果只有一个参数
x => { ... }
 
// 如果没有参数
() => { ... } // 或者 _ => {}
  • 没有 this,如果访问 this 对象,则会获取外层词法环境中的 this(因此不能被 new 调用当作构造函数)
  • 没有 arguments 对象,如果访问 arguments 对象,则会获取外层词法环境中的 arguments
  • supernew.target 也是和上面一样
  • 不能使用 yield 命令(所以不能用作 Generator 函数)

使用

如果函数里还有内部函数,根据 this 的原则,这个内部函数直接调用的 this 指向 undefined 或 window,此时可以在内层函数前面先使用 var that = this 来捕获 this:

function f1() {
    console.log(this.x)
 
    function f2() {
        console.log(this.x)
    }
    f2()
}
f1.call({x: 1}) // 1 undefined
//===================================
function f1() {
    console.log(this.x)
 
    var that = this
    function f2() {
        console.log(that.x)
    }
    f2()
}
f1.call({x: 1}) // 1 1

使用箭头函数:

function f1() {
    console.log(this.x)
 
    (() => {
        console.log(this.x)
    })()
}
f1.call({x:1}) // 1 1

Warning

不适用箭头函数:

  • 定义对象的方法:会导致以对象形式调用方法时 this 指向错误而不是该对象本身
  • 给元素添加监听事件:会导致 this 不能指向触发事件的元素本身

总结

普通函数的 this 是运行时动态绑定的,而箭头函数的 this 是定义时静态决定的(词法作用域)

构造函数

用于创建对象的特殊函数

使用 new 操作符来调用函数时,“被调用的函数”就叫构造函数。任何函数,只要他被 new 操作符调用,那它就被视为构造函数。它并不是一个特殊的函数,不被 new 调用时它就是一个普通的函数,所以可以说实际上并不存在“构造函数”,只有对函数的“构造调用”。

function foo(a) {
  this.a = a
}
 
var obj = new foo(1)  // 此时 foo 就叫做构造函数
console.log(obj.a) // 1

执行 new 操作时会发生:

  1. 创建一个新的空对象
  2. 将这个空对象的原型(__proto__),指向构造函数的 prototype 属性
  3. 将构造函数的作用域赋值给新对象(函数内部的 this 就指向了这个空对象)
  4. 执行构造函数中的代码(为新对象添加属性和方法)
  5. 如果构造函数没有返回值或者返回原始值,那么 new 操作会自动返回这个新创建的对象;如果构造函数显式返回一个对象,那么就用显式返回的对象

new.target

在函数内部,可以使用 new.target 属性来检查它是否被使用 new 进行调用了。对于常规调用,它为 undefined,对于使用 new 的调用,则等于该函数

function User() {
  alert(new.target)
}
 
// 不带 "new":
User(); // undefined
 
// 带 "new":
new User(); // function User { ... }

NOTE

class 语法是创建对象的更好办法

闭包

是指一个函数可以记住其外部变量并可以访问这些变量
JavaScript 中的函数会自动通过隐藏的 [[Environment]] 属性记住创建它们的位置,所以它们都可以访问外部变量

装饰器(decorator)

一个特殊的函数,它接受另一个函数并返回增加了某些功能的新函数

间谍装饰器

创建一个装饰器 spy(func),它应该返回一个包装器,该包装器将所有对函数的调用保存在其 calls 属性中

function work(a, b) { // work 是一个任意的函数或方法
  console.log( a + b )
}
 
work = spy(work); // 装饰一下 work
 
// 再调用新 work
work(1, 2) // 3
work(3, 4) // 7
 
for (let args of work.calls) {
  console.log( 'call:' + args.join() ) // "call:1,2", "call:3,4"
}

思路:
首先装饰器固定写法:返回一个函数,并接收原函数的所有参数,然后在合适的时候调用原函数:

function spy(fn) {
  return function (...args) {
    fn()
  }
}

返回的函数需要执行原函数,如果原函数有用到 this,在返回的函数里直接调用 fn() 根据默认绑定,this 是 undefined 而不是返回的函数被调用前面的对象。所以还需要绑定 this。如果原函数有返回值,还需要 return:

function spy(fn) {
  return function (...args) {
    return fn.apply(this, args) // 要有 this 和 return
  }
}

需要在原函数上面绑定一个属性,那么在返回的函数里面肯定要用到函数的名称,所以需要有 name:

function spy(fn) {
  return function newFn (...args) { // 要有name
    return fn.apply(this, args)
  }
}

最后写上业务逻辑即可:

function spy(fn) {
  return function newFn (...args) { // 要有name
    // 业务逻辑
    if (newFn.calls) {
      newFn.calls.push(args)
    } else {
      newFn.calls = [args]
    }
    
    return fn.apply(this, args)
  }
}

延时装饰器

同理,但是由于 setTimeOut() 的 特性,需要用箭头函数:

function delay(f, timeout) {
  return function(...args) {
    setTimeout(() => {
      f(...args)
    }, timeout)
  }
}

如果不用箭头函数,那么就需要在外面保存 this:

function delay(f, timeout) {
  return function(...args) {
    let savedThis = this // 将 this 存储到中间变量
    setTimeout(function() {
      f.apply(savedThis, args) // 在这儿使用它
    }, timeout)
  }
}

防抖

一段时间后执行事件,如果在这段时间内再次触发事件,则重新计时

function debounce(fn, timeout) {
  let timer
  return function(...args) {
    clearTimeout(timer) // 每次调用都清除上一次的定时器
    timer = setTimeout(() => { // 再开启新的定时器
      fn.apply(this, args)
    }, timeout)
  }
}

节流

一段时间内只运行一次,若在这段时间内重复触发,只有一次生效
1.时间戳写法:会立即调用第一次:

function throttle(fn, timeout) {
  let last = 0
  return function(...args) {
    let current = Date.now()
    if (current - last >= timeout) {
      last = current
      fn.apply(this, args)
    }
  }
}

2.setTimeout 写法:会延时调用第一次:

function throttle(fn, timeout) {
  let timer
  return function(...args) {
    if (timer) return
    timer = setTimeout(() => { // 再开启新的定时器
      fn.apply(this, args)
      timer = null
    }, timeout)
  }
}