浅谈JS做用域、this及闭包

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

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

注意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被函数外部所引用的关系
复制代码

通常用于:app

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

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

相关文章
相关标签/搜索