在 JavaScript 中,作用域和闭包是非常重要的概念。在 Node.js 中,这些概念同样也非常重要。本文将介绍 Node.js 中的作用域和闭包,并提供一些实战示例。
原文:Node.js重点概念之作用域、闭包、this的使用详解和代码示例
更多技术信息请关注公众号:CTO Plus,获取更多。
作用域
在 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 也不属于 myObject。foo 并没有被绑定在任何对象上,所以当调用时,它的 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 可以引用全局对象、函数对象和对象方法。
更多精彩,关注我公号 ,一起学习、成长
推荐阅读: