跳到主要内容

函数✅

什么是执行上下文 ?

答案

执行上下文(Execution Contexts) 是规范定义的内部对象, 用来记录当前代码执行时的所有状态信息,包括 this 的绑定、变量的作用域等。是规范定义的内部状态。

执行上下文包含如下核心组件:

组件目的
通用执行环境状态
code evaluation state追踪代码执行状态,包括暂停和恢复执行。例如 Promsie 的状态,迭代器状态等
Function关联的函数对象(如果是函数执行环境),否则为 null
Realm代码访问 ECMAScript 资源的 Realm 记录。用来做沙箱隔离,比如 Iframe 中访问挂载的 ifram window.Array 和 Iframe 宿主对应的 window.Array 不同
ScriptOrModule代码来源的 Script 记录或 Module 记录,初始化时可能为 null
ECMAScript 代码执行环境额外状态
LexicalEnvironment词法环境,存储作用域链,用于解析标识符。
VariableEnvironment变量环境,存储 var 变量的绑定。
PrivateEnvironment私有环境,存储类的私有字段,若无则为 null
生成器执行环境额外状态
Generator关联的生成器对象,表示正在执行的 Generator。

典型的执行上下文包含如下类型

类型触发条件特点
全局执行上下文脚本首次运行时创建包含规范定义的内置对象,和宿主环境的全局对象,以及自定义的全局信息
函数执行上下文每次函数调用时创建包含函数作用域、this 绑定、参数等信息
Eval 执行上下文eval() 代码执行时作用域取决于调用位置,严格模式下有独立作用域
提示

执行上下文是 ECMAScript Executable Code and Execution Contexts 章节定义的抽象概念。实际开发中无法直接访问或观察执行上下文,引擎的实现也不一定会完全遵从该定义。但是通过深入理解了执行上下文的机制,可以辅助理解作用域链、闭包、this 等 JavaScript 核心语言特性, 对于 V8 可以参考 context 了解上下文包含的核心信息

延伸阅读

说一下 this?

答案

this 是一个关键字,this 的值由当前执行上下文动态绑定,这也是导致 this 的值复杂的原因!this 值指向的核心规则如下

调用方式值的规则注意事项
函数调用this 指向全局对象(非严格模式下)或 undefined(严格模式下)如果函数是作为对象的方法调用,则 this 指向该对象
new 调用this 指向新创建的实例对象new 调用中 this 无法被修改
call/apply 调用this 指向第一个参数指定的对象可以显式改变 this 的指向
bind 调用this 指向第一个参数指定的对象,并返回一个新函数可以显式改变 this 的指向,并预设参数
箭头函数this 指向定义时所在的上下文(词法作用域)箭头函数没有自己的 this,继承外层作用域的 this(这也是为什么无法修改箭头函数 this 的原因),而外层作用域 this 取决于运行时的执行上下文

使用注意事项:

  • 将对象方法作为回调函数或者引用传递时,会导致 this 丢失,因为此时调用方式变成了普通函数调用而不是成员方法调用,此时 this 会指向全局对象或 undefined。
  • 箭头函数的 this 绑定是固定的取决于外层作用域的 this, 但是值任然由执行上下文动态绑定,不要错误的认为箭头函数的 this 是不可变的
  • 严格模式下,未绑定的 this 为 undefined

示例说明

/* eslint-disable no-eval */
/* eslint-disable strict */

describe.skip('this 关键字行为详解', () => {
  // 1. 普通函数调用
  describe('普通函数调用', () => {
    test('严格模式下 this 为 undefined', () => {
      'use strict'
      function fnStrict () { return this }
      expect(fnStrict()).toBeUndefined()
    })
  })

  // 2. 对象方法调用
  describe('对象方法调用', () => {
    const obj = {
      x: 1,
      getX () { return this?.x }
    }
    test('作为对象方法调用,this 指向该对象', () => {
      expect(obj.getX()).toBe(1)
    })
    test('方法赋值给变量后调用,this 丢失,变为全局对象', () => {
      const f = obj.getX
      expect(f()).toBeUndefined() // global.x 可能不存在
    })
  })

  // 3. call/apply 显式绑定
  describe('call/apply 显式绑定', () => {
    function fn () { return this }
    test('call/apply 显式绑定 this', () => {
      const ctx = { a: 1 }
      expect(fn.call(ctx)).toBe(ctx)
      expect(fn.apply(ctx)).toBe(ctx)
    })
  })

  // 4. bind 绑定
  describe('bind 绑定', () => {
    function fn () { return this }
    test('bind 返回新函数,this 永远绑定', () => {
      const ctx = { b: 2 }
      const bound = fn.bind(ctx)
      expect(bound()).toBe(ctx)
    })
    test('bind 后 call/apply 不能再修改 this', () => {
      const ctx = { b: 2 }
      const other = { c: 3 }
      const bound = fn.bind(ctx)
      expect(bound.call(other)).toBe(ctx)
    })
  })

  // 5. new 构造函数调用
  describe('new 构造函数调用', () => {
    function Person (name) {
      this.name = name
    }
    test('new 调用 this 指向新实例', () => {
      const p = new Person('Tom')
      expect(p).toEqual({ name: 'Tom' })
    })
    test('new 优先级高于 bind/call/apply', () => {
      function Foo (x) { this.x = x }
      const obj = { x: 42 }
      const Bound = Foo.bind(obj)
      const f = new Bound(100)
      expect(f).toEqual({ x: 100 })
    })
  })

  // 6. 箭头函数
  describe('箭头函数', () => {
    test('箭头函数 this 继承外层作用域', () => {
      const ctx = { val: 1 }
      function outer () {
        return (() => this)()
      }
      expect(outer.call(ctx)).toBe(ctx)
    })
    test('箭头函数 this 不可被 call/apply/bind 修改', () => {
      const ctx = { val: 2 }
      const arrow = () => this
      // 不能直接比较 this,容易导致 jest 尝试序列化全局对象
      expect(arrow.call(ctx)).toBe(arrow())
      expect(arrow.bind(ctx)()).toBe(arrow())
    })
    test('对象方法中定义箭头函数,this 取决于方法调用时的 this', () => {
      const obj = {
        getThis: function () {
          return (() => this)()
        }
      }
      expect(obj.getThis()).toBe(obj)
      const f = obj.getThis
      expect(f()).toBe(undefined)
    })
  })

  // 7. eval
  describe('eval', () => {
    test('eval 采用外层执行环境的 this', () => {
      const obj = {
        test () {
          return eval('this')
        }
      }
      expect(obj.test()).toBe(obj)
    })
    test('eval 中定义函数,this 规则同普通函数', () => {
      const res = eval('(function(){return this})()')
      expect(res).toBe(undefined)
    })
  })

  // 8. 回调/引用传递 this 丢失
  describe('回调/引用传递 this 丢失', () => {
    const obj = {
      x: 1,
      getX () { return this?.x }
    }
    function callFn (fn) { return fn() }
    test('直接传递方法引用,this 丢失', () => {
      expect(callFn(obj.getX)).toBeUndefined()
    })
    test('通过 bind 绑定 this 保持', () => {
      expect(callFn(obj.getX.bind(obj))).toBe(1)
    })
  })

  // 9. 严格模式下 this
  describe('严格模式下 this', () => {
    test('严格模式下未绑定 this 为 undefined', () => {
      'use strict'
      function f () { return this }
      expect(f()).toBeUndefined()
    })
    test('对象方法严格模式下 this 仍指向对象', () => {
      'use strict'
      const obj = { f () { return this } }
      expect(obj.f()).toBe(obj)
    })
  })
})

Open browser consoleTests

什么是作用域 ?

答案

作用域是指 js 访问变量,函数等标识符的规则,这些规则决定了标识符的可见性和生命周期。 常见的 JS 作用域包括。

  1. 全局作用域:在全局作用域中定义的变量和函数可以在任何地方访问。浏览器中是 window 对象,Node.js 中是 global 对象。
  2. 函数作用域:在函数内部定义的变量和函数只能在该函数内部访问。每次调用函数时,都会创建一个新的函数作用域。
  3. 块级作用域:在 ES6 中引入的块级作用域,使用 letconst 声明的变量只能在其所在的代码块内访问。
  4. 模块作用域:在 ES6 中引入的模块作用域,每个模块都有自己的作用域,模块内的变量和函数默认是私有的,除非显式导出。
  5. with,eval 作用域:with 语句和 eval 会动态在当前作用域中注入新的绑定, 注意在严格模式下不支持 with, 同时 eval 也会创建新的作用域,不会对外部作用域造成影响。

作用域会形成嵌套结构,在访问对应变量或函数等标识符时,会基于作用域链逐级向上找到第一个匹配的标识符名称,所以当存在同名的标识符的时候,内层作用域会覆盖外层作用域的同名标识符。

提示

V8 等引擎支持的作用域类型,详见 ScopeType。在规范中作用域的信息实际上叫做 环境记录(Environment Record), 而作用域链逐级查找的规则,可以查看 GetIdentifierReference 的定义。

参考示例

// scope.test.js
describe('JavaScript Scope 基本概念', () => {
  // 1. 全局作用域
  it('全局作用域变量可全局访问', () => {
    globalThis.globalVar = 42
    expect(globalVar).toBe(42)
    function accessGlobal () {
      return globalVar
    }
    expect(accessGlobal()).toBe(42)
  })

  // 2. 函数作用域
  it('函数作用域变量只能在函数内部访问', () => {
    function foo () {
      const inner = 'inside'
      return inner
    }
    expect(foo()).toBe('inside')
    // @ts-expect-error
    expect(typeof inner).toBe('undefined')
  })

  // 3. 块级作用域
  it('块级作用域 let/const 变量只在块内有效', () => {
    if (true) {
      const blockVar = 100
      const blockConst = 200
      expect(blockVar).toBe(100)
      expect(blockConst).toBe(200)
    }
    // @ts-expect-error
    expect(typeof blockVar).toBe('undefined')
    // @ts-expect-error
    expect(typeof blockConst).toBe('undefined')
  })

  // 4. 模块作用域(模拟,Node.js 环境下每个文件就是模块作用域)
  it('模块作用域变量默认私有', () => {
    const moduleVar = 'private'
    expect(moduleVar).toBe('private')
    // 不能在模块外访问 moduleVar(此测试仅说明,实际无法跨文件访问)
  })

  // 5. with 作用域, ESM 为 strict 模式下不允许使用 with
  // it.skip('with 语句可以动态扩展作用域链', () => {
  //    const obj = { a: 1 };
  //    let result;
  //    with (obj) {
  //       result = a;
  //    }
  //    expect(result).toBe(1);
  // });

  // 6. eval 作用域, ESM 为 strict 模式下 eval 不会对当前作用于产生污染
  //   it('eval 可以动态注入变量到当前作用域', () => {
  //     eval('var y = 20; x = 30;')
  //     expect(x).toBe(30)
  //     expect(y).toBe(20)
  //   })

  // 7. 作用域链查找
  it('作用域链查找最近的同名变量', () => {
    const a = 1
    function outer () {
      const a = 2
      function inner () {
        const a = 3
        return a
      }
      return inner()
    }
    expect(outer()).toBe(3)
  })

  // 8. 作用域链覆盖
  it('内层作用域变量覆盖外层同名变量', () => {
    const v = 'global'
    function test () {
      const v = 'local'
      return v
    }
    expect(test()).toBe('local')
    expect(v).toBe('global')
  })

  // 9. 闭包与作用域
  it('闭包可以访问其创建时的作用域', () => {
    function makeCounter () {
      let count = 0
      return function () {
        count++
        return count
      }
    }
    const counter = makeCounter()
    expect(counter()).toBe(1)
    expect(counter()).toBe(2)
  })
})

Open browser consoleTests

什么是闭包?

答案

闭包是指内部函数持有对外部函数作用域变量的引用,即使外部函数已经执行完毕,这些变量也不会被垃圾回收,从而实现数据的持久化和封装。

闭包的典型使用场景包括

  1. 实现私有变量和方法,保护数据不被外部直接访问
  2. 回调和异步操作中保存状态
  3. 函数柯里化和高阶函数
  4. 实现缓存功能

使用必包时需要注意以下几点:

  1. 闭包会导致其引用的外部变量常驻内存,可能造成内存泄漏,尤其是在循环或大量创建闭包时需注意释放引用。
  2. 闭包访问的变量是引用而非副本,若变量被修改,所有闭包实例都会感知变化。
提示

规范中并未详细提及必包的概念,但是在 ECMAScript Function Objects 函数定义中包含了 [[Environment]] 的内置属性,这使得嵌套函数有能力通过,Environment Records[[OuterEnv]] 属性访问到外层的执行上下文的变量。此外在引擎具体的实现时,只会对消费到的变量进行闭包化处理,以此减少内存开销。

示例说明

describe('闭包(Closure)', () => {
  test('基本使用:内部函数访问外部变量', () => {
    function makeCounter () {
      let count = 0
      return function () {
        count++
        return count
      }
    }
    const counter = makeCounter()
    expect(counter()).toBe(1)
    expect(counter()).toBe(2)
    expect(counter()).toBe(3)
  })

  test('实现私有变量:外部无法直接访问', () => {
    function createSecret (secret) {
      return {
        getSecret: function () {
          return secret
        }
      }
    }
    const obj = createSecret('mySecret')
    expect(obj.getSecret()).toBe('mySecret')
    expect(obj.secret).toBeUndefined()
  })

  test('回调和异步操作中保存状态', done => {
    function delayedLogger (msg) {
      setTimeout(function () {
        expect(msg).toBe('hello')
        done()
      }, 10)
    }
    delayedLogger('hello')
  })

  test('函数柯里化:通过闭包保存参数', () => {
    function add (a) {
      return function (b) {
        return a + b
      }
    }
    const add5 = add(5)
    expect(add5(3)).toBe(8)
    expect(add(2)(4)).toBe(6)
  })

  test('实现缓存功能', () => {
    function memoize (fn) {
      const cache = {}
      return function (x) {
        if (cache[x] !== undefined) return cache[x]
        cache[x] = fn(x)
        return cache[x]
      }
    }
    const square = memoize(x => x * x)
    expect(square(4)).toBe(16)
    expect(square(4)).toBe(16) // 来自缓存
  })

  test('注意事项:闭包访问的是引用,变量变化会影响所有闭包', () => {
    function makeFuncs () {
      const arr = []
      for (var i = 0; i < 3; i++) {
        arr.push(function () {
          return i
        })
      }
      return arr
    }
    const funcs = makeFuncs()
    // 由于 var 没有块级作用域,所有函数都引用同一个 i
    expect(funcs[0]()).toBe(3)
    expect(funcs[1]()).toBe(3)
    expect(funcs[2]()).toBe(3)
  })

  test('注意事项:使用 let 可避免变量提升带来的闭包陷阱', () => {
    function makeFuncs () {
      const arr = []
      for (let i = 0; i < 3; i++) {
        arr.push(function () {
          return i
        })
      }
      return arr
    }
    const funcs = makeFuncs()
    expect(funcs[0]()).toBe(0)
    expect(funcs[1]()).toBe(1)
    expect(funcs[2]()).toBe(2)
  })

  test('注意事项:闭包可能导致内存泄漏', () => {
    let leaked
    function outer () {
      const large = new Array(10000).fill(1)
      leaked = function () {
        return large
      }
    }
    outer()
    expect(typeof leaked).toBe('function')
    // large 数组不会被回收,直到 leaked 解除引用
    leaked = null // 解除引用,便于垃圾回收
  })
})

Open browser consoleTests

构造函数和普通调用有什么区别?

答案

普通函数是指直接调用的函数,而构造函数是通过 new 关键字调用的函数。从规范的角度普通函数调用内部的 [[Call]] 方法,构造函数调用内部的 [[Construct]] 方法。

核心区别实际上就是执行上下文的状态差异,包含

  1. this 的指向
    • 普通函数调用时,this 指向全局对象(非严格模式下)或 undefined(严格模式下)。
    • 构造函数调用时,this 指向新创建的实例对象。
  2. 返回值
    • 普通函数调用时,返回值是函数执行的结果。
    • 构造函数调用时,返回值是新创建的实例对象,除非显式返回一个对象。
  3. 额外属性注入
    • 构造函数会注入 super, new.target 等额外属性到执行上下文中。

示例说明

// constructor-call.test.js

describe('构造函数与普通函数调用的区别', () => {
  // 1. this 的指向
//   test('普通函数调用时 this 指向全局对象(非严格模式)', () => {
//     function foo () {
//       return this
//     }
//     expect(foo()).toBe(global) // node 环境下全局对象为 global
//   })

  test('普通函数调用时 this 为 undefined(严格模式)', () => {
    'use strict'
    function foo () {
      return this
    }
    expect(foo()).toBeUndefined()
  })

  test('构造函数调用时 this 指向新创建的实例对象', () => {
    function Foo () {
      this.value = 42
    }
    const obj = new Foo()
    expect(obj.value).toBe(42)
    expect(obj instanceof Foo).toBe(true)
  })

  // 2. 返回值
  test('普通函数调用返回函数执行结果', () => {
    function foo () {
      return 123
    }
    expect(foo()).toBe(123)
  })

  test('构造函数调用返回新创建的实例对象', () => {
    function Foo () {
      this.x = 1
    }
    const obj = new Foo()
    expect(obj.x).toBe(1)
    expect(obj instanceof Foo).toBe(true)
  })

  test('构造函数显式返回对象时,返回该对象', () => {
    function Foo () {
      this.x = 1
      return { y: 2 }
    }
    const obj = new Foo()
    expect(obj.x).toBeUndefined()
    expect(obj.y).toBe(2)
  })

  test('构造函数显式返回非对象时,仍返回实例对象', () => {
    function Foo () {
      this.x = 1
      return 123
    }
    const obj = new Foo()
    expect(obj.x).toBe(1)
    expect(obj instanceof Foo).toBe(true)
  })

  // 3. 额外属性注入
  test('构造函数调用时 new.target 指向构造函数本身', () => {
    function Foo () {
      return new.target
    }
    const result = new Foo()
    expect(result).toBe(Foo)
  })

  test('普通函数调用时 new.target 为 undefined', () => {
    function foo () {
      return new.target
    }
    expect(foo()).toBeUndefined()
  })

  test('class 构造函数中 super 可用', () => {
    class Parent {
      constructor () {
        this.parentValue = 1
      }
    }
    class Child extends Parent {
      constructor () {
        super()
        this.childValue = 2
      }
    }
    const c = new Child()
    expect(c.parentValue).toBe(1)
    expect(c.childValue).toBe(2)
  })

  // 其他关键点
  test('构造函数 prototype 属性', () => {
    function Foo () {}
    const obj = new Foo()
    expect(Object.getPrototypeOf(obj)).toBe(Foo.prototype)
  })

  test('普通函数调用不会自动设置原型', () => {
    function Foo () {}
    const result = Foo()
    expect(result).toBeUndefined()
  })
})

Open browser consoleTests

JS 有几种创建函数的方式,有哪些区别 ?

答案
创建方式语法特点
函数声明function functionName() {}具名函数,提升到作用域顶部,可以在声明之前调用
函数表达式const functionName = function() {}匿名函数,不能提升,必须在定义后调用
箭头函数const functionName = () => {}匿名函数,简洁语法,没有自己的 this,继承外层作用域的 this
函数构造器new Function('arg1', 'arg2', 'return ...')动态创建函数,参数为字符串形式,不推荐使用,性能较差
evaleval('functionName = function() {}')动态执行字符串代码,存在安全隐患和性能问题,不推荐使用
类方法class ClassName { methodName() {} }类的实例方法,具名函数,提升到类定义之前,可以在类实例上调用

示例说明

// create-function.test.js

describe('函数创建方法及区别', () => {
  // 函数声明
  test('函数声明:function functionName() {}', () => {
    expect(typeof funcDecl).toBe('function')
    // 可以在声明前调用, 申明会提升
    expect(funcDecl()).toBe('decl')
    function funcDecl () {
      return 'decl'
    }
    // 可以在声明前调用
    expect(funcDecl()).toBe('decl')
  })

  // 函数表达式
  test('函数表达式:const functionName = function() {}', () => {
    const funcExpr = function () {
      return 'expr'
    }
    expect(typeof funcExpr).toBe('function')
    expect(funcExpr()).toBe('expr')
    // 不能在定义前调用
    let error
    try {
      funcExprBefore()
    } catch (e) {
      error = e
    }
    expect(error).toBeInstanceOf(ReferenceError)
    const funcExprBefore = function () {}
  })

  // 箭头函数
  test('箭头函数:const functionName = () => {}', () => {
    const arrowFunc = () => 'arrow'
    expect(typeof arrowFunc).toBe('function')
    expect(arrowFunc()).toBe('arrow')
    // 没有自己的 this
    const obj = {
      value: 42,
      getValue: () => this?.value,
      getValueNormal () { return this?.value }
    }
    expect(obj.getValue()).toBe(undefined)
    expect(obj.getValueNormal()).toBe(42)
  })

  // 函数构造器
  test('函数构造器:new Function()', () => {
    const func = new Function('a', 'b', 'return a + b')
    expect(typeof func).toBe('function')
    expect(func(1, 2)).toBe(3)
  })

  // eval
  test('eval 创建函数', () => {
    let evalFunc
    eval('evalFunc = function() { return "eval"; }')
    expect(typeof evalFunc).toBe('function')
    expect(evalFunc()).toBe('eval')
  })

  // 类方法
  test('类方法:class ClassName { methodName() {} }', () => {
    class MyClass {
      method () { return 'class method' }
    }
    const instance = new MyClass()
    expect(typeof instance.method).toBe('function')
    expect(instance.method()).toBe('class method')
  })
})

Open browser consoleTests

用过哪些函数对象的方法?

答案
分类名称说明常用
静态方法Function.prototype所有函数的原型对象✔️
Function.length形参个数✔️
Function.name函数名✔️
Function.apply()指定 this 调用函数,参数为数组✔️
Function.call()指定 this 调用函数,参数逐个传递✔️
Function.bind()返回绑定 this 的新函数✔️
Function.toString()返回函数源码字符串✔️
Function.isGeneratorFunction()判断是否生成器函数(部分环境支持)
原型方法Function.prototype.constructor构造函数引用自身✔️
Function.prototype.apply()同上,所有函数实例可用✔️
Function.prototype.call()同上,所有函数实例可用✔️
Function.prototype.bind()同上,所有函数实例可用✔️
Function.prototype.toString()同上,所有函数实例可用✔️
属性arguments函数内部类数组对象,包含所有实参(非箭头函数)✔️
caller调用当前函数的函数(非严格模式下)
prototype构造函数的原型对象,实例共享✔️

函数表达式和函数声明的区别?

答案
区别函数声明 (Function Declaration)函数表达式 (Function Expression)
语法function foo() {}const foo = function() {}const foo = () => {}
是否提升是,声明提升到作用域顶部否,只有赋值语句被提升,函数体不会提升
调用时机可以在声明前调用只能在定义后调用
是否必须命名必须有函数名可匿名或具名
作用域当前作用域赋值变量的作用域
this 绑定动态绑定动态绑定(普通表达式)/ 词法绑定(箭头函数)
调试栈显示显示函数名匿名函数无名,具名表达式有名
用途通常用于全局/局部函数定义常用于回调、IIFE、赋值等

说一下箭头函数?

答案

箭头函数(Arrow Function)是 ES6 引入的一种简洁函数写法,主要为了解决传统函数中 this 指向混乱、代码冗长等问题。

箭头函数的语法如下:

// 采用  => 定义箭头函数
const add = (a, b) => a + b

箭头函数的特点:

特性普通函数箭头函数
this调用时动态绑定定义时静态绑定
arguments有自己的没有自己的
作为构造函数可以(可 new)不可以
prototype 属性没有
yield可用(可生成器)不可用
super/new.target支持不支持

示例说明

// arrow.test.js
describe('Arrow Function 特点', () => {
  test('箭头函数语法更简洁', () => {
    const add = (a, b) => a + b
    expect(add(2, 3)).toBe(5)
  })

  test('箭头函数没有自己的 this', () => {
    function Counter () {
      this.num = 0
      setTimeout(() => {
        this.num++
      }, 10)
    }
    const counter = new Counter()
    return new Promise((resolve) => {
      setTimeout(() => {
        expect(counter.num).toBe(1)
        resolve()
      }, 20)
    })
  })

  test.skip('箭头函数没有 arguments 对象', () => {
    const fn = () => typeof arguments
    expect(fn()).toBe('undefined')
  })

  test.skip('箭头函数不能作为构造函数', () => {
    const Arrow = () => {}
    expect(() => new Arrow()).toThrow(TypeError)
  })

  test('箭头函数不能使用 super', () => {
    class Parent {
      constructor () {
        this.value = 1
      }
    }
    class Child extends Parent {
      constructor () {
        super()
        this.getValue = () => {
          // super.value; // 不能直接用 super
          return this.value
        }
      }
    }
    const c = new Child()
    expect(c.getValue()).toBe(1)
  })

  test('箭头函数适合用作回调函数', () => {
    const arr = [1, 2, 3]
    const squared = arr.map(x => x * x)
    expect(squared).toEqual([1, 4, 9])
  })
})

Open browser consoleTests

call,apply,bind 区别?

答案

call、apply 和 bind 都是 JavaScript 中用于改变函数执行上下文(即 this 指向)的方法。

方法语法格式作用返回值传参方式是否立即执行支持版本
callfunc.call(thisArg, arg1, arg2, ...)改变 this 指向并调用函数函数执行结果依次传递参数ES3+
applyfunc.apply(thisArg, [argsArray])改变 this 指向并调用函数函数执行结果参数以数组形式传递ES3+
bindfunc.bind(thisArg[, arg1[, arg2[, ...]]])创建绑定 this 的新函数新函数依次传递参数(可预设部分参数)ES5+
describe('Function.prototype.call, apply, bind', () => {
  function greet (greeting, punctuation) {
    return `${greeting}, ${this.name}${punctuation}`
  }

  const context = { name: 'Alice' }

  test('call 以给定的 this 和参数调用函数', () => {
    const result = greet.call(context, 'Hello', '!')
    expect(result).toBe('Hello, Alice!')
  })

  test('apply 以给定的 this 和参数数组调用函数', () => {
    const result = greet.apply(context, ['Hi', '?'])
    expect(result).toBe('Hi, Alice?')
  })

  test('bind 返回一个新的函数,绑定 this 和可选参数', () => {
    const boundGreet = greet.bind(context, 'Hey')
    expect(boundGreet('~')).toBe('Hey, Alice~')
  })

  test('bind 可以实现部分应用,并在之后调用', () => {
    const boundGreet = greet.bind(context, 'Welcome')
    const result = boundGreet('!!!')
    expect(result).toBe('Welcome, Alice!!!')
  })

  test('call/apply 不会创建新函数,bind 会', () => {
    expect(typeof greet.call).toBe('function')
    expect(typeof greet.apply).toBe('function')
    const bound = greet.bind(context)
    expect(typeof bound).toBe('function')
    expect(bound).not.toBe(greet)
  })

  test('bind 返回的函数可以作为构造函数使用(忽略 thisArg)', () => {
    function Person (name) {
      this.name = name
    }
    const BoundPerson = Person.bind({ notUsed: true })
    const p = new BoundPerson('Bob')
    expect(p).toBeInstanceOf(Person)
    expect(p.name).toBe('Bob')
  })
})

Open browser consoleTests

IIFE 是什么?

答案

IIFE 是立即执行函数表达式。

要理解 IIFE 必须知晓函数申明和函数表达式的区别。一个最简单的判别方法是若 function 关键字未出现一行的开头则且不属于上一行的组成部分则为函数申明否则为函数表达式。

所以只要函数表达式被立即调用则称之为 IIFE.

此外函数表达式和函数申明具有如下区别

  1. 函数表达式不会被提升
  2. 函数表达式的标识符是可省略的
  3. 函数表达式的标识符作用域为该函数体,外部执行环境无法使用
  4. 具名函数表达式的名称作为函数名,匿名函数名称取决于申明方式

推荐阅读:

eval 了解多少?

答案

eval() 是 JavaScript 的全局函数,用于将字符串当作代码在当前作用域内解析并执行。它的基本用法如下:

const x = 1
const y = 2
const result = eval('x + y') // 3
  • 动态执行代码:可以在运行时动态生成并执行 JavaScript 代码。
  • 访问当前作用域eval 执行的代码可以访问当前作用域的变量和函数。
  • 动态表达式计算:常用于需要根据字符串动态计算表达式的场景。

注意事项

  1. 安全风险极高eval 能执行任意字符串代码,若字符串内容可控,极易被注入恶意代码,造成 XSS 等安全漏洞。
  2. 性能较差:每次调用 eval 都需要解析和编译字符串,无法享受编译优化,执行效率远低于静态代码。
  3. 调试困难eval 生成的代码在调试和错误定位时不直观,增加维护难度。
  4. 作用域混淆eval 代码可访问和修改当前作用域变量,易引发作用域混乱和难以追踪的 bug。
  5. 严格模式限制:在严格模式下,eval 的作用域更加受限,不能影响外部作用域。
  • eval 代码在运行时动态解析和编译,无法被引擎提前优化,频繁使用会显著拖慢性能。
  • 简单、偶尔使用时影响不大,但复杂或高频场景下应避免。
  • 静态代码可被编译器优化,执行更快且资源消耗更低。

优势

  • 极高灵活性:可动态生成和执行代码,适合表达式求值、元编程等特殊场景。
  • 可访问当前作用域:与 Function 构造器不同,eval 可直接操作当前作用域变量。
  • 动态命名空间操作:可用于动态定义变量、函数等。

eval 功能强大但风险极高,实际开发中应极力避免使用。绝大多数需求可用更安全的替代方案(如 JSON.parseFunction 构造器、模板引擎等)实现。仅在完全可控、无安全隐患的场景下谨慎使用。

参考资料:

49%