this/闭包/作用域

229 阅读9分钟

前言

一个系统/程序/页面的执行状态分为静态创建阶段动态执行阶段静态创建阶段会进行代码分析或者说是结构分析,比如说作用域链的确定(当前变量和所有的父级变量)、变量声明(参数、变量、函数)。 动态执行阶段执行的第一个就是this,this指向当前环境的上下文,第二个就是变量的使用,建立一个引用关系。第三个是函数的引用,函数在动态执行过程中,在动态执行阶段后才能区别或者拿到返回值,还可以去判断当前函数的作用域是局部作用域还是全局作用域。

课程目标

  • 作用域、作用域链
  • 执行上下文
  • 立即执行、块级作用域

知识要点

作用域 作用域链

作用域

作用域和执行上下文是息息相关的,作用域就是执行上下文,值和表达式在其中可见或可被访问到的上下文。
简单来说,作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。

function fn(){
    var scope = "作用域";
    console.log(scope)
}
fn() // 作用域
console.log(scope) // error

作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。 作用域链是一种向上查找的关系,白话概述就是儿子可以用爸爸的东西。

let a = "grandpa";
console.log(a);
function father(){
    let b = "father";
    console.log(b);
    son();
    function son(){
        let c = "son";
        console.log(c);
        
        console.log(a) // 作用域向上查找
    }
}
father()

为什么函数的执行可以在函数声明之前而不会报错?

函数提升

fn();
function fn(){ // 会提升到当前作用域的最顶端
    console.log(1)
}

函数有函数提升,那么变量也会有变量提升吗?

变量提升

console.log(a);
var a = "a";
// ===
var a;
console.log(a);
a = "a";

在变量提升中有两个例外就是ES6新增的let/const关键字。

暂时性死区(TDZ)

console.log(a); // error
// let a = "a";
const a = "a";

在使用let和const声明之前去使用(读写操作)就会出现暂时性死区。

ES6规定,let/const 命令会使区块形成封闭的作用域。若在声明之前使用变量,就会报错。总之,在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。这在语法上,称为  暂时性死区 ( temporal dead zone,简称 TDZ)。

变量提升和函数提升的优先级

var fn = "fn"
function fn(){}
console.log(fn) // "fn"

在JavaScript中函数是第一公民,函数提升的优先级大于变量提升

块级作用域

整个作用域链归根结底就是向上查找和优先级的问题,随之而来的就是块级作用域, 块级作用域存在的意义就在于限制变量只能在这个块之内。

if(true){
  let a = 1;
  var b = 2;
console.log(a,b) // 1 2
}
console.log(a) // error
console.log(b) // 2

总结

在我们目前的代码编写过程中:

  • 对于作用域链,我们直接通过创建态来定位作用域链
  • 手动取消全局,使用块级作用域

this 上下文 context

this是在执行时动态读取上下文决定的,而不是创建时,重点在于当前this的指向。

函数直接调用

function foo(){
    console.log("函数内部的this"this)
}
foo() // window
foo() === window.foo() // true

在当前环境下,foo是在全局环境下指向,并且没有表明被任何人调用,这里隐式绑定的就是window。在基础岗面试中函数的直接调用会被演化为函数表达式匿名函数嵌套函数

隐式绑定

function fn(){
    console.log("隐式绑定",this.a);
}
const obj = {
    a:1,
    fn // fn:fn 在ES6中,如果函数的属性和值相同,可以缩写成这样
}
obj.fn();

在上述代码中,在fn函数执行的过程中,它已经不在是全局下的孤立函数,它的this/上下文做了赋值和绑定,在obj中引用了fn函数,它指向的就是obj对象。

隐式绑定的时候,this指向的是调用的上一级,这种情况通常会存在于对象,数组,等引用关系逻辑之上。 举个例子:

const foo = {
    bar:100,
    fn:funciton(){
        console.log(this.bar);
        console.log(this)
    }
}
//取出
const fn1 = foo.fn;
fn1();

在上诉代码中,foo.fn被拿出来了,同时被fn1引用,作为一个全局下的独立函数,这时this的指向是window。

如何改变this指向

const o1 = {
    text:"o1",
    fn:function (){
        // 直接使用上下文,领导直接分活
        return this.text
    }
}
const o2 = {
    text:"o2",
    fn:function (){
        // 呼叫了隔壁组o1的leader协助执行 - 部门协助
        return o1.fn()
    }
}
const o3 = {
    text:"o3",
    fn:function (){
        // 临时构造,没有直接的上下文,直接指向全局环境
        const fn = o1.fn;
        return fn()
    }
}
console.log(o1.fn(),"o1fn"); // o1 o1fn
console.log(o2.fn(),"o2fn"); // o1 o2fn
console.log(o3.fn(),"o3fn"); // undefined o3Fn

在执行函数时,函数被上一级调用,去找发起方。
直接变成公共执行时,指向的是window全局。

问题:如何使o2.fn()输出o2? 不人为干涉:引用赋值。
人为干涉:使用 call/bind/apply 改变this指向。

const o1 = {
    text:"o1",
    fn:function (){
        return this.text
    }
}
const o2 = {
    text:"o2",
    fn:o1.fn
}
console.log(o1.fn(),"o1fn"); // o1 o1fn
console.log(o2.fn(),"o2fn"); // o2 o2fn

call/apply/bind

区别:call/apply 传参不同 依次传入/数组传入,bind和call/apply 返回函数体,需要执行。

call

const obj = {
    a:1,
    b:2
}
function fn(){
    console.log(this.a,this.b,...arguments) // arguments是一个类似于数组的对象,对应于传递给函数的参数
}
fn.call(obj,3,4,5) // 1,2,3,4,5

在上诉代码中,我们可以看到使用call来改变this指向,指向obj的同时传递三个参数,在函数中我们可以使用arguments对象来接收,argument对象是隐式的,我们不能手动的创建。
对于原理或手写类题目,我们首先要清晰的是解题思路,第一步就是call有什么用?需要放在什么地方或者说挂载在哪里?第二部就是把你的思路转换成代码实现。

 Function.prototype._call = function(context,...args){
     context = (context === null || context === undefined) ? window : new Object(context);
     context.fn = this;
     const result =  context.fn(...args);
     delete context.fn;
     return result
 }

分析代码,首先我们创建了一个context上下文...args对剩余参数进行解构,接着我们对context进行边缘检测(如果传进来的对象不存在,则this指向全局window),然后我们给context添加fn属性并引用当前调用call的函数。例如 fun.call,我们当前的this就指向fun。为了使我们的call可以被多次调用,我们创建一个变量进行接收,方便后续返回,避免占用内存,我们选择当函数执行完之后去清除这个属性。
apply

 const obj = {
    a:1,
    b:2
}
function fn(){
    console.log(this.a,this.b,...arguments)
}
fn.apply(obj,[3,4,5]) // 1,2,3,4,5

与call方法类似,call方法接收的是一个参数列表,而apply方法接收的是一个包含多个参数的数组。接收除了object、array、function、null之外的参数会出现报错。

    Function.prototype._apply = function (context, args) {
        if (typeof args === "array" || typeof args === "object" || typeof args ==="function"|| typeof args ==="null") {
            context = context === undefined || context == null ? window : new Object(context);
            context.fn = this;
            const result = args ? context.fn(...args) : context.fn()
            delete context.fn;
            return result
        } else {
            throw "TypeError: CreateListFromArrayLike called on non-object"
        }
    }

bind

 const obj = {
    a:1,
    b:2
}
function fn(){
    console.log(this.a,this.b,...arguments)
}
let fun = fn.bind(obj,3,4,5)
fun(6,7) // 1,2,3,4,5,6,7

在 bind 被调用时,这个新函数的 this 被指定为 bind 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

Function.prototype._bind = function(context,...args1){
    const _this = this;
    return function(...args2){
        return _this.call(context,...args1,...args2)
    }
}

new this指向

class Course {
    constructor(name) {
        this.name = name;
        console.log('构造函数中的this:', this);
     }

     test() {
        console.log('类方法中的this', this);
     }
}

const course = new Course('this');
course.test();

在构造函数中,this指向的是创建的实例对象

构造函数中异步方法的this指向

class Course {
    constructor(name) {
        this.name = name;
        console.log('构造函数中的this:', this);
    }

    test() {
        console.log('类方法中的this', this);
    }

    asyncTest() {
        console.log('异步方法外:', this);
        setTimeout(function() {
            console.log('异步方法内', this);
        }, 500)
        ajax.get().then(res => {
            // 异步方法
        })
    }
}
    const course = new Course('this');
    course.test();
    course.asyncTest();

异步中的this指向的是去全局window,为什么? 异步函数创造的是一个独立的作用域,异步函数之内就相当于在执行栈之外,因为我们的JavaScript是单线程,所有的异步都是在消息队列里,当我们的当前执行栈执行完之后,才会去调用消息队列里的东西,在执行消息队列的时候,我们的上下文已经消失了。 执行setTimeout,匿名函数执行上下文,在队列中和全局执行函数效果相同 - 指向window。

如何解决异步方法指向问题

  • 记录this
  • 显式
  • 箭头函数

闭包

概念:一个函数和它周围状态的引用捆绑在一起的组合。

// 函数作为返回值的场景
function mail() {
    let content = '信';
    return function() {
        console.log(content);
    }
}
const envelop = mail();
envelop();
  • 函数可以作为返回值传递的(在上述bind已经说明了)
  • 函数外部可以通过一定方式获取到内部局部作用域的变量 => 导致内部局部变量不能被GC(垃圾回收)
// 函数作为参数传递

// 单一职责
let content;

// 通用存储
function envelop(fn) {
    content = 1;
    fn()
}
// 业务逻辑
function mail() {
    console.log(content);
}
envelop(mail);

函数嵌套

let counter = 0;

function outerFn() {
    function innerFn() {
        counter++;
        console.log(counter);
    }
    return innerFn;
}

outerFn()();

可以通过外层函数去调用内层的函数。什么意思? 当我们想把核心的代码通过函数拆解成若干个微型的方法,通过一个大的逻辑外壳去包裹,而外部只用关心逻辑外壳给我们提供的方法去调用就可以了,并不知道也并不关心里面有多少个方法,这就是高内聚

// 事件处理(异步)的闭包

let lis = document.getElementsByTagName('li');
for(var i = 0; i<lis.length; i++) {
    (function(i) {
        lis[i].onclick = function() {
            console.log(i);
        }
    })(i);
}

立即执行函数

作用:可以让内部的方法/变量生成一个独立的作用域,同时也可以让外界无感知。

(function immediateA(a) {
    return (function immediateB(b) {
        console.log(a);
    })(1);
})(0);

立即执行函数 / 立即执行嵌套 => 拥有独立作用域,立即执行函数就是JavaScript早期模块化的基础。

私有变量

function createStack() {
    return {
        items: [], // 外部可以更改
        push(item) {
            this.items.push(item);
        }
    }
}

const stack = {
    items: [], // 内部维护
    push: function() {}
}

function createStack() {
    const items = [];

    return {
        push(item) {
            items.push(item);
        }
        setItems() {},
        getItems() {}
        // 通过内部提供的set和get让用户读写,可以加于限制
    }
}