函数
this
与其他语言相比,函数的 this
关键字在 JavaScript 中的表现略有不同,此外,在严格模式和非严格模式之间也会有一些差别。
在绝大多数情况下,函数的调用方式决定了 this
的值(运行时绑定)。this
不能在执行期间被赋值,并且在每次函数被调用时 this
的值也可能会不同。ES5 引入了 bind 方法来设置函数的 this
值,而不用考虑函数如何被调用的。ES2015 引入了箭头函数,箭头函数不提供自身的 this 绑定(this
的值将保持为闭合词法上下文的值)。
this
当前执行上下文(global、function 或 eval)的一个属性,在非严格模式下,总是指向一个对象,在严格模式下可以是任意值。
this指向-普通函数
普通函数的调用方式决定了 this 的值,即【谁调用 this 的值指向谁】。
// 普通函数
function fn() {
console.log(this)
}
// 函数表达式
const fun = function () {
console.log(this)
}
// 函数的调用方式决定了this指向
fn() //window
window.fn() //window
// 普通对象
const obj = {
sing: function () {
console.log(this)
},
}
// 函数调用方式,决定了this的值
obj.sing()
//严格模式
'use strict'
function fn() {
console.log(this)
}
fn() // undefined
普通函数没有明确调用者时 this 值为 window,严格模式下没有调用者时 this 的值为 undefined。
this指向-箭头函数
箭头函数中的 this 与普通函数完全不同,也不受调用方式的影响,事实上箭头函数中并不存在 this!
- 箭头函数会默认帮我们绑定外层 this 的值,所以在箭头函数中 this 的值和外层的 this 是一样的。
- 箭头函数中的this引用的就是最近作用域中的this。
- 向外层作用域中,一层一层查找this,直到有this的定义。
// 普通对象
const obj = {
// 该箭头函数中的 this 为函数声明环境中 this 一致
sing: () => {
console.log(this)
},
}
obj.sing()
注意情况:
- 在开发中【使用箭头函数前需要考虑函数中 this 的值】,事件回调函数使用箭头函数时,this 为全局的window因此DOM事件回调函数如果里面需要DOM对象的this,则不推荐使用箭头函数。
- 同样由于箭头函数 this 的原因,基于原型的面向对象也不推荐采用箭头函数。
总结:
- 普通函数在非严格模式下指向window, 开启严格模式指向undefined。
- 在函数套函数的时候指向window。
- 对象调用函数指向调用函数的对象。
- 箭头函数没有this。
- 箭头函数中的this指向的是箭头函数定义时离this最近的一个函数,如果没有则指向window。
- 不适用:构造函数,原型函数,dom事件函数等等。
- 适用:需要使用上层this的地方。
改变this
call()
语法:
函数名.call(this指向的值,函数实参,...)
示例:
let obj = {
uname:'obj'
}
function fn (x,y) {
let uname = 'fn'
console.log(this.uname);
console.log(x+y);
}
fn.call() // 可以调用函数的 undefined NaN
fn.call(obj,20,30) // 'obj' 50
总结:
call
方法能够在调用函数的同时指定this
的值。- 使用
call
方法调用函数时,第1个参数为this
指定的值。 call
方法的其余参数会依次自动传入函数做为函数的参数。- 此方法只是临时改变this指向一次。
apply()
语法:
函数名.apply(this指向的值,[函数实参,...])
示例:
let obj = {
uname: 'obj',
}
function fn(x, y, z) {
console.log(this.uname)
console.log(x, y, z)
}
fn.apply() // 可以调用函数的 undefined NaN
fn.apply(obj, [10, 20, 30]) // 'obj' 60
总结:
apply
方法能够在调用函数的同时指定this
的值。- 使用
apply
方法调用函数时,第1个参数为this
指定的值。 apply
方法的第二个参数是函数接受的实参,以数组的形式传入。- 此方法只是临时改变this指向一次。
bind()
语法:
函数名.bind(this指向的值,函数实参,...)
示例:
const obj = {
uname: '张三',
}
function fn() {
console.log(this.uname)
}
// 不会直接调用
// 返回值 :改变this后的函数
const fun = fn.bind(obj)
fun() // 张三
总结:
bind
方法指定this
的值时不会直接调用函数。- 具有返回值,返回值为永久改变
this
后的函数。 - 使用
bind
方法调用函数时,第1个参数为this
指定的值。 bind
方法的其余参数会依次自动传入函数做为函数的参数。
总结
相同点:
- 都可以改变函数内部的this指向。
不同点:
- call和 apply会调用函数, 并且改变函数内部this指向。
- call和 apply传递的参数不一样, call 传递参数 aru1, aru2…形式,apply必须数组形式[arg]。
bind不会调用函数, 可以改变函数内部this指向
。
使用场景:
- call调用函数并且可以传递参数。
- apply经常跟数组有关系. 比如借助于数学对象实现数组最大值最小值。
bind不调用函数,但是还想改变this指向. 比如改变定时器内部的this指向
。
调用栈
调用栈是解释器(比如浏览器中的 JavaScript 解释器)追踪函数执行流的一种机制。当执行环境中调用了多个函数时,通过这种机制,我们能够追踪到哪个函数正在执行,执行的函数体中又调用了哪个函数。
- 每调用一个函数,解释器就会把该函数添加进调用栈并开始执行。
- 正在调用栈中执行的函数还调用了其他函数,那么新函数也将会被添加进调用栈,一旦这个函数被调用,便会立即执行。
- 当前函数执行完毕后,解释器将其清出调用栈,继续执行当前执行环境下的剩余的代码。
- 当分配的调用栈空间被占满时,会引发“堆栈溢出”错误。
function greeting() {
// [1] Some codes here
sayHi();
// [2] Some codes here
}
function sayHi() {
return "Hi!";
}
// 调用 `greeting` 函数
greeting();
// [3] Some codes here
上面的代码会按照如下流程这样执行:
-
忽略前面所有函数,直到
greeting()
函数被调用。 -
把
greeting()
添加进调用栈列表。 -
执行
greeting()
函数体中的所有代码。调用栈列表: - greeting
-
代码执行到
sayHi()
时,该函数被调用。 -
把
sayHi()
添加进调用栈列表。 -
执行
sayHi()
函数体中的代码,直到全部执行完毕。调用栈列表: - sayHi - greeting
-
返回来继续执行
greeting()
函数体中sayHi()
后面的代码。 -
删除调用栈列表中的
sayHi()
函数。 -
当
greeting()
函数体中的代码全部执行完毕,返回到调用greeting()
的代码行,继续执行剩下的 JS 代码。调用栈列表: - greeting
-
删除调用栈列表中的
greeting()
函数。
一开始,我们得到一个空空如也的调用栈。随后,每当有函数被调用都会自动地添加进调用栈,执行完函数体中的代码后,调用栈又会自动地移除这个函数。最后,我们又得到了一个空空如也的调用栈。
递归、尾递归
递归
一种函数调用自身的操作。递归被用于处理包含有更小的子问题的一类问题。一个递归函数可以接受两个输入参数:一个最终状态(终止递归)或一个递归状态(继续递归)。
export function transListToTree(data, pid) {
const arr = []
data.forEach((item) => {
if (item.pid === pid) {
// 当前: item 就是1级数据 item.id
const children = transListToTree(data, item.id)
if (children.length) {
item.children = children
}
arr.push(item)
}
})
return arr
}
尾递归
当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活动记录而不是在栈中去创建一个新的。编译器可以做到这点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率会变得更高。
这是一个使用JavaScript实现的递归函数:
function recsum(x) {
if (x===1) {
return x;
} else {
return x + recsum(x-1);
}
}
如果你调用recsum(5)
,JavaScript解释器将会按照下面的次序来计算:
recsum(5)
5 + recsum(4)
5 + (4 + recsum(3))
5 + (4 + (3 + recsum(2)))
5 + (4 + (3 + (2 + recsum(1))))
5 + (4 + (3 + (2 + 1)))
15
注意在JavaScript解释器计算recsum(5)之前,每个递归调用必须全部完成。
这是同一函数的尾递归版本:
function tailrecsum(x, running_total=0) {
if (x===0) {
return running_total;
} else {
return tailrecsum(x-1, running_total+x);
}
}
下面是当你调用tailrecsum(5)的时候实际的事件调用顺序:
tailrecsum(5, 0)
tailrecsum(4, 5)
tailrecsum(3, 9)
tailrecsum(2, 12)
tailrecsum(1, 14)
tailrecsum(0, 15)
15
在尾递归的情况下,每次递归调用的时候,running_total
都会更新。
参数
默认值
函数默认参数允许在没有值或 undefined
被传入时使用默认形参。
function multiply(a, b = 1) {
return a * b;
}
console.log(multiply(5, 2));
// Expected output: 10
console.log(multiply(5));
// Expected output: 5
JavaScript 中函数的参数默认是 undefined
。然而,在某些情况下可能需要设置一个不同的默认值。这是默认参数可以帮助的地方。
以前,一般设置默认参数的方法是在函数体测试参数是否为 undefined
,如果是的话就设置为默认的值。
Arguments 对象
arguments
是一个对应于传递给函数的参数的类数组对象。
function func1(a, b, c) {
console.log(arguments[0]);
// Expected output: 1
console.log(arguments[1]);
// Expected output: 2
console.log(arguments[2]);
// Expected output: 3
}
func1(1, 2, 3);
arguments
对象是所有(非箭头)函数中都可用的局部变量。你可以使用arguments
对象在函数中引用函数的参数。此对象包含传递给函数的每个参数,第一个参数在索引 0 处。
参数也可以被设置:
arguments[1] = 'new value';
arguments
对象不是一个 Array 。它类似于Array,但除了 length 属性和索引元素之外没有任何Array属性。例如,它没有 pop 方法。但是它可以被转换为一个真正的Array:
var args = Array.prototype.slice.call(arguments);
var args = [].slice.call(arguments);
// ES2015
const args = Array.from(arguments);
const args = [...arguments];
剩余参数
剩余参数语法允许我们将一个不定数量的参数表示为一个数组。
function sum(...theArgs) {
let total = 0;
for (const arg of theArgs) {
total += arg;
}
return total;
}
console.log(sum(1, 2, 3));
// Expected output: 6
console.log(sum(1, 2, 3, 4));
// Expected output: 10
如果函数的最后一个命名参数以...
为前缀,则它将成为一个由剩余参数组成的真数组,其中从0
(包括)到theArgs.length
(排除)的元素由传递给函数的实际参数提供。
在上面的例子中,theArgs
将收集该函数的第三个参数(因为第一个参数被映射到a
,而第二个参数映射到b
)和所有后续参数。
剩余参数和 arguments对象的区别
剩余参数和 arguments对象之间的区别主要有三个:
- 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参。
- arguments对象不是一个真正的数组,而剩余参数是真正的 Array实例,也就是说你能够在它上面直接使用所有的数组方法,比如 sort,map,forEach或pop。
- arguments对象还有一些附加的属性(如callee属性)。
作用域
作用域(scope)规定了变量能够被访问的“范围”,离开了这个“范围”变量便不能被访问,作用域分为全局作用域和局部作用域。
局部作用域
局部作用域分为函数作用域和块作用域。
函数作用域
在函数内部声明的变量只能在函数内部被访问,外部无法直接访问。
// 声明 counter 函数
function counter(x, y) {
// 函数内部声明的变量
const s = x + y
console.log(s) // 18
}
// 设用 counter 函数
counter(10, 8)
// 访问变量 s
console.log(s)// 报错
总结:
- 函数内部声明的变量,在函数外部无法被访问。
- 函数的参数也是函数内部的局部变量。
- 不同函数内部声明的变量无法互相访问。
- 函数执行完毕后,函数内部的变量实际被清空了。
块作用域
在 JavaScript 中使用 {}
包裹的代码称为代码块,代码块内部声明的变量外部将【有可能】无法被访问。
<script>
{
// age 只能在该代码块中被访问
let age = 18;
console.log(age); // 正常
}
// 超出了 age 的作用域
console.log(age) // 报错
let flag = true;
if(flag) {
// str 只能在该代码块中被访问
let str = 'hello world!'
console.log(str); // 正常
}
// 超出了 age 的作用域
console.log(str); // 报错
for(let t = 1; t <= 6; t++) {
// t 只能在该代码块中被访问
console.log(t); // 正常
}
// 超出了 t 的作用域
console.log(t); // 报错
</script>
JavaScript 中除了变量外还有常量,常量与变量本质的区别是【常量必须要有值且不允许被重新赋值】,常量值为对象时其属性和方法允许重新赋值。
总结:
let
声明的变量会产生块作用域,var
不会产生块作用域。const
声明的常量也会产生块作用域。- 不同代码块之间的变量无法互相访问。
- 推荐使用
let
或const
。
注:开发中 let
和 const
经常不加区分的使用,如果担心某个值会不小被修改时,则只能使用 const
声明成常量。
全局作用域
<script>
标签和 .js
文件的【最外层】就是所谓的全局作用域,在此声明的变量在函数内部也可以被访问。
<script>
// 此处是全局
function sayHi() {
// 此处为局部
}
// 此处为全局
</script>
全局作用域中声明的变量,任何其它作用域都可以被访问。
例:
<script>
// 全局变量 name
const name = '小明'
// 函数作用域中访问全局
function sayHi() {
// 此处为局部
console.log('你好' + name)
}
// 全局变量 flag 和 x
const flag = true
let x = 10
// 块作用域中访问全局
if(flag) {
let y = 5
console.log(x + y) // x 是全局的
}
</script>
总结:
- 为
window
对象动态添加的属性默认也是全局的,不推荐! - 函数中未使用任何关键字声明的变量为全局变量,不推荐!!!
- 尽可能少的声明全局变量,防止全局变量被污染。
JavaScript 中的作用域是程序被执行时的底层机制,了解这一机制有助于规范代码书写习惯,避免因作用域导致的语法错误。
闭包
闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。
function fn() {
var a = 10
return function getData() {
return a
}
}
var getData = fn()
var a1 = getData()
闭包的原理就是利用作用域链的特性,首先在当前作用域访问数据,当前作用域访问不到,则向父级访问,父级也没有,一直找到全局。
作用:数据私有化,防止污染全局
var a = 10
function fn() {
console.log(a)
}
console.log(a)
缺点:闭包会造成内存泄漏,因为闭包的数据没有被回收
function fn() {
var a = 10
return function getData() {
return a
}
}
var getData = fn()
var a1 = getData()
解决方案:将全局指向的函数重新置为 null,利用标记清除的特性
function fn() {
var a = 10
return function getData() {
return a
}
}
var getData = fn()
getData = null
变量提升
变量提升(Hoisting)被认为是,Javascript 中执行上下文(特别是创建和执行阶段)工作方式的一种认识。在 ECMAScript® 2015 Language Specification 之前的 JavaScript 文档中找不到变量提升(Hoisting)这个词。不过,需要注意的是,开始时,这个概念可能比较难理解,甚至恼人。
例如,从概念的字面意义上说,“变量提升”意味着变量和函数的声明会在物理层面移动到代码的最前面,但这么说并不准确。实际上变量和函数声明在代码里的位置是不会动的,而是在编译阶段被放入内存中。
JavaScript 在执行任何代码段之前,将函数声明放入内存中的优点之一是,你可以在声明一个函数之前使用该函数。例如:
/**
* 正确的方式:先声明函数,再调用函数 (最佳实践)
*/
function catName(name) {
console.log("我的猫名叫 " + name);
}
catName("Tigger");
/*
以上代码的执行结果是:"我的猫名叫 Tigger"
*/
上面的代码片按照是你的正常思维(先声明,后调用)去书写的。现在,我们来看看当我们在写这个函数之前调用这个函数会发生什么:
/**
* 不推荐的方式:先调用函数,再声明函数
*/
catName("Chloe");
function catName(name) {
console.log("我的猫名叫 " + name);
}
/*
代码执行的结果是:"我的猫名叫 Chloe"
*/
即使我们在定义这个函数之前调用它,函数仍然可以工作。这是因为在 JavaScript 中执行上下文的工作方式造成的。
变量提升也适用于其他数据类型和变量。变量可以在声明之前进行初始化和使用。但是如果没有初始化,就不能使用它们。
译者注:函数和变量相比,会被优先提升。这意味着函数会被提升到更靠前的位置。
IIFE、匿名自执行函数
IIFE(立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数。IIFE 这个名字是由 Ben Alman 在他的博客中提出的。
(function () {
// …
})();
(() => {
// …
})();
(async () => {
// …
})();
它是一种设计模式,也被称为自执行匿名函数,主要包含两部分:
- 第一部分是一个具有词法作用域的匿名函数,并且用圆括号运算符 () 运算符闭合起来。这样不但阻止了外界访问 IIFE 中的变量,而且不会污染全局作用域。
- 第二部分创建了一个立即执行函数表达式 (),通过它,JavaScript 引擎将立即执行该函数。
函数表达式
function
关键字可以用来在一个表达式中定义一个函数。
你也可以使用 Function 构造函数和一个函数声明来定义函数。
let function_expression = function [name]([param1[, param2[, ..., paramN]]]) {
statements
};
name
函数名称。可被省略,此种情况下的函数是匿名函数(anonymous)。函数名称只是函数体中的一个本地变量。
paramN
被传递给函数的一个参数名称。一个函数至多拥有 255 个参数。
statements
构成函数体的语句。
函数表达式(function expression)非常类似于函数声明(function statement)(详情查看函数声明),并且两者拥有几乎相同的语法。函数表达式与函数声明的最主要区别是函数名称(function name),在函数表达式中可省略它,从而创建匿名函数(anonymous functions)。
箭头函数
箭头函数表达式的语法比函数表达式
更简洁,并且没有自己的 this
,arguments
,super
或 new.target
。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。
const materials = [
'Hydrogen',
'Helium',
'Lithium',
'Beryllium'
];
console.log(materials.map(material => material.length));
// Expected output: Array [8, 6, 7, 9]
基础语法
(param1, param2, …, paramN) => { statements }
(param1, param2, …, paramN) => expression
//相当于:(param1, param2, …, paramN) =>{ return expression; }
// 当只有一个参数时,圆括号是可选的:
(singleParam) => { statements }
singleParam => { statements }
// 没有参数的函数应该写成一对圆括号。
() => { statements }
高级语法
//加括号的函数体返回对象字面量表达式:
params => ({foo: bar})
//支持剩余参数和默认参数
(param1, param2, ...rest) => { statements }
(param1 = defaultValue1, param2, …, paramN = defaultValueN) => {
statements }
//同样支持参数列表解构
let f = ([a, b] = [1, 2], {x: c} = {x: a + b}) => a + b + c;
f(); // 6
new Function
Function()
构造函数创建了一个新的 Function
对象。直接调用构造函数可以动态创建函数,但可能会经受一些安全和类似于 eval()
(但远不重要)的性能问题。然而,不像 eval
(可能访问到本地作用域),Function
构造函数只创建全局执行的函数。
const sum = new Function('a', 'b', 'return a + b');
console.log(sum(2, 6));
// Expected output: 8
使用 Function
构造函数创建的 Function
对象会在函数创建时完成解析。这比用函数表达式或函数声明创建一个函数并在代码中调用它的效率要低,因为使用表达式或声明创建的函数会和其他的代码一起被解析。
setTimeout
全局的 setTimeout()
方法设置一个定时器,该定时器在定时器到期后执行一个函数或指定的一段代码。
setTimeout(code)
setTimeout(code, delay)
setTimeout(functionRef)
setTimeout(functionRef, delay)
setTimeout(functionRef, delay, param1)
setTimeout(functionRef, delay, param1, param2)
setTimeout(functionRef, delay, param1, param2, /* … ,*/ paramN)
functionRef
当定时器到期后,将要执行的 function。
code
这是一个可选语法,允许你包含在定时器到期后编译和执行的字符串而非函数。使用该语法是不推荐的,原因和使用 eval() 一样,有安全风险。
delay
定时器在执行指定的函数或代码之前应该等待的时间,单位是毫秒。如果省略该参数,则使用值 0,意味着“立即”执行,或者更准确地说,在下一个事件循环执行。
paramN
附加参数,一旦定时器到期,它们会作为参数传递给 functionRef
指定的函数。
setTimeout(() => {
console.log("延迟了 1 秒。");
}, "1000");
setInterval
Window 和 Worker 接口提供的 setInterval() 方法重复调用一个函数或执行一个代码片段,在每次调用之间具有固定的时间间隔。
它返回一个 interval ID,该 ID 唯一地标识时间间隔,因此你可以稍后通过调用 clearInterval() 来移除定时器。
var intervalID = setInterval(func, [delay, arg1, arg2, ...]);
var intervalID = setInterval(function[, delay]);
var intervalID = setInterval(code, [delay]);
func
要重复调用的函数,每经过指定 delay
毫秒后执行一次。第一次调用发生在 delay
毫秒之后。
code
这是一个可选语法,允许你包含在定时器到期后编译和执行的字符串而非函数。使用该语法是不推荐的,原因和使用 eval() 一样,有安全风险。
delay
是每次延迟的毫秒数(一秒等于 1000 毫秒),函数的每次调用会在该延迟之后发生。如果未指定,则其默认值为 0。
argN
当计时结束的时候,将被传递给 func
函数的附加参数。
var intervalID = setInterval(myCallback, 500, 'Parameter 1', 'Parameter 2');
function myCallback(a, b)
{
// Your code here
// Parameters are purely optional.
console.log(a);
console.log(b);
}
部分施用、柯里化
原理
柯里化:从一个多参数函数变成一连串单参数的变换,描述的是变换的过程,不涉及变换之后对函数的电泳。调用者可以决定对多少个参数实施变换,余下的部分衍生成为一个参数数目较少的新函数。
部分施用:提前带入一部分参数值,使一个多参数函数得以省略部分参数,从而转变成一个参数数目较少的函数。
异同:都是先提供部分参数值之后,产出可以凭借余下的参数实施调用的一个函数。当让,柯里化返回的是一个链条中的下一个函数,而部分调用部分施用是把参数的取值绑定到用户在操作中提供的具体值上,因而产生一个“元数”较少的函数。
例如:函数process(x, y, z)完全柯里化之后就变成了process(x)(y)(z)的形式,其中process(x)、process(x)(y)都是单参数的函数。如果是部分施用,接受一个参数后就变成了还剩两个参数的函数:process(y, z)
作用
- 参数复用:需要输入多个参数,最终只需输入一个,其余通过argument或者剩余参数来获取。
- 提前返回:避免重复去判断某一个条件是否符合,不符合则return不再继续执行下面的操作。
- 延迟执行:避免重复的去执行程序,等真正需要结果的时候在执行。
手写柯里化函数
function curry(fn) {
return function curried(...args) {
return args.length >= fn.length ? fn(...args) : (...arg) => curried(...args, ...arg)
}
}
function sum(a, b, c) {
return a + b + c
}
console.log(curry(sum)(1)(2)(3, 4))