夯实基础(三)-函数

199 阅读10分钟

函数

在写这一篇文章的时候,我停止了很长的时间,因为我发现我无法下手,不知道函数应该说些什么? 能想到的就是一等公民,执行上下文,this,闭包,函数柯里化仅此而已。

1.一等公民

在 js 中函数是一等公民,在编程语言中,一等公民可以作为函数参数,可以作为函数返回值,也可以赋值给变量。

// 赋值
const sayHello = function () {
  console.log("hello");
};

// 传参
function a(func) {
  return func && func();
}

// 返回值
function b() {
  return function () {
    console.log("world");
  };
}

a(sayHello);

这个很好理解。

2.执行上下文栈

执行上下文栈,遵循 LIFO(后进先出)的规则,通过下面这个栗子来看看看。

// 递归
function recursive(count) {
  if (count === 2) {
    return;
  }
  recursive(++count);
}

recursive(0);

首先在执行上下文栈 创建一个 全局执行上下文 -> recursive(0) -> recursive(1) -> recursive(2) 当执行完 recursive(2)的时候会退出执行栈,控制流程到达当前栈中的下一个上下文,直到 Global context。

了解以后,我们做两个题目。

// 第一段代码
let scope = 'global'
function a() {
    let scope = 'local'

    function f() {
        return scope
    }
    return f()
}
a()

这里的执行上下文栈表示如下

const ecstask = []

1. ecstask.push('global context')
2. ecstask.push('a context')
3. ecstask.push('f context')
4. ecstask.pop('f context')
5. ecstask.pop('a context')

// 第二段代码
let scope = 'global'
function a() {
    let scope = 'local'

    function f() {
        return scope
    }
    return f
}
a()()

const ecstask = []

1. ecstask.push('global context')
2. ecstask.push('a context')
3. ecstask.pop('a context')
4. ecstask.push('f context')
5. ecstask.pop('f context')

上面的代码虽然结果一样,但是执行上下文栈是不一样的。

3.执行上下文

这个问题在我们日常面试中,应该是时常被问到。 执行上下文(Execution context)一般分为三类:

  1. 全局执行上下文: 全局有切仅有一个全局执行上下文,是默认的,全局执行上下文做了两件事情,第一,创建 windows 对象,第二,使得 this 执行 windows 对象。

  2. 函数执行上下文: 每个函数执行之前都会创建一个函数执行上下文,该函数内部的代码执行都在这个上下文中

  3. eval 执行上下文: 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,eval 的执行上下文有点诡异.

4.代码是如何执行的

看下面一段代码。

function a() {
    var b = 1
}

看完前面的执行上下文,我们来分析一下,具体的执行过程。

1. ecstask.push('global context')
2. ecstask.push('a context')
2. ecstask.pop('a context')

那么来看看,在当前函数a的执行栈是怎么执行一段代码的人~

4.1 执行上下文中的状态组件

一段执行上下文中,上指的的outer,下指的是内部的状态组件~分为三种:

const context = {
    // 状态组件
    stateComponents: {
        // 词法环境
        LexicalEnvironment: {
            // 环境记录器
            environmentRecord = {};
            // 外层词法环境环境的引用
            outerEnvironment = undefined; // 可能为null  全局环境的外层没有所以为null
        },
        // 变量环境
        VariableEnvironment: {},

        // this绑定
        ThisBinding: {}
    }
}
组件 描述
词法环境 是Js引擎内部用来跟踪标识符和特定变量之间的映射关系
变量环境 标识其环境记录持有由以下程序创建的绑定的词法环境此执行上下文中的变量声明。
This 绑定 指定该执行环境内的 ECMA 脚本代码中 this 关键字所关联的值。

4.2 词法环境

1.词法环境包含一个环境记录项和一个包含当前词法环境的外层词法环境的引用

大概这样

function LexicalEnvironment() {
  this.environmentRecord = {};
  this.outerEnvironment = undefined; // 可能为null  全局环境的外层没有所以为null
}

环境记录项,用于存储需要被声明变量的信息。

4.3 环境记录项

环境记录项分为 声明式记录项、对象式记录项,函数环境记录项,全局环境记录项,模块环境记录项

4.3.1 声明式记录项

声明式环境记录项用于定义那些将 标识符 与语言值直接绑定的 ECMA 脚本语法元素,例如 函数定义 , 变量定义 以及 Catch 语句。

4.3.2 对象式环境记录项

用于定义那些将 标识符 与具体对象的属性绑定的 ECMA 脚本元素,例如 程序 以及 With 表达式 。

4.3.3 函数环境记录项

函数环境记录项是一种声明式环境记录项,用于表达对象的顶层作用域,如果函数不是箭头函数则提供this,如果是箭头函数并且引用了super,那么函数内部也能调用

4.3.4 全局环境记录项

全局环境记录项用于表达最顶层的作用域,提供内置的绑定以及脚本。

4.3.5 模块环境记录项

模块环境记录是一个声明性环境记录,用于表示 ECMAScript模块。除了普通的可变和不可变绑定之外,模块环境记录还 提供不可变的导入绑定,这些绑定提供对中存在的目标绑定的间接访问 另一个环境记录。

还是上面那段代码


function a() {
    var b = 1
}
const context = {
    // 状态组件
    stateComponents: {
        // 词法环境
        LexicalEnvironment: {
            // 环境记录器
            environmentRecord: {
                type: 'Declarative', // 函数属于声名式函数记录器
                a: <func>
            };
            // 外层词法环境环境的引用
            outerEnvironment: global; // 可能为null  全局环境的外层没有所以为null
        },
        // 变量环境
        VariableEnvironment: {
            environmentRecord: {
                b: 1
            },
            outerEnvironment: global
        },

        // this绑定
        ThisBinding: global
    }
}

以上一段代码抽象后的执行上下文

以下是环境记录项中的一些方法,比较多,列举了一些,随意看看~ 有兴趣的朋友可以查看EcamScript2020

4.3.6 环境记录项的抽象方法

环境记录项目包含很多的抽象方法(部分)

方法 描述
HasBinding(N) 判断一个标识符(变量,声明)是否在当前的环境记录项中,N 为标识符的字符串,有则返回 true,否则返回 false
CreateMutableBinding(N, D) 在当前环境记录项中,定义一个新的可变绑定标识符,N 为标识符的字符串,D 为布尔值,表示能否被删除
CreateImmutableBinding(N,S) 在环境记录中创建一个新的但未初始化的不可变绑定。字符串值 N 是绑定名的文本。如果 S 为真,则尝试在它之后设置它已初始化的异常总是抛出异常,无论严格模式如何设引用该绑定的操作。
InitializeBinding(N, V) 为已经定义的标识符,初始化绑定值,可以是任何绑定|
SetMutableBinding(N, V, S) 设置环境记录中已经存在的可变绑定的值。字符串值 N 是绑定名称的文本。V 是绑定的值,可以是任何 ECMAScript 语言类型的值。S 是一个布尔标志。如果 S 为真且无法设置绑定引发类型错误异常。
GetBindingValue(N, S) 返回环境记录中已有绑定的值。字符串值 N 是绑定名的文本,S 用于标识引用源自严格模式代码或需要严格模式引用的如果 S 为真且绑定不存在,则引发引用错误异常 ReferenceError。如果绑定存在但未初始化,则抛出 ReferenceError,无论是否是严格模式
DeleteBinding(N) 删除当前环境记录中已经绑定的值,N 为标识符文本,如果绑定能够删除则返回 true,不能删除返回 false,不存在也返回 true
HasThisBinding() 检测一个环境记录项是否有 this 绑定,有则返回 true,否则 false
HasSuperBinding() 检测一个环境记录项是否有 super 方法绑定,有则返回 true,否则 false
WithBaseObject() 在一个环境记录项中。是否执行了 with 语句,有则返回 with 的对象,否则返回 undefined

上面的抽象方法看起来很难理解,其实仔细想象在我们的日常代码中都会得到体现,例如:给变量赋值的时候都会返回一个 undefined 等等。

外层词法环境的引用在作用域链中起到很重要的作用,举个栗子。在你们家族的族谱中,有很多的人。你是第五代子孙,第五代子孙有一本族谱里面记录第五代人的信息,还有一个几代信息的记录,这个几代信息的记录就是外层词法环境的引用,用来告诉别人,想要找这里不存在的人,你得去 4 代族谱里面去寻找,4 代找不到就去 3 代里面找直到找到,或者直到最顶层。

全局环境的 outer environment reference 是null

模块的 outer environment reference 是window,包含一个模块顶层this的绑定

4.3.6 词法环境的运算

GetIdentifierReference (lex, name, strict)

当调用GetIdentifierReference的时候,制定一个词法环境,标识符name,是否是严格模式。具体执行环境如下:

1.令 envRec 为 lex 的环境数据 2.以 name 为参数 N,调用 envRec 的 HasBinding(N) 具体方法,并令 exists 为调用的结果。 3.如果 exists 为 true,则: 返回一个类型为 引用 的对象,其基值为 envRec,引用的名称为 name,严格模式标识的值为 strict。 4.否则: 令 outer 为 lex 的 外部环境引用 。 以 outer、name 和 strict 为参数,调用 GetIdentifierReference,并返回调用的结果。

NewDeclarativeEnvironment (E)

当调用 NewDeclarativeEnvironment 抽象运算时,需指定一个 词法环境 E,其值可以为 null,此时按以下步骤进行:

1.令 env 为一个新建的 词法环境 。 2.令 envRec 为一个新建的 声明式环境数据 ,该环境数据不包含任何绑定。 3.令 env 的环境数据为 envRec。 4.令 env 的外部词法环境引用至 E。 5.返回 env。

NewObjectEnvironment (O, E)

当调用 NewObjectEnvironmentis 抽象运算时,需指定一个对象 O 及一个 词法环境 E(其值可以为 null),此时按以下步骤进行:

1.令 env 为一个新建的 词法环境 。 2.令 envRec 为一个新建的 对象环境数据 ,该环境数据包含 O 作为绑定对象。 3.令 env 的环境数据为 envRec。 4.令 env 的外部词法环境引用至 E。 5.返回 env。

5.函数作用域

前面说到了。在函数执行之前会创建一个执行上下文,其中包含一个outer,这个outer指向当前环境记录器的外层,函数的作用域就是当前的执行上下文。而由outer链接起来的链条即为作用域链~

当访问非当前作用域的时候就会报错,ReferenceError

var a = 1

function b() {
    var c = 2
    console.log(d)
}

function c() {
    var d = 3
    b()
}
c()

在上面的 抽象方法中就说到,如果当前的 环境记录项中不存在改变量 则 GetBindingValue 返回 ReferenceError

上图:

左侧为执行上下文栈,b和c的outer皆为全局环境记录项目,但是全局中又没有d,所以报错 ReferenceError

6.this绑定

this绑定主要分为这几类: 1.普通函数,普通函数的this绑定到 windows 2.对象的调用,this执行调用 3.显示绑定:apply, call, bind执行绑定值 4.隐式绑定:new 操作符,指向新初始化的对象 5.箭头函数:指向箭头函数最外层的普通函数的this

this的指向是在代码运行时确定的。

7.闭包

概念:一个函数能够访问另一个函数作用域中的变量

    function a() {
        let b = {}
        return function () {
            return b
        }
    }

7.柯里化

函数的柯里化的目的是什么?

1.延迟执行 2.参数复用 3.动态生成函数

概念:将一个带有多个参数的函数转换为一次一个的函数的过程。每次调用函数时,它只接受一个参数,并返回一个函数,直到传递所有参数为止

按照这个思路,基本实现就是:

var curry = function (fn) {
    var args = [].slice.call(arguments, 1);
    return function() {
        var newArgs = args.concat([].slice.call(arguments));
        return fn.apply(null, newArgs);
    };
}

// es6

const curry = (fn, ...args) => (...args2) => fn.apply(null, [].concat(...args, ...args2))

const add = (...args) => {
    return [...args].reduce((a,b) => a + b)
}

var curryAdd = curry(add, 2)
// add 无需再次传递,复用了
curryAdd(1, 2,2) // 3

共勉,准备面试了。

本文使用 mdnice 排版