浅谈JS作用域、this及闭包

229 阅读4分钟

作用域可以理解为环境上下文,包含了变量、函数声明、参数等。在es6之前,JS使用的是全局作用域和函数作用,无块级作用域。

JS有自己特有的作用域链,函数中声明的变量在整个函数中都是有定义的。查找一个变量时,先在变量所在函数体内找,找不到向更外层函数找,一直到全局变量(注:全局变量都是window对象的属性)。代码写出时就定义好了作用域,比如谁嵌套在谁里面。

注意for、if、else 是不能创造作用域的。

// 只有一个popup函数级作用域,变量i、j、k在整个popup函数体内都是全局的
function popup() {
    var i = 0;
    if(true) {
        var j = 0;
        for(var k = 0; k < 3; k++) {
            console.log(k); // 分别输出 0 1 2
        }
        console.log(k); // 输出3
    }
    console.log(j); // 输出0
}

let的增加,引入了块级作用域,相比var的一些好处:

  • 避免了变量声明提升,防止变量的覆盖/泄露
// 虽然此处引用a在声明a之前,但并未报错,即变量提升
console.log(a); // undefined 
var a = 1;

// 上面代码可间接理解成如下逻辑
var b;
console.log(b);  // undefined
b = 1;

// 一函数体内任意位置声明的函数或变量,都会被提升到函数体内最顶层
// 形参不会被重新定义,且同名的优先级 函数>形参>变量
var c = 1;
function run(x, y, z, w) { // 形参会被添加到函数的作用域中
    console.log(c); // 内部有c的声明,所以输出'undefined',而不是1
    var c = 'runnerman';
    console.log(c); // 输出'runnerman'
    
    console.log(x); // 
    var x = 5; // x=5被执行
    function x() { // 被提升到了作用域顶部
        console.log('x coming');
    }
    console.log(x); // 5
    
    console.log(y); // parma2
    var y = 10; // var y被忽略,y=10被执行
    console.log(y); // 10
    
    console.log(z); // param3
    var z = function z() { // var z被忽略
        console.log('z coming');
    };
    console.log(z); // 
    
    console.log(w); // 
    function w() {
        console.log('w coming');
    };
    w = 20;
    console.log(w); // 20
}
run('param1', 'param2', 'param3', 'param4');
/* 输出:
undefined
runnerman
function x() {
    console.log('x coming');
}
5
param2 
10
param3
function z() {
    console.log('z coming');
}
function w() {
    console.log('w coming');
}
20
*/

// 但如此使用let,便会报错
console.log(d); // Uncaught ReferenceError: d is not defined
let d = 1;

var e = 90;
var e = 900; // 可以,会被覆盖

// let不可重复声明
let f = 90;
let f = 900; // Uncaught SyntaxError: Identifier 'f' has already been declared
  • TDZ暂时性死区:绑定块级作用域,不受外部影响,封闭作用域
for(var i=0;i<3;i++) {
    setTimeout(function() {
        console.log(i)
    }, 1000)
}

// 结果:3,3,3

for(let j=0;j<3;j++) {
    setTimeout(function() {
        console.log(j)
    }, 1000)
}

// 结果:0,1,2

关于this的指向

  • this总是指向函数的直接调用者(而非间接调用者)所在环境
  • 如果有new关键字,this指向new出来的那个对象
  • 在事件机制中,this指向触发这个事件的对象(除了IE的attachEvent中的this总是指向全局对象window)
function fight() {
    console.log(this) // Window
}
// 此处相当于Window调用了fight
fight()

var ironman = {
    name: "Tony Stark",
    fly: function() {
        console.log(this.name + ' is flying')  // this === ironman
    }
}
ironman.fly() // Tony Stark is flying

function Superhero(name, power) {
    this.name = name // this指向spiderman
    this.power = power
    //return this
}

// 首先new字段会创建一个空的对象,然后调用apply()函数,将this指向这个空对象
var spiderman = new Superhero('spiderman', 'jumping')
  • 更改this指向
var name = 'anyone', age = '30'
var ironman = { 
    name: "Tony Stark", 
    imAge: this.age, 
    run: function(skill) {
        console.log(this.name + " is " + this.age + ', ready to ' + skill)
    } 
}

// ironman为全局变量,此时的this指向为Window
console.log(ironman.imAge) // 30

// 此时函数中的this指向ironman
ironman.run('fly') // Tony Stark is undefined, ready to fly

// call,apply,bind第一个参数都是this指向的对象
// call和apply如果第一个参数指向null或undefined时,那么this会指向Window对象
// call,apply都是改变上下文中的this,并立即执行;bind方法可随后手动调用
var starlord = {name: "dude", age: 13}
ironman.run.call(starlord, "dance") // dude is 13, ready to dance
ironman.run.apply(starlord, ["dance"]) // dude is 13, ready to dance
ironman.run.bind(starlord, "dance")() // dude is 13, ready to dance
  • 箭头函数中的特殊情况
var globalObject = this;
var foo1 = (() => this); // 箭头函数:声明时已确定了指向
var foo2 = function() { return this }; // 运行时才能确定指向
console.log(foo1() === globalObject); // true
console.log(foo2() === globalObject); // true

var obj = {foo1: foo1, foo2: foo2};
console.log(obj.foo1() === globalObject); // true
console.log(obj.foo2() === obj); // true 指向调用其的对象

console.log(foo1.call(obj) === globalObject); // true
console.log(foo2.call(obj) === obj); // true

foo1 = foo1.bind(obj);
foo2 = foo2.bind(obj);
console.log(foo1() === globalObject); // true
console.log(foo2() === obj); // true

闭包(Closure)

  • 可以简单理解成读取所在函数内部其它变量的函数,定义在函数内部的函数,即内部函数;或者说内部函数和其词法作用域形成了一个闭包。
  • 连通起函数外部与函数内部的媒介
function Printer() {
  var count = 0;
  this.print = function() { // 引用了函数局部变量count
    count++; 
    console.log(count);
  };
}
var p = new Printer();
p.print(); // 1 相当于从外部引用了函数内部的局部变量
p.print(); // 2 此处也说明了count一直在内存中,并未在print调用后清除,原因正是因为count被函数外部所引用的关系

一般用于:

  • 读取函数内部的变量
  • 将变量保持在内存中

注意:滥用闭包会导致函数中的变量都被保存在内存中,内存消耗很大,导致网页性能问题,IE中可能导致内存泄露。所以在退出函数之前,最好将不使用的局部变量全部清除。