现代JS学习笔记:函数进阶
Posted by Mars . Modified at
学习内容:《现代JavaScript教程》
11 函数进阶
11.1 递归recursion
函数内部调用自身,就是函数递归。
递归深度:
最大的嵌套调用次数(包括首次)被称为 递归深度。
最大递归深度受限于 JavaScript 引擎。对我们来说,引擎在最大迭代深度为 10000 及以下时是可靠的,有些引擎可能允许更大的最大深度,但是对于大多数引擎来说,100000 可能就超出限制了。
11.2 ★执行上下文context
函数运行执行过程的相关信息,存储在【执行上下文context】中。
执行上下文中储存着函数运行的:
- this值指向;
- 当前控制流所在位置(函数执行到第几行了?);
- 当前的变量;
- 其他内部细节;
一个函数调用的过程中,只有一个与其对应的执行上下文。
执行上下文不是对象,是一种特殊的内部数据结构。
当递归调用函数的时候,存在一个执行上下文堆栈。递归调用时,前一函数运行的执行上下文被固定并推入上下文堆栈中,新的函数调用完毕返回值后,从上下文堆栈中读取函数执行上下文,继续运行。
11.3 Rest参数
11.3.1 Rest参数
Rest 参数可以使函数传入任意数量的参数。
通过使用三个点 … 并在后面跟着包含剩余参数的数组名称,来将它们包含在函数定义中。这些点的字面意思是“将剩余参数收集到一个数组中”。
function fun1(…args){}
// 传入的参数都被收纳在args这个数组中,可以在函数内调用。
Rest参数必须放在函数参数列表的最末尾,放在中间会报错。
rest参数与arguments对象的区别:rest是真正的数组,而Arguments是类数组的对象,无法使用数组的自带函数。(arguments是历史遗留问题,新代码尽量不用。)
function sum(...arg){ //Rest参数
return arg.reduce((accu,item)=>{accu += item; return accu;});
}
let arr = [1,2,3,4,5];
console.log( sum(...arr) ); //15
11.3.2 Spread参数
与Rest参数相反,Spread参数可以把数组展开为独立的参数列表。
func(a1,a2,a3);
let arr = [1,2,3];
func( …arr ); //这里可以正常传入1,2,3,数组被Spread打散。
11.4 变量作用域与闭包
11.4.1 变量作用域
11.4.1.1 代码块
代码块是用{}括起来的一段代码,使用let/const在代码块中声明的变量,只能在该代码块中使用,代码块外无法访问。
for(let xxx;;){}循环中,在圆括号内声明的变量也被视为代码块的一部分。
不同代码块,可以声明相同名称的变量,互不影响。
11.4.1.2 嵌套函数
嵌套函数有两种情况:
- 在一个函数内,声明另一个函数,则内部函数可以获取外部函数作用域内变量。
- 函数的返回值也可以是一个函数,这个函数在任何部位调用,都可以访问函数内部的变量。
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter = makeCounter();
alert( counter() ); // 0
alert( counter() ); // 1
alert( counter() ); // 2
11.4.2 词法环境
在 JavaScript 中,每个运行的函数,代码块 {…} 以及整个脚本,都有一个被称为 词法环境(Lexical Environment)的内部(隐藏)的关联对象。
词法环境由两部分组成:①当前环境记录:一个对象,储存当前环境所有的局部变量及其他信息(如this的值);②外部词法环境:对外部词法环境记录的引用;
词法环境对象,不是一般的JS对象,而是一个规范对象(specification object):它仅仅是存在于编程语言规范中的“理论上”存在的,用于描述事物如何运作的对象。我们无法在代码中获取该对象并直接对其进行操作。
11.4.2.1 在全局环境中声明一个变量并赋值,词法环境的变化情况
- 没用用let声明之前,词法环境里就有了phrase这个属性,也就是浏览器已经知道phrase的存在,但是状态是unintiallized未初始化,不能引用(报错);
- let phrase 声明之后,phrase的值在词法环境里变成了undefined,可以使用;
- 赋值后,相应的词法环境中属性值也会改变。
11.4.2.2 声明一个函数情况
创建一个执行环境的时候,所有环境内(如代码块内)的函数声明都立即被创建为可执行函数,而不是等到读取到函数声明才创建。这就解释了为什么函数可以在声明之前使用。
创建(声明)函数的时候,函数内部的属性[[Environment]]
会记录下它创建时的外部词法环境,但只有当函数运行的时候,它本身的词法环境才被创建,这时候会使用声明时记录下的[[Environment]]属性值作为自己词法环境的outer外部引用参数。
11.4.2.3 内部和外部词法环境
比如在一个函数内部创建另一个函数,内部函数的词法环境就包含当前环境记录,记录了内部函数的参数和局部变量;此外还包含外部词法环境的引用,直到全局环境。
这个内部函数内查询变量,是从内向外的。局部词法环境查询不到就去上级环境里查询,使用可查询到的第一个变量。
所有的函数在“诞生”时都会记住创建它们的词法环境。从技术上讲,这里没有什么魔法:所有函数都有名为 [[Environment]]
的隐藏属性,该属性保存了对创建该函数的词法环境的引用。
11.4.3 闭包
闭包 是指内部函数总是可以访问其所在的外部函数中声明的变量和参数,即使在其外部函数被返回(寿命终结)了之后。
11.4.4 函数对象
JS中,函数是对象。(可以把函数想象成可被调用的“行为对象(action object)”。)
函数对象包含的属性有:
- name 函数名;
- length 参数的个数;(rest参数不算在内)
- 自定义属性: 自己可以给函数指定属性;
11.4.5 命名函数表达式NFE
带有名字的函数表达式。
- 常规函数表达式: let a = function(){};
- 命名函数表达式: let a = function fun1(){};
关于名字 func 有两个特殊的地方,这就是添加它的原因:
- 它允许函数在内部引用自己。(比引用函数表达式的变量名好,因为这个名字是函数本身的,不怕变量名修改。)
- 它的命名在函数外是不可见的。
11.5 new Function()創建函數
new Function('a', 'b', 'return a + b'); // 基础语法
new Function('a,b', 'return a + b'); // 逗号分隔
new Function('a , b', 'return a + b'); // 逗号和空格分隔
一般情况下不需要使用new Function()这种特殊的形式创建函数。但是它有特殊的用途。
new Function()的特殊之处在于:它创建的函数词法环境为全局环境,无法访问当前声明环境中的局部变量。
11.6 函数柯里化Currying
函数的柯里化,指的是把一次性传入全部参数的函数,转化为可连续依次传入参数的函数类型。
例如,原函数为f(a,b,c),柯里化后使用方式为f(a)(b)(c)。
JS中,一般Currying柯里化高级一点的实现,可以保证函数既可以像原来一样同时传入多个参数调用,也可以一个一个传入参数调用。
柯里化的好处是:如果传入参数不足原参数数量,则返回保存了已传入参数的剩余函数,这样在之后只需要传入剩下的参数就可以获取结果。这句话难以理解,看下面的例子:
一个函数柯里化后,例如f1(a,b,c)被柯里化为f2(a)(b)(c),如果调用一次f2(a),则返回的是可继续调用两次的函数f3(b)(c),这时a可以看做是被传入了默认值,之后使用f3只需要依次再传入两个参数b,c即可。
这个f3函数叫做原f1函数的偏函数。
函数柯里化的好处,就是随时随地可以轻松创建偏函数。