作用域、作用域链
作用域
编程语言中存储、访问、修改变量当中的值是一项基本能力、存储变量、访问变量必须按照一定的规则,这套规则就是作用域。JavaScript中的作用域可分为三种:全局作用域、函数作用域、块作用域
- 全局作用域
在任何函数之外的顶层作用域,
全局可使用
,如windows对象,document对象, 全局变量在全局作用域、函数作用域和块作用域里都可以获取到。
// 全局作用域
var temp = 'A';
// 函数作用域
function showTemp() {
console.log(temp);
}
console.log(temp) // A
showTemp(); // A
// 块作用域
{
temp = 'B'
}
console.log(temp) // B
- 函数作用域
函数中定义的作用域,只能在
当前函数中使用
。
// 函数作用域
function showTemp() {
var temp = 'A'
console.log(temp);
}
showTemp(); // A
console.log(temp) // 报错: temp not defined
- 块作用域
- ES6 新增的两个用于声明变量的新关键词
let
和const
。这两个关键字定义的变量如果处于大括号 { } 中,大括号中的变量就形成了一个块作用域。- 在
if/while/for
等的大括号{ }里也形成了一个块作用域。
{
let temp = 'A'
}
{
console.log(temp) // 报错: temp not defined
}
console.log(temp) // 报错: temp not defined
作用域链
实际工程中,通常会使用多种作用域。当在当前作用域中无法找到目标变量时,就会向上级作用域寻找,这一层层向上的过程称为
作用域链
。
const A = 1;
function printSum(A) {
const B = 2
console.log(A + B)
}
printSum(A) // 3
上面是一个简单的示例,printSum函数中找到了需要的变量 B ,但是找不到变量 A ,于是沿着 作用域链 找到了 全局作用域 的目标变量A。
循环中的作用域
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
上面是一道经典的面试题目,最终会输出五个5,根据作用域的理论,在function中找不到变量i,当向上层寻找时,for循环里的i早已经执行到 i=5, 函数在延迟执行后取到的i只会是5。要实现预计的0-4打印效果,可以在 setTimeout 外面再套一层函数,或者在循环中使用let:
var print = function (i) {
setTimeout(function () {
console.log(i);
}, 1000);
};
for (var i = 0; i < 5; i++) {
print(i); // 会去print函数的作用域去寻找变量 i
}
// 0 1 2 3 4
for (let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
// 0 1 2 3 4
自由变量、闭包
自由变量
- 定义:某变量 a 在作用域 A 中被使用,却没有在该作用域中被定义,需要沿
作用域链
寻找,则对于作用域A来说,a是一个自由变量
。- 自由变量确定:沿
作用域链
向上级作用域一层层寻找,直到找到;若在全局作用域都没找到,会报错:xxx is not defined
。
闭包的定义、表现、应用
如果一个函数引用了
自由变量
,即该函数使用了某变量,但它既 不是函数参数
、也不是函数内部定义的变量
,则该函数就叫闭包
。
闭包通常有两种表现:函数作为返回值、 函数作为参数被传递。
- 函数作为返回值
function closure() {
const a = 100
// 返回值函数
return function () {
console.log(a) // 100
}
}
// 把返回值函数赋给fn
const fn1 = closure()
fn1()
- 函数作为参数被传递
function print(fn2){
fn2() //函数参数执行
}
const b = 100
function fn2(){ //函数参数定义
console.log(b)
}
print(fn2) //100
通过上述描述其实可以看到:闭包是作用域应用的一种特殊表现形式。那么,闭包到底有什么作用呢?一句话:闭包可以使变量仅在对象内部生效,无法从外部触及,只提供API,从而保护数据
。
举例一: 闭包隐藏数据,不能直接修改数据
function cache() {
const data = {} // data是在函数cache作用域中被定义的,全局中未定义
return {
set: function (key, val) {
data[key] = val
},
get: function (key) {
return data[key]
}
}
}
const data = cache()
data.set('name', 'jackeroo')
const data_name = data.get('name')
console.log(data_name) // jackeroo
console.log(data.name) // undefiend 无法直接获取name属性
举例二: 创建一个User对象,能够调取它的login方法获取用户名和密码,也能够直接访问该用户的用户名,但是不能取到该用户的密码
const User = function () {
let _password;
return class User {
constructor(name, password) {
this.userName = name;
_password = password;
}
login() {
console.log(`使用账号:${this.userName}, 密码:${_password}进行登录`)
}
}
}()
const theUser = new User('jackeroo', 123)
theUser.login() // 使用账号:jackeroo, 密码:123进行登录
console.log(theUser.userName) // jackeroo
console.log(theUser.password, theUser._password) // undefined undefined
如何确定在闭包中获取正确的变量
⭐在函数定义的地方向上级作用域查找,注意是函数定义的地方而不在函数执行处
// 示例1
function create(){
const a = 100
return function (){ // 函数的定义处
console.log(a)
}
}
const a = 200
const fn1 = create() // 函数执行处
fn1()//100
// 示例2
function print(fn2){
const b = 200
fn2() //函数执行处
}
const b = 100
function fn2(){ //函数定义处
console.log(b)
}
print(fn2) // 100
// 示例3
const c = 1;
function test(){
a = 2;
return function(){ // 函数定义处
console.log(a);
}
var a = 3; // 变量提升 => var a = 2
}
test()(); // 2
通过以上三个例子再次强调:闭包/所有自由变量的查找是在函数定义的地方向上级作用域查找,而不是在函数执行的地方!!!
总结
作用域、作用域链
- 作用域
- 作用域链
自由变量、闭包
- 自由变量
- 闭包的定义、表现、应用
如何确定在闭包中获取正确的变量
闭包中的自由变量的查找是在函数定义的地方向上级作用域查找