现代JS学习笔记:函数进阶

Posted by Mars . Modified at

学习内容:《现代JavaScript教程》

11 函数进阶

11.1 递归recursion

函数内部调用自身,就是函数递归。

递归深度:

最大的嵌套调用次数(包括首次)被称为 递归深度。

最大递归深度受限于 JavaScript 引擎。对我们来说,引擎在最大迭代深度为 10000 及以下时是可靠的,有些引擎可能允许更大的最大深度,但是对于大多数引擎来说,100000 可能就超出限制了。

11.2 ★执行上下文context

函数运行执行过程的相关信息,存储在【执行上下文context】中。

执行上下文中储存着函数运行的:

  1. this值指向;
  2. 当前控制流所在位置(函数执行到第几行了?);
  3. 当前的变量;
  4. 其他内部细节;

一个函数调用的过程中,只有一个与其对应的执行上下文。

执行上下文不是对象,是一种特殊的内部数据结构。

当递归调用函数的时候,存在一个执行上下文堆栈。递归调用时,前一函数运行的执行上下文被固定并推入上下文堆栈中,新的函数调用完毕返回值后,从上下文堆栈中读取函数执行上下文,继续运行。

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 嵌套函数

嵌套函数有两种情况:

  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 在全局环境中声明一个变量并赋值,词法环境的变化情况

  1. 没用用let声明之前,词法环境里就有了phrase这个属性,也就是浏览器已经知道phrase的存在,但是状态是unintiallized未初始化,不能引用(报错);
  2. let phrase 声明之后,phrase的值在词法环境里变成了undefined,可以使用;
  3. 赋值后,相应的词法环境中属性值也会改变。

11.4.2.2 声明一个函数情况

创建一个执行环境的时候,所有环境内(如代码块内)的函数声明都立即被创建为可执行函数,而不是等到读取到函数声明才创建。这就解释了为什么函数可以在声明之前使用。

创建(声明)函数的时候,函数内部的属性[[Environment]]会记录下它创建时的外部词法环境,但只有当函数运行的时候,它本身的词法环境才被创建,这时候会使用声明时记录下的[[Environment]]属性值作为自己词法环境的outer外部引用参数。

11.4.2.3 内部和外部词法环境

比如在一个函数内部创建另一个函数,内部函数的词法环境就包含当前环境记录,记录了内部函数的参数和局部变量;此外还包含外部词法环境的引用,直到全局环境。

这个内部函数内查询变量,是从内向外的。局部词法环境查询不到就去上级环境里查询,使用可查询到的第一个变量。

所有的函数在“诞生”时都会记住创建它们的词法环境。从技术上讲,这里没有什么魔法:所有函数都有名为 [[Environment]]的隐藏属性,该属性保存了对创建该函数的词法环境的引用。

11.4.3 闭包

闭包 是指内部函数总是可以访问其所在的外部函数中声明的变量和参数,即使在其外部函数被返回(寿命终结)了之后。

11.4.4 函数对象

JS中,函数是对象。(可以把函数想象成可被调用的“行为对象(action object)”。)

函数对象包含的属性有:

  1. name 函数名;
  2. length 参数的个数;(rest参数不算在内)
  3. 自定义属性: 自己可以给函数指定属性;

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函数的偏函数。

函数柯里化的好处,就是随时随地可以轻松创建偏函数。

Keywords: JavaScript
previousPost nextPost
已经有 1000000 个小伙伴看完了这篇推文。