函数声明,函数表达式
声明
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 |
---|---|---|
1 | new绑定 | 使用 new 操作符调用函数时,this 绑定到新创建的对象上 |
2 | 显式绑定 | 使用 apply ,call ,bind 方法,它们的第一个参数就是 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 super
和new.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 操作时会发生:
- 创建一个新的空对象
- 将这个空对象的原型(
__proto__
),指向构造函数的 prototype 属性 - 将函数内部的 this 指向这个空对象
- 执行构造函数中的代码(为新对象添加属性和方法)
- 如果构造函数没有返回值或者返回原始值,那么 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, wait) {
let timer
return function(...args) {
clearTimeout(timer) // 每次调用都清除上一次的定时器
timer = setTimeout(() => { // 再开启新的定时器
fn.apply(this, args)
}, wait)
}
}
增加立即执行的写法:
function debounce(fn, wait, immediate) {
let timer
return function(...args) {
clearTimeout(timer) // 每次调用都清除上一次的定时器
if (immediate) {
let callnow = !timer
// 下面的作用:保证一段时间内不触发调用
timer = setTimeout(() => {
timer = null
}, wait)
if (callnow) {
fn.apply(this, args)
}
} else {
timer = setTimeout(() => { // 再开启新的定时器
fn.apply(this, args)
}, wait)
}
}
}
节流
一段时间内只运行一次,若在这段时间内重复触发,只有一次生效
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)
}
}