Node.js重点概念之作用域、闭包、this的使用详解和代码示例

718 阅读7分钟

图片

在 JavaScript 中,作用域和闭包是非常重要的概念。在 Node.js 中,这些概念同样也非常重要。本文将介绍 Node.js 中的作用域和闭包,并提供一些实战示例。

原文:Node.js重点概念之作用域、闭包、this的使用详解和代码示例

更多技术信息请关注公众号:CTO Plus,获取更多。

图片.png

作用域

在 JavaScript 中,作用域是指变量的可访问范围。在 Node.js 中,作用域的规则与浏览器中的 JavaScript 相同。变量可以在全局作用域、函数作用域和块级作用域中定义。

以下是一个示例,演示了在 Node.js 中定义变量的不同作用域:

// 全局作用域
var globalVar = 'globalVar';

function testScope() {
  // 函数作用域
  var funcVar = 'funcVar';

  if (true) {
    // 块级作用域
    let blockVar = 'blockVar';
  }
}

console.log(globalVar); // 'globalVar'
console.log(funcVar); // ReferenceError: funcVar is not defined
console.log(blockVar); // ReferenceError: blockVar is not defined

在上面的示例中,我们定义了一个全局变量 globalVar 和一个函数 testScope()。在函数中,我们定义了一个函数变量 funcVar 和一个块级变量 blockVar。在全局作用域中,我们可以访问全局变量 globalVar。在函数作用域中,我们可以访问函数变量 funcVar。在块级作用域中,我们可以访问块级变量 blockVar。但是,在函数作用域和块级作用域之外,我们无法访问这些变量。内部函数可以访问外部函数的变量,外部不能访问内部函数的变量。

下面的示例,演示了在 Node.js 中不使用var定义变量就变成了全局变量

var parent = function () {
  var name = "parent_name";
  var age = 13;

  var child = function () {
    var name = "child_name";
    var childAge = 0.3;
    childAge02 = 25;  // 如果此变量不使用var定义,那么变量就被声明为全局变量了

    console.log(name, age, childAge);  // child_name 13 0.3
  };

  child();
  console.log(childAge02);  // 25
  // console.log(name, age, childAge);  // ReferenceError: childAge is not defined
};

parent();

这个例子中内部函数 child 可以访问变量 age,而外部函数 parent 不可以访问 child 中的变量 childAge,因此会抛出没有定义变量的异常。

function foo() {
  value = "SteveRocket";
}
foo();
console.log(value); // 输出 SteveRocket
console.log(global.value, global.childAge02) // 输出SteveRocket 25

这个例子可以很正常的输出 SteveRocket,是因为 value 变量在定义时,没有使用 var 关键词,所以被定义成了全局变量。在 Node 中,全局变量会被定义在 global 对象下,在浏览器中,全局变量会被定义在 window 对象下。

如果你确实要定义一个全局变量的话,请显示地定义在 global 或者 window 对象上。

这类不小心定义全局变量的问题可以被 jshint 检测出来,如果你使用 sublime 编辑器的话,记得装一个 SublimeLinter 插件,这是插件支持多语言的语法错误检测,js 的检测是原生支持的。

关于jshint 工具的详细介绍和使用,请关注公众号:CTO Plus,查看文章《JavaScript 代码质量检查jshint的详细介绍》。

图片

块级作用域

在es6中新增了 let 关键字,与块级作用域,具体请关注公众号:CTO Plus,参考后续的文章《let和const命令》,在 ES5 及其之前的版本中,JavaScript 中没有块级作用域,只有函数作用域和全局作用域,在 ES6 及其之后的版本中,JavaScript 中有块级作用域。这意味着在一个块级语句(如 if、for、while 等)中定义的变量,在块级语句外部仍然可以访问。

例如:

if (true) {
     var x = 1;
}

console.log(x); // 1

在这个例子中,变量 x 在 if 语句块中定义,但是在 if 语句块外部仍然可以访问。

在 ES6 中,JavaScript 引入了块级作用域。使用 let 和 const 声明变量时,变量的作用域被限制在当前块级语句中。

例如:

if (true) {
     let x = 1;
}
console.log(x); // ReferenceError: x is not defined

在这个例子中,变量 x 使用 let 声明,它的作用域被限制在 if 语句块中,因此在 if 语句块外部访问 x 会抛出 ReferenceError 错误。

js 中,函数中声明的变量在整个函数中都有定义。比如如下代码段,变量 i 和 value 虽然是在 for 循环代码块中被定义,但在代码块外仍可以访问 i 和 value02。

function foo() {
  value = "SteveRocket";
  for (var i = 0; i < 10; i++) {
    var value02 = `hello ${value}`;
  }
  console.log(i); //输出  10
  console.log(value02);//输出  hello SteveRocket
}
foo();
console.log(value); // 输出 SteveRocket
console.log(global.value, global.childAge02) // 输出SteveRocket 25

所以有种说法是:应该提前声明函数中需要用到的变量,即,在函数体的顶部声明可能用到的变量,这样就可以避免出现一些低级的错误。

闭包

闭包这个概念,在函数式编程里很常见,简单的说,就是使内部函数可以访问定义在外部函数中的变量,即可以访问其外部作用域的能力。在 Node.js 中,闭包通常用于在函数中定义私有变量和方法。

以下是一个示例,演示了如何在 Node.js 中使用闭包:

function createCounter() {
  let count = 0;

  return {
    increment() {
      count++;
    },
    decrement() {
      count--;
    },
    getCount() {
      return count;
    }
  };
}

const counter = createCounter();

counter.increment();
counter.increment();
counter.decrement();

console.log(counter.getCount()); // 1

在上面的示例中,我们定义了一个 createCounter() 函数,该函数返回一个对象,该对象包含三个方法:increment()、decrement() 和 getCount()。在 createCounter() 函数中,我们定义了一个 count 变量,并将其初始化为 0。在返回的对象中,我们使用闭包访问了该变量。因此,在调用 increment() 和 decrement() 方法时,我们可以改变 count 变量的值。在调用 getCount() 方法时,我们可以获取 count 变量的值。

 

定义构造一个名为 adder 的构造器,如下:

var adder = function (args) {
  var base = args;
  return function (number) {
    console.log(args, number);
    return args + base;
  };
};

console.log(adder(10)(15.5));  // 25.5
console.log(adder(11.11)(15.5));  // 26.61

每次调用 adder 时,adder 都会返回一个函数给我们。我们传给 adder 的值,会保存在一个名为 base 的变量中。由于返回的函数在其中引用了 base 的值,于是 base 的引用计数被 +1。当返回函数不被垃圾回收时,则 base 也会一直存在。

闭包的一些问题

1. 循环中的闭包问题

在循环中使用闭包时,需要注意变量作用域的问题,否则可能会导致意外的结果。例如:

for (var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  }, 5);
}

输出结果

图片

上面这个代码块会打印五个 5 出来,而我们预想的结果是打印 0 1 2 3 4。

 

之所以会这样,是因为 setTimeout 中的 i 是对外层 i 的引用。当setTimeout 的代码被解释的时候,运行时只是记录了 i 的引用,而不是值。而当 setTimeout 被触发时,五个 setTimeout 中的 i 同时被取值,由于它们都指向了外层的同一个 i,而那个 i 的值在迭代完成时为 5,所以打印了五次 5

 

为了得到我们预想的结果,我们可以把 i 赋值成一个局部的变量,从而摆脱外层迭代的影响。

for (var i = 0; i < 5; i++) {
  (function (idx) {
    setTimeout(function () {
      console.log(idx);
    }, 5);
  })(i);
}

输出结果

图片

2. 模块中的闭包问题

 

在 Node.js 中,每个模块都是通过闭包实现的,因此模块中的变量和函数都是私有的。但是,如果在模块中定义了一个全局变量,它会被所有模块共享,这可能会导致一些意外的结果。例如:

// module1.js
var count = 0;
exports.increment = function() {
  count++;
};

// module2.js
var module1 = require('./module1');
module1.increment();
console.log(module1.count);

在这个例子中,我们在模块中定义了一个全局变量 count,并将其导出。在另一个模块中,我们通过 require 引入了第一个模块,并调用了 increment 函数来增加 count 的值。但是在输出 count 的值时,我们发现它的值仍然是 0,这是因为 count 是一个私有变量,不能被外部访问。为了解决这个问题,我们可以将 count 定义为一个对象的属性:

// module1.js
var obj = { count: 0 };
exports.increment = function() {
  obj.count++;
};
exports.getCount = function() {
  return obj.count;
};

// module2.js
var module1 = require('./module1');
module1.increment();
console.log(module1.getCount());

在这个例子中,我们将 count 定义为一个对象的属性,并将其导出。在另一个模块中,我们通过 getCount 函数来获取 count 的值。这样就可以避免模块间的变量共享问题。

this

在 JavaScript 中,this关键字用于引用当前对象。在 Node.js 中,this 关键字的行为与浏览器中的 JavaScript 相同。this 可以引用全局对象、函数对象和对象方法。在函数执行时,this 总是指向调用该函数的对象本身。要判断 this 的指向,其实就是判断 this 所在的函数属于谁。

this 出现的场景分为四类:

  • 有对象就指向调用对象。
  • 没调用对象就指向全局对象。
  • 用new构造就指向新对象。
  • 通过 apply 或 call 或 bind来改变 this 的所指。

1)函数有所属对象时:指向所属对象

函数有所属对象时,通常通过 . 表达式调用,这时 this 自然指向所属对象。比如下面的例子:

var myObject = {value: 100};
myObject.getValue = function () {
  console.log(this.value);  // 输出 100

  // 输出 { value: 100, getValue: [Function] },
  // 其实就是 myObject 对象本身
  console.log(this);

  return this.value;
};

console.log(myObject.getValue()); // => 100

getValue() 属于对象myObject,并由 myOjbect 进行 .调用,因此 this 指向对象 myObject

2) 函数没有所属对象:指向全局对象

var myObject = {value: 100};
myObject.getValue = function () {
  var foo = function () {
    console.log(this.value) // => undefined
    console.log(this);// 输出全局对象 global
  };

  foo();

  return this.value;
};

console.log(myObject.getValue()); // => 100

在上述代码块中,foo 函数虽然定义在 getValue 的函数体内,但实际上它既不属于 getValue 也不属于 myObjectfoo 并没有被绑定在任何对象上,所以当调用时,它的 this 指针指向了全局对象 global

3)构造器中的 this:指向新对象

js 中,我们通过 new 关键词来调用构造函数,此时 this 会绑定在该新对象上。

var SomeClass = function(){
  this.value = 100;
}

var myCreate = new SomeClass();

console.log(myCreate.value); // 输出100

4) apply 和 call 调用以及 bind 绑定:指向绑定的对象

apply() 方法接受两个参数第一个是函数运行的作用域,另外一个是一个参数数组(arguments)。

call() 方法第一个参数的意义与apply() 方法相同,只是其他的参数需要一个个列举出来。

简单来说,call 的方式更接近我们平时调用函数,而 apply 需要我们传递 Array 形式的数组给它。它们是可以互相转换的。

var myObject = {value: 100};

var foo = function(){
  console.log(this);
};

foo(); // 全局变量 global
foo.apply(myObject); // { value: 100 }
foo.call(myObject); // { value: 100 }

var newFoo = foo.bind(myObject);
newFoo(); // { value: 100 }

this示例

// 全局对象
console.log(this === global); // true

function testSteveRocket() {
  // 函数对象
  console.log(this === global); // false
  console.log(this === testSteveRocket); // true

  this.name = 'testSteveRocket';
}

testSteveRocket();

console.log(name); // 'testSteveRocket'

const obj = {
  // 对象方法
  testThis() {
    console.log(this === obj); // true
    console.log(this === global); // false

    this.name = 'obj.testThis';
  }
};

obj.testThis();

console.log(obj.name); // 'obj.testThis'

在上面的示例中,我们首先比较了全局对象和 this 的值。由于在 Node.js 中,全局对象是global,因此 this === global 的值为 true。然后,我们定义了一个 testThis() 函数,在函数中比较了 this 和全局对象的值。由于该函数是在全局作用域中定义的,因此 this === global 的值为 false,而 this === testSteveRocket的值为 true。在调用 testThis() 函数时,我们使用 this 关键字设置了全局变量 name 的值。最后,我们定义了一个对象 obj,并在该对象中定义了一个 testThis() 方法。在该方法中,我们比较了 this 和对象 obj 的值。由于该方法是在对象 obj 中定义的,因此 this === obj 的值为 true,而 this === global 的值为 false。在调用 obj.testThis() 方法时,我们使用 this 关键字设置了对象属性 name 的值。

总结

作用域和闭包是 JavaScript 中非常重要的概念,在 Node.js 中同样如此。在 Node.js 中,作用域的规则与浏览器中的 JavaScript 相同。变量可以在全局作用域、函数作用域和块级作用域中定义。闭包通常用于在函数中定义私有变量和方法。在 Node.js 中,this 关键字的行为与浏览器中的 JavaScript 相同。this 可以引用全局对象、函数对象和对象方法。

更多精彩,关注我公号 ,一起学习、成长

图片.png

推荐阅读: