安全基础 --- js的闭包和this属性

js闭包

简介

一个函数和对其周围状态(lexical exviroment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)

在js中,通俗来讲,闭包就是能够读取外层函数内部变量的函数

(1)变量作用域:

两种

  • 全局作用域
  • 局部作用域

[1] 函数内部可以读取全局变量

javascript 复制代码
var code = 200;

function f1() {
    console.log(code);
}
f1(); //200

[2] 函数外部无法读取函数内部的局部变量

javascript 复制代码
function f1() {
    var code = 200;
}
console.log(code);  // 无法读取,在外部未被定义

(2)读取函数内部的局部变量

[1] 在函数内部再定义一个函数

javascript 复制代码
function f1(){
    var code = 200;

    function f2() {
        console.log(code);
    }
}

函数 f1 内部函数 f2 可读取 f1 中所有的局部变量。因此,若想在外部访问函数 f1 中的局部变量 code,可通过函数 f2 间接访问

[2] 为外部程序提供访问函数局部变量的入口

javascript 复制代码
function f1() {
    var code = 200;
    function f2() {
        console.log(code);
    }
    return f2;
}
f1()();  // 200

(3)闭包概念

函数 f2 就是闭包,作用就是将函数内部与外部进行了连接

  • 闭包访问的变量,是每次运行上层函数时重新创建的,是相互独立的
javascript 复制代码
function f1() {
    var obj = {};

    function f2() {
        return obj;
    }
    return f2;
}

let result1 = f1();
let result2 = f1();
console.log(result1() === result2());  // false
  • 不同的闭包,可共享上层函数中的局部变量
javascript 复制代码
function f() {
    var num = 0;
    function f1() {
        console.log(++num);
    }
    function f2() {
        console.log(++num);
    }
    return {f1,f2};
}

var result1 = f();
result1.f1(); // 1
result1.f2(); // 2

// 闭包f1和闭包f2共享上层函数中的局部变量num

例:旅行者走路的问题

javascript 复制代码
function factory() {
    var start = 0;
    function walk(step) {
        var new_total = start + step;
        start = new_total;
        return start;
    }
    return walk;
}

var res = factory();
console.info(res(1));
console.info(res(2));
console.info(res(3));

// start 将记录上一次的值

PS:

  1. 闭包使得函数中变量保存在内存中,内存消耗大,不可滥用,否则会造成网页的性能问题,IE中可能导致内存泄露。解决办法:退出函数前,不使用的局部变量全部删除
  2. 闭包会在父函数外部,改变父函数内部变量的值。若把父函数当做对象(object)使用,把闭包当做它的公用方法(Public Method),把内部变量当做它的私有属性(private value),这时不可随意改变父函数内部变量的值

this关键字

(1)关键点:

  1. this始终指向调用该函数的对象;
  2. 没有指明调用的对象,则顺着作用域链向上查找,最顶层为global(window)对象;
  3. 箭头函数中的this是定义函数时绑定的,与执行上下文有关;
  4. 简单对象(非函数,非类)没有执行上下文;
  5. 类中的this,始终指向该实例对象;
  6. 箭头函数体内的this对象,就是定义该函数时所在的作用域指向的对象,而不是使用时所在的作用域指向的对象

(2)四类调用方式

[1] 作为对象方法使用

javascript 复制代码
function f(){
    console.log(this.code);
}
var obj = {
    code:200,
    f:f
};
obj.f();  // 200

[2] 纯粹的函数调用

javascript 复制代码
function f() {
    console.log(this.code);
}
// 此处,通过var(函数作用域)声明的变量会绑定在windows上,
// 如果使用let(块作用域)声明变量code,则不会绑定在windows上,
// 因此下面的两次函数调用f(),显示为undefined
// let code = 200;

var code = 200;
f();// 200
code = 404;
f();// 404

复杂:

javascript 复制代码
function doF(fn) {
    this.code = 404;
    fn();
}

function f() {
    console.log(this.code);
}

let obj = {
    code:200,
    f:f
};
var code = 500;
doF(obj.f);  // 404

结果解析 --- 该例中,为分析出 this 的指向,应找到关键点:哪个对象调用了函数 f()。obj.f 作为doF()的入参,将函数 f 传给了doF,而doF 是由 window 调用的,所以函数doF中的 this 指向 window ,继而函数 f 中的 this 指向window

最终执行是 doF,所以 this 指向 doF,结果为404

[3] 作为[构造函数]调用

javascript 复制代码
code = 404;
function A() {
    this.code = 200;
    this.callA = function() {
        console.log(this.code);
    }
}
var a = new A();
a.callA();  // 200,callA在new A返回的对象里

[4] 使用apply、call、bind调用

<1> apply

作用:与call方法类似,也是改变this指向,然后再调用该函数。唯一的区别为,它接受一个数组作为函数执行时的参数

javascript 复制代码
var code = 404;
let obj = {
    code:200,
    f:function() {
        console.log(this.code);
    }
};
obj.f();  // 200,作为对象的方法调用
obj.f.apply(); // 404,参数为空时,默认使用全局对象global,在此处为对象window
obj.f.apply(obj); // 200,this指向参数中设置的对象
<2> call

函数实例的call方法,作用:可以指定函数内部的this指向(即函数执行时所在的域),然后在所指定的作用域中,调用该函数

javascript 复制代码
function f() {
    console.log(this.code);
}

var obj = {
    code:200
};

f.call(obj);  // 200
<3> bind

bind()方法**作用:**用于将函数体内的this绑定到某个对象,然后返回一个新函数

javascript 复制代码
// bind返回一个新的函数
function f(b) {
    console.log(this.a,b);
    return this.a+b;
}
var obj = {
    a:2
};
var newF = f.bind(obj);
var result = newF(3); // 2 3
console.log(result); // 5

(3)箭头函数中的this

[1] 概念

箭头函数中的this是定义函数时绑定的,而不是在执行函数时绑定。若箭头函数在简单对象中,由于简单对象没有执行上下文,所以this指向上层的执行上下文;若箭头函数在函数、类等有执行上下文的环境中,则this指向当前函数、类

[2] 箭头函数在普通对象中

javascript 复制代码
var code = 404;
let obj = {
    code:200,
    getCode:() => {
        console.log(this.code);
    }
};
obj.getCode(); // 404

结果解析:

在箭头函数中,this 的值是在定义函数时确定的,而不是在运行时确定的。函数 getCode 是在对象 obj 定义时创建的,而不是在调用obj.getCode()的时候

箭头函数中 this 指向的是外层语法作用域的 this 值,而不是指向调用他的对象。在全局作用域中,this 指向的是全局对象(在浏览器环境中通常是window对象)。所以的那个箭头函数中使用 this.code 时,实际上引用全局作用域的code变量,值为404

[3] 箭头函数在函数中

javascript 复制代码
var code = 404;
function F() {
    this.code = 200;
    let getCode = () => {
        console.log(this.code);
    };
    getCode();
}
var f = new F(); // 200
var f = F();  // 构造函数没有new调用,成为了一个普通函数
console.log(f);
console.log(code);  // 200

[4] 箭头函数在类中

javascript 复制代码
var code = 404;
class Status {
    constructor(code){
        this.code = 200;
    }

    getCode = () => {
        console.log(this.code);
    };
}
let status = new Status(200);
status.getCode(); // 200

**PS:**不管是箭头函数还是普通函数,只要在类中,this就指向实例对象

实例解析

(1)例1:

javascript 复制代码
var code = 404;

let status = {
    code:200,
    getCode:function(){
        return function(){
            return this.code;
        };
    }
};

console.log(status.getCode()()); // 404

执行status.getCode()时,返回函数,status.getCode()()表示当前返回的函数,其调用者为全局变量window,所以this.code为绑定在window中的code,值为404

(2)例2:

javascript 复制代码
var code = 404;
let status = {
    code:200,
    getCode:function(){
        let that = this;
        return function(){
            return  that.code;
        };
    }
};

console.log(status.getCode()()); // 200

执行status.getCode()时,this指向status,并通过局部变量that保存this的值,最后返回值为函数。**status.getCode()()表示执行返回的函数,其that指向的status,**所以返回值为200

(3)例3:复杂

javascript 复制代码
function f() {
    // 宏任务
    setTimeout(() => {
        console.log(">>>" + this); // >>>[object object],语句5
        this.code = 401;
    },0)
    // 同步
    console.log(this.code);
}

let obj = {
    // ">>>" + this
    code:200,
    foo:f
};

var code = 500;

// 1.箭头函数 this 指向问题
// 2.字符串 + this [object object]
obj.foo(); // 200  语句1

console.log("---" + obj.code);  // 200  语句3

// 宏任务
setTimeout(() =>{console.log("---" + obj.code);},0);  //  401  语句4

obj.foo(); --- (语句1):调用obj对象的foo方法

输出:200

解释:在 foo 方法内部的console.log(this.code) 打印出 obj 对象的 code 属性,其值为200

console.log("---" + obj.code); --- (语句3):打印obj对象的code属性

输出:---200

解释:全局作用域中,code被赋值为500,这里的obj.code指向的是obj对象的code属性,其值仍然是200

setTimeout(() => console.log("---" + obj.code);},0); --- (语句4):设置一个0延时的定时器,其中的箭头函数在调用

输出:---401

解释:在调用obj.foo()的过程中,foo方法中的setTimeout在当前宏任务结束后执行。由于是箭头函数,this的值保持与父作用域一致(也就是obj对象),所以在箭头函数内部,this.code的值被设为401

setTimeout(() => {console.log(">>>" + this); this.code = 401;},0) --- (语句5):设置一个0延时的定时器,其中的箭头函数在调用

输出:>>>[object object]

解释:在调用obj.foo()的过程中,foo方法中的setTimeout在当前宏任务结束后执行。箭头函数的this始终指向被他创建时的外部作用域,所以this指向了obj对象,在控制台中打印this时会将其转换为字符串。所以输出为 >>>[object object]

setTimeout()

函数setTimeout用于创建一个定时器,同一个对象,各个定时器用一个编号池,不同的对象是用独立的编号池,同一个对象上的多个定时器有不同的定时器编号;所以,setTimeout到了执行时间点时,其内部的this指向定时器所绑定的对象。

分析 --- :函数setTimeout中传入的函数句柄,由于js是单线程执行,即使延时为0,仍需等到本次执行的所有同步代码执行完毕,才能执行。在两次执行obj.foo()的过程中,其内部的setTimeout的入参函数(语句5)都未执行。知道执行语句4,当前同步代码执行完毕,语句5执行(执行2次,因为语句1和2分别执行1次),obj上绑定的code被执行为401。最终语句4的入参函数执行,输出obj.code的值为401。

(4)扩展

javascript 复制代码
function doFoo(fn){
    this.code = 404;
    fn();
}

function f() {
    setTimeout(() => {
        console.log(">>>" + this);  //  >>>[object window],语句3
        this.code = 401;  // 语句4
    },0)
    console.log(this.code);
}

let obj = {
    code:200,
    foo:f
};

var code = 500;

doFoo(obj.foo);  //  语句1
setTimeout(() => {console.log(obj.code)},0);  // 200,语句5
setTimeout(() => {console.log(window.code)},0);  // 401,语句6

结果:obj.foo为函数句柄,作为入参传入函数doFoo,doFoo的调用方为全局变量window,所以,语句2中doFoo对象的code是404,3、4中的this均指向window

相关推荐
腾讯TNTWeb前端团队5 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰9 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪9 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪9 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy10 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom10 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom10 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom10 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom11 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom11 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试