JS的上下文与闭包

变量 作用域 与内存

1.1原始值和引用值

JavaScript 中的变量分为两种类型:原始值和引用值。

原始值包括 null、undefined、布尔值、数字和字符串,当我们创建一个原始值的变量时,变量会直接存储该值的副本。

引用值包括对象、数组和函数,当我们创建一个引用值的变量时,变量实际上存储的是该值在内存中的地址,也就是指向该值的指针。当我们修改引用值的属性或元素时,实际上是修改了该值在内存中的实际内容。

下面是一个示例代码,展示了原始值和引用值的区别:

var a = 1; // a 是一个原始值变量,它存储了数字 1 的副本
var b = a; // b 是一个原始值变量,它存储了数字 1 的副本
a = 2; // 修改 a 的值为 2
console.log(a, b); // 输出 2, 1,a 的值已经改变,b 的值不受影响
var c = { name: 'Alice' }; // c 是一个引用值变量,它存储了一个对象的地址
var d = c; // d 是一个引用值变量,它存储了 c 的地址
c.name = 'Bob'; // 修改 c 对象的 name 属性为 'Bob'
console.log(c.name, d.name); // 输出 'Bob', 'Bob',d 的值也受到了影响

在上面的代码中,我们创建了一个原始值变量 a 和一个引用值变量 c,并将它们的值复制给了变量 b 和变量 d。当我们修改变量 a 时,变量 b 的值不受影响,因为它存储的是 a 的副本。但是当我们修改变量 `c的时候,发现d也发生了改变(原因是c和d是引用值,指向同一个存储对象的堆内存地址)

1.2 原始值和引用值的参数传递

在 JavaScript 中,函数参数可以是原始值或者引用值。当我们将一个变量作为参数传递给函数时,实际上是将该变量的值传递给了函数。但是,对于原始值和引用值,它们的传递方式是不同的。

对于原始值,传递的是该值的副本,也就是说,在函数内部修改原始值的变量不会影响原始值的变量在函数外部的值。例如:

functionfoo(a) {
  a = 2; // 修改 a 的值为 2
  console.log(a); // 输出 2
}
var b = 1;
foo(b); // 将 b 的值 1 传递给函数 foo
console.log(b); // 输出 1,函数 foo 不会修改 b 的值

在上面的代码中,我们将变量 b 的值作为参数传递给了函数 foo。在函数 foo 中,我们修改了参数 a 的值为 2。但是,这个修改不会影响变量 b 在函数外部的值,因为传递的是 b 的副本。

对于引用值,传递的是该值在内存中的地址,也就是说,在函数内部修改引用值的属性或元素会影响原始值的变量在函数外部的值。例如:

functionfoo(obj) {
  obj.name = 'Alice'; // 修改 obj 对象的 name 属性为 'Alice'
  console.log(obj.name); // 输出 'Alice'
}

var person = { name: 'Bob' };
foo(person); // 将 person 对象的地址传递给函数 foo
console.log(person.name); // 输出 'Alice',函数 foo 修改了 person 对象的 name 属性

在上面的代码中,我们将对象 person 的地址作为参数传递给了函数 foo。在函数 foo 中,我们修改了参数 obj 对象的 name 属性为 'Alice'。这个修改会影响变量 person 在函数外部的值,因为传递的是 person 对象在内存中的地址,而不是该对象的副本。

总之,JavaScript 中的原始值和引用值在传递参数时的行为是不同的。对于原始值,传递的是该值的副本,对于引用值,传递的是该值在内存中的地址。对于原始值,修改参数的值不会影响原始值的变量在函数外部的值;对于引用值,修改参数的属性或元素会影响原始值的变量在函数外部的值。

要注意参数传递传的不是引用(指针),而是数值,但是对于传入的引用值而言,传入的是内存地址,所以对其更改会直接修改内存中的对象。

1.3 执行上下文与变量对象

执行上下文是 JavaScript 引擎在执行代码时创建的一个内部数据结构,用于存储代码执行所需的信息。每当 JavaScript 执行一段可执行代码时,都会创建一个新的执行上下文。

作用域链是由当前执行环境和所有包含的父级执行环境的变量对象组成的链式结构,它用于决定变量和函数的作用域。当 JavaScript 引擎在执行代码时访问一个变量或函数时,会先在当前执行上下文的变量对象中查找,如果找不到,就会在作用域链中的下一个变量对象中查找,直到找到该变量或函数或者到达作用域链的顶部。

变量对象是执行上下文中的一个特殊对象,用于存储执行上下文中定义的变量和函数。在函数执行之前,JavaScript 引擎会创建一个变量对象,并将其添加到执行上下文的作用域链的顶部。变量对象包括了以下属性:

函数参数:变量对象的属性名是函数参数的名称,属性值是函数参数的值。

函数声明:变量对象的属性名是函数名称,属性值是函数的引用。函数声明会被提升到变量对象的顶部,因此在函数内部可以在声明之前调用函数。

变量声明:变量对象的属性名是变量名称,属性值为 undefined。变量声明也会被提升到变量对象的顶部。

变量对象的创建和初始化是在进入执行上下文时完成的。在函数执行过程中,JavaScript 引擎会根据作用域链和变量对象来查找和修改变量的值。

以下是一个函数执行环境的伪代码示例,展示了函数执行环境的结构:

// 创建函数执行环境
var executionContext = createExecutionContext(foo);
// 函数执行环境对象的结构
var ExecutionContext = {
  lexicalEnvironment: {
    // 作用域链
   scopeChain: {
      // 当前执行上下文的变量对象
       variableObject: variableObject,
      // 父级执行上下文的作用域链
       outerScope: outerScope
    },
    // this 值
    thisValue: thisValue
  },
  variableEnvironment: {
    // 变量对象variableObject: 
     variableObject
  },
  // 代码执行状态
   codeStatus: codeStatus,
  // 代码执行位置
   codePosition: codePosition,
  // 执行上下文类型
   contextType: contextType,
  // 父级执行上下文
   outerContext: outerContext
};

// 创建变量对象
var variableObject = createVariableObject(executionContext);

// 添加函数参数到变量对象中
variableObject.addParameter('a', 1);
variableObject.addParameter('b', 2);

// 添加变量声明到变量对象中
variableObject.addVariableDeclaration('c', undefined);

// 将变量对象添加到执行上下文中
executionContext.addVariableObject(variableObject);

// 创建作用域链
var scopeChain = createScopeChain(executionContext);

// 向作用域链添加全局变量和函数
scopeChain.addVariable('console', console);
scopeChain.addVariable('setTimeout', setTimeout);

// 向作用域链添加局部变量和函数
scopeChain.addVariable('a', 1);
scopeChain.addVariable('b', 2);
scopeChain.addVariable('c', undefined);
scopeChain.addFunctionDeclaration('bar', bar);

// 执行函数
var result = foo(1, 2);

// 销毁函数执行环境
destroyExecutionContext(executionContext);

下面是一个示例代码,展示了执行上下文、作用域链和变量对象的用法:

var a = 1;
function foo(b) {
  var c = 2;
  function bar(d) {
   var e = 3; 
   console.log(a, b, c, d, e); 
  } 
  bar(4); 
}
foo(5);

在上面的代码中,我们定义了一个全局变量 a和一个函数 foo。在函数 foo 中,我们定义了一个局部变量 c 和另一个函数 `bar`,并将函数 `bar` 的引用返回。在函数 `bar` 中,我们定义了两个局部变量 d 和 e。

当我们调用函数 foo时,JavaScript 引擎会创建一个执行上下文,并将变量对象添加到作用域链的顶部。在函数 foo 中,我们可以访问全局变量 a 和函数参数 b,以及局部变量 c。当我们调用函数 bar 时,JavaScript 引擎会创建另一个执行上下文,并将该执行上下文的变量对象添加到作用域链的顶部,形成一个作用域链。在函数 bar 中,我们可以访问全局变量 a、函数参数 b`和局部变量 c,因为它们都在作用域链中。

总之,执行上下文、作用域链和变量对象是 JavaScript 中非常重要的概念,它们决定了变量和函数的可见性和作用域。理解执行上下文、作用域链和变量对象的工作原理,可以更好地理解 JavaScript 的作用域和闭包机制,从而编写更加优雅和高效的代码。

以下是代码的作用域链结构:

全局执行上下文
  变量对象:{
    a: 1,
    foo: <function>
  }
  作用域链:[变量对象]

foo 执行上下文
  变量对象:{
    b: 5,
    c: 2,
    bar: <function>
  }
  作用域链:[变量对象, 全局变量对象]

bar 执行上下文
  变量对象:{
    d: 4,
    e: 3
  }
  作用域链:[变量对象, foo 变量对象, 全局变量对象]

上述作用域链结构的说明如下:

  • 全局执行上下文的作用域链只包含全局变量对象本身。
  • 函数 foo 的作用域链包含了函数 foo 的变量对象和全局变量对象,其中函数 foo 的变量对象位于作用域链的顶部。
  • 函数 bar 的作用域链包含了函数 bar 的变量对象、函数 foo 的变量对象和全局变量对象,其中函数 bar 的变量对象位于作用域链的顶部。

以下是上述作用域链结构的图形化表示:

1.4 作用域链的增强

作用域链的增强是指在函数执行过程中,JavaScript 引擎会将函数的父级执行环境的变量对象添加到当前执行环境的作用域链中,从而扩展了当前执行环境的作用域链。

作用域链的增强通常发生在函数嵌套的情况下。当函数 A 中定义了函数 B,而函数 B 中又使用了函数 A 中的变量时,JavaScript 引擎会将函数 A 的变量对象添加到函数 B 的作用域链中,从而使函数 B 可以访问函数 A 中的变量。这种作用域链的增强被称为词法作用域。

以下是一个示例代码,展示了作用域链的增强:

function foo() {
  var a = 1;
  function bar() {
    var b = 2;
    console.log(a, b);
  }
  bar();
}

foo();

在上面的代码中,函数 bar 使用了函数 foo 中的变量 a。在函数 bar 内部,JavaScript 引擎会将函数 foo 的变量对象添加到作用域链中,从而使变量 a 可以被访问到。

当我们调用函数 foo 时,JavaScript 引擎会创建一个执行上下文,并将变量对象添加到作用域链的顶部。在函数 foo 中,我们定义了一个局部变量 a 和另一个函数 bar,在函数 bar 中,我们定义了一个局部变量 b。当我们调用函数 bar 时,JavaScript 引擎会创建另一个执行上下文,并将该执行上下文的变量对象添加到作用域链的顶部,形成一个作用域链。在函数 bar 中,我们可以访问变量 ab,因为它们都在作用域链中。

1.5 内存回收机制

JavaScript 中的内存回收机制是自动的,由 JavaScript 引擎负责管理。当变量不再被引用时,引擎会自动将其标记为垃圾,并将其占用的内存释放回内存池,以便下次使用。

JavaScript 引擎使用的垃圾回收算法是标记清除算法。该算法的基本思想是通过标记所有仍然在使用的变量,然后将未标记的变量标记为垃圾并回收其占用的内存。

具体来说,当 JavaScript 引擎需要释放内存时,它会首先找到所有根变量(即全局变量和当前调用栈中的变量),然后从这些变量开始遍历内存中的对象和变量,将所有能够访问到的变量标记为“存活”,而未被标记的变量则被认为是垃圾并被回收。

此外,JavaScript 引擎还实现了一些优化技术来提高垃圾回收的效率,例如分代回收和增量回收。分代回收是指将内存中的对象分为几个代,新创建的对象放入第一代,当第一代满时就将其中存活的对象移到第二代,以此类推。这样可以减少每次垃圾回收的时间和开销。增量回收是指将垃圾回收过程分为若干个小步骤,每次执行一小步,然后让 JavaScript 代码执行一会儿,再继续执行下一小步,以此来减少垃圾回收对 JavaScript 代码执行的影响。

总之,JavaScript 中的内存回收机制是自动的,并且由 JavaScript 引擎负责管理。通过标记清除算法以及分代回收和增量回收等优化技术,JavaScript 引擎可以高效地管理内存,避免内存泄漏和其他内存相关的问题。但是,开发人员仍然需要注意避免创建不必要的对象,避免循环引用等问题,以便帮助 JavaScript 引擎更好地管理内存。

闭包

2.1 基本概念

闭包是指一个函数可以访问另一个函数内部的变量和函数,即使在外部函数执行结束之后,内部函数仍然可以访问外部函数的作用域。在 JavaScript 中,由于函数可以作为返回值被传递和存储,这就为闭包的实现提供了可能。

闭包的实现基于函数作用域和作用域链的特性。当函数被执行时,会创建一个新的执行上下文,其中包括变量对象和作用域链。当函数内部访问一个变量时,JavaScript 引擎会首先在当前函数的变量对象中查找该变量,如果找不到,就会在作用域链中查找,直到找到该变量或到达作用域链的顶部。如果内部函数引用了外部函数的变量,那么当外部函数执行结束后,其变量对象就理应会被销毁,但是由于内部函数还引用了这些变量,所以这些变量就会被保存在内存中,直到内部函数也被销毁。

以下是一个示例代码,展示了闭包的实现:

function outer() {
  var count = 0;
  function inner() {
    count++;
    console.log(count);
  }
  return inner;
}

var fn = outer();
fn(); // 输出 1
fn(); // 输出 2
fn(); // 输出 3

在上面的代码中,函数 outer 返回了一个内部函数 inner,该函数引用了外部函数 outer 中的变量 count。当我们调用函数 outer 并将其返回值赋值给变量 fn 时,实际上是将内部函数 inner 的引用保存在了变量 fn 中。当我们调用 fn 时,内部函数 inner 会访问外部函数 outer 中的变量 count,并将其自增后输出。由于内部函数 inner 引用了外部函数 outer 中的变量 count,因此变量 count 不会在外部函数执行结束后被销毁,而是一直被保存在内存中,直到内部函数 inner 也被销毁。

闭包的使用可以让代码更加简洁和灵活,常见的应用场景包括封装私有变量、实现模块化、延迟函数执行等。但是,闭包也可能导致内存泄漏和性能问题,因此需要注意避免滥用闭包,尽量减少对外部变量的引用,以便减少内存占用和提高执行效率。

2.2 闭包的延迟执行和参数记忆

function counter() {
  let count = 0;
  function increment() {
    count++;
    console.log(count);
  }
  return increment;
}

const counter1 = counter();
counter1(); // 输出 1
counter1(); // 输出 2
const counter2 = counter();
counter2(); // 输出 1

在这个例子中,我们还可以看到闭包的另外两个主要作用:延迟执行和参数记忆。

延迟执行:在这个例子中,counter() 函数返回了一个内部定义的 increment() 函数,而 count 变量则被保存在 counter() 函数的闭包中。由于 increment() 函数可以访问到 count 变量,因此我们可以通过调用counter() 函数返回的 increment() 函数来递增并输出 count 的值。

这种方式实现了函数的延迟执行,即我们可以将一个函数保存在变量中,以便在需要时再执行它。在这个例子中,counter1() 函数的执行被延迟到了我们实际调用它的时候。

参数记忆:

闭包能实现参数记忆,是因为在闭包中定义的函数可以访问到父函数中的变量,这些变量在父函数执行完毕后并不会被销毁,而是保存在闭包中。所以在这个例子中 count 变量被保存在闭包中,因此每次调用 increment 函数时,它都能够记住上一次调用时的 count 值,从而实现了参数记忆的功能。

注意:

每次调用 counter 函数都会创建一个新的闭包环境,因此 counter1()counter2() 访问的是不同的闭包。在调用 counter2() 时,count 变量被重新初始化为 0,因此第一次调用 increment 函数时输出的是 1

如果想要实现多个闭包共享同一个变量,可以将变量定义在父函数的外部,以便所有闭包都可以访问到该变量。例如:

let count = 0;
function counter() {
  function increment() {
    count++;
    console.log(count);
  }
  return increment;
}

const counter1 = counter();
counter1(); // 输出 1
counter1(); // 输出 2
const counter2 = counter();
counter2(); // 输出 3

在这个例子中,count 变量被定义在 counter 函数的外部,从而使得所有闭包都可以访问到该变量。因此,每次调用 counter1()counter2() 函数时,它们都能够共享同一个 count 变量,最终输出的结果分别是 123

2.3 闭包的应用场景

以下是几个常见的闭包应用场景的示例代码:

封装私有变量

function createCounter() {
  var count = 0;
  return {
    increment: function() {
      count++;
      console.log(count);
    },
    decrement: function() {
      count--;
      console.log(count);
    }
  };
}
var counter = createCounter();
counter.increment(); // 输出 1
counter.increment(); // 输出 2
counter.decrement(); // 输出 1

在上面的代码中,函数 createCounter 返回了一个包含 incrementdecrement 方法的对象,这些方法可以访问外部函数 createCounter 中的变量 count,并且可以修改其值,实现了对变量的封装。

实现函数柯里化

函数柯里化指的是将接受多个参数的函数转化为接收一个参数的函数序列的过程。通过使用闭包,我们可以实现函数柯里化。例如:

function add(x) {
  return function(y) {
    return x + y;
  };
}

const add5 = add(5);
console.log(add5(3)); // 输出8
console.log(add5(7)); // 输出12

在这个例子中,add() 函数返回了一个函数,该函数可以访问到 x 变量。我们将 add() 函数的第一个参数 x 传入后,返回一个新的函数 add5,该函数可以接收一个参数 y,并返回 x + y 的值。由于 add5() 函数可以访问到 x 变量,因此我们可以通过调用 add5() 函数并传入一个参数 y 来计算 x + y 的值。

实现缓存

通过使用闭包,我们可以实现一个简单的缓存功能,从而将一些计算结果缓存起来,避免重复计算。例如:

function createCache() {
  let cache = {};
  return function(key, fn) {
    if (!cache[key]) {
      cache[key] = fn();
    }
    return cache[key];
  };
}

const cache = createCache();
const result1 = cache('foo', function() {
  console.log('calculating foo');
  return Math.random();
});
console.log(result1); // 输出一个随机数
const result2 = cache('foo', function() {
  console.log('calculating foo');
  return Math.random();
});
console.log(result2); // 输出相同的随机数,并且不输出 "calculating foo"

在这个例子中,我们定义了一个 createCache() 函数,该函数返回一个新的函数。新函数可以接受一个键名 key 和一个计算函数 fn,并将计算函数的结果缓存起来。如果缓存中已经存在该键名,则直接返回缓存中的结果,否则调用计算函数并保存结果到缓存中。由于 cache 变量被保存在闭包中,因此它可以在多次调用新函数之间共享。

通过使用闭包,我们可以实现一个简单的缓存功能,从而避免重复计算和提高代码的性能。这种模式在一些数据处理和复杂计算等场景中有广泛的应用。

实现模块化

  1. 实现模块化编程

通过使用闭包,我们可以实现模块化编程,将代码分割成独立的模块,从而使得代码更易于维护和扩展。例如:

const calculator = (function() {
  let _result = 0;
  
  function add(num) {
    _result += num;
  }
  
  function subtract(num) {
    _result -= num;
  }
  
  function multiply(num) {
    _result *= num;
  }
  
  function divide(num) {
    _result /= num;
  }
  
  function getResult() {
    return _result;
  }
  
  function reset() {
    _result = 0;
  }
  
  return {
    add,
    subtract,
    multiply,
    divide,
    getResult,
    reset
  };
})();

calculator.add(5);
calculator.multiply(2);
console.log(calculator.getResult()); // 输出10

在这个例子中,我们使用了立即执行函数表达式(IIFE)来创建一个模块化的计算器。在该函数内部,我们定义了一些私有变量和函数,例如 _result 变量和 add()subtract()multiply()divide()getResult()reset() 函数。这些变量和函数都保存在该函数的闭包中,外部代码无法直接访问它们。然后,我们将这些变量和函数通过一个对象返回,从而暴露出一个公共的接口供外部使用。通过使用这个接口,我们可以对计算器进行加、减、乘、除、获取结果和重置等操作。

实现异步编程

通过使用闭包,我们可以实现一些异步编程的功能,例如实现一个定时器函数。例如:

function delay(ms, callback) {
  setTimeout(function() {
    callback();
  }, ms);
}

console.log('start');
delay(1000, function() {
  console.log('callback');
});
console.log('end');

在这个例子中,我们定义了一个 delay() 函数,该函数接受两个参数:一个毫秒数 ms 和一个回调函数 callback。在函数内部,我们使用 setTimeout() 函数来等待指定的时间后调用回调函数。由于回调函数被保存在 delay() 函数的闭包中,因此它可以访问到 delay() 函数的参数和变量。在外部代码中,我们先输出 start,然后调用 delay() 函数,并在回调函数中输出 callback,最后输出 end

在这个例子中,我们使用闭包来实现一个简单的定时器函数,从而实现了异步编程。这种模式在 JavaScript 中非常常见,例如在事件处理、Ajax 请求和 Promise 等场景中都有广泛的应用。

在 Ajax 请求中,我们可以使用闭包来保存一些状态信息,从而在请求回调函数中访问到这些信息。例如:

function makeAjaxRequest(url) {
  let data = null;
  $.ajax({
    url: url,
    success: function(response) {
      data = response;
    },
    error: function() {
      console.log('Error');
    }
  });
  return function() {
    return data;
  };
}

const getData = makeAjaxRequest('https://example.com/data');
setTimeout(function() {
  console.log(getData()); // 输出请求回来的数据
}, 1000);

在这个例子中,我们定义了一个 makeAjaxRequest() 函数,该函数接受一个 URL 并发起一个 Ajax 请求。在请求回调函数中,我们将返回的数据保存在一个闭包内的变量 data 中。在函数结尾处,我们返回一个新的函数,该函数可以访问到闭包内的变量 data。在外部代码中,我们调用 makeAjaxRequest() 函数来发起请求,并使用返回的函数来获取请求回来的数据。由于请求是异步的,我们需要使用 setTimeout() 函数来等待一段时间后再获取数据。由于变量 data 被定义在闭包内部,因此它不会污染全局作用域。

在Promise中的应用

当使用 Promise 时,闭包的一个常见应用是在 then() 方法中访问变量。由于 then() 方法是一个回调函数,它将在 Promise 的状态发生改变时被调用。如果我们需要在 then() 方法中访问一些变量,我们可以使用闭包来保存这些变量的值,从而在回调函数中访问到这些值。例如:

function fetchData() {
  let data = null;
  const promise = newPromise(function(resolve, reject) {
    $.ajax({
      url: 'https://example.com/data',
      success: function(response) {
        data = response;
        resolve();
      },
      error: function() {
        reject();
      }
    });
  });
  
  // 返回一个新的 Promise 对象,该对象在成功时返回数据
  return new Promise(function(resolve, reject) {
    promise.then(function() {
      resolve(data);
    }).catch(function() {
      reject();
    });
  });
}

const promise = fetchData();
promise.then(function(data) {
  console.log(data); // 输出请求回来的数据
});

在这个例子中,我们定义了一个 fetchData() 函数,该函数返回一个 Promise 对象,并在 Promise 构造函数中发起一个 Ajax 请求。在请求回调函数中,我们将返回的数据保存在闭包内的变量 data 中,并使用 resolve() 函数将 Promise 标记为已解决。在函数结尾处,我们返回一个新的 Promise 对象,该对象在成功时返回闭包内的变量 data。在外部代码中,我们调用 fetchData() 函数来获取数据,并使用 then() 方法来访问数据。由于变量 data 被保存在闭包内部,因此它可以在 then() 方法中访问到。

通过使用闭包,在 Promise 中访问变量可以帮助我们避免全局变量污染,并提高代码的可维护性和可重用性。

实现迭代器

通过使用闭包,我们可以实现一个简单的迭代器。例如:

function createIterator(arr) {
  let i = 0;
  return function() {
    if (i < arr.length) {
      const value = arr[i];
      i++;
      return value;
    } else {
      return undefined;
    }
  };
}

const numbers = [1, 2, 3, 4, 5];
const iterator = createIterator(numbers);
console.log(iterator()); // 输出1
console.log(iterator()); // 输出2
console.log(iterator()); // 输出3
console.log(iterator()); // 输出4
console.log(iterator()); // 输出5
console.log(iterator()); // 输出undefine

在这个例子中,我们定义了一个 createIterator() 函数,该函数接受一个数组作为参数,并返回一个新的函数。新函数可以访问到 i 变量和 arr 数组,每次调用时返回数组中下一个元素的值。由于 iarr 变量被保存在闭包中,因此它们的值在函数调用之间得以保留。在外部代码中,我们通过调用 createIterator() 函数并传入一个数组来创建一个迭代器,然后通过多次调用迭代器函数来遍历数组中的元素。

通过使用闭包,我们可以实现一个简单的迭代器,从而将数据和遍历方法解耦,使得代码更加简洁和可读。

实现事件监听器

通过使用闭包,我们可以实现一个简单的事件监听器。例如:

function createEventEmitter() {
  let listeners = {};
  
  function on(eventName, callback) {
    if (!listeners[eventName]) {
      listeners[eventName] = [];
    }
    listeners[eventName].push(callback);
  }
  
  function emit(eventName, ...args) {
    const eventListeners = listeners[eventName] || [];
    eventListeners.forEach(callback => {
      callback(...args);
    });
  }
  return {
    on,
    emit
  };
}

const emitter = createEventEmitter();
emitter.on('click', function(x, y) {
  console.log(`clicked: ${x}, ${y}`);
});
emitter.emit('click', 10, 20); // 输出 "clicked: 10, 20"

在这个例子中,我们定义了一个 createEventEmitter() 函数,该函数返回一个包含 on()emit() 方法的对象。on() 方法用于向监听器中添加一个回调函数,emit() 方法用于触发一个事件并执行所有监听该事件的回调函数。在内部,我们使用一个 listeners 对象来保存所有事件监听器和对应的回调函数。由于 listeners 变量被保存在闭包中,因此它可以在 on()emit() 方法中共享。在外部代码中,我们通过调用 createEventEmitter() 函数来创建一个事件监听器,并使用 on() 方法添加一个回调函数。然后,我们调用 emit() 方法触发事件,并传入该事件的参数。

通过使用闭包,我们可以实现一个简单的事件监听器,从而实现了事件驱动的编程模式。这种模式在浏览器和 Node.js 等环境中都有广泛的应用。

 

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值