this指针、闭包、作用域

83 阅读7分钟

执行上下文

个人觉得要了解指针、闭包、作用域,需要先了解执行上下文,简单总结:当JavaScript代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

变量对象

变量对象是与执行上下文相关的数据作用域,存储了在上下文定义的变量个函数申明

  • 当函数执行时,VO会转变为AO(Activity Object),来表示变量对象
  • VO\AO本质是一个东西,VO不能通过js直接被访问,函数激活时,VO被激活成AO,的各种属性和对象才能够被访问
  • AO通过函数的arguments属性初始化

作用域链

当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

函数的作用域在函数定义的时候就决定了。函数有一个内部属性 [[scope]],当函数激活时,进入上下文,创建VO/AO后,就会将活动对象添加到作用域链的最前端:


ScopeChain = [AO].concat([[Scope]]);

作用域链延长

大部分情况都是这样的,作用域链有多长主要看它当前嵌套的层数,但是有些语句可以在作用域链的前端临时增加一个变量对象,这个变量对象在代码执行完后移除,这就是作用域延长了。能够导致作用域延长的语句有两种:try...catch的catch块和with语句。

try...catch

let x = 1;
try {
  x = x + y;
} catch(e) {
  console.log(e);
}

上述代码try里面我们用到了一个没有申明的变量y,所以会报错,然后走到catchcatch会往作用域链最前面添加一个变量e,这是当前的错误对象,我们可以通过这个变量来访问到错误对象,这其实就相当于作用域链延长了。这个变量e会在catch块执行完后被销毁。

with

function f(obj, x) {
  with(obj) {
    console.log(x);  // 1
  }
  
  console.log(x);   // 2
}

f({x: 1}, 2);

with语句可以操作作用域链,可以手动将某个对象添加到作用域链最前面,查找变量时,优先去这个对象查找,with块执行完后,作用域链会恢复到正常状态。 上述代码,with里面输出的x优先去obj找,相当于手动在作用域链最前面添加了obj这个对象,所以输出的x是1。with外面还是正常的作用域链,所以输出的x仍然是2。需要注意的是with语句里面的作用域链要执行时才能确定,引擎没办法优化,所以严格模式下是禁止使用with的。

this

this是在执行时动态读取上下文决定的,而不是创建时(谁调用,指向谁,箭头函数除外)。

作用域链是创建时就确定的,this是在执行时确认的

示例:

checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: 'local scope'
    },
    ScopeChain: [AO, [[Scope]]]
}

执行栈

是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。当JS引擎工作时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。

作用域

let a = 'global';
console.log(a);

function first() {
    let b = 'first';
    console.log(b);

    second();
    function second() {
        let c = 'second';
        console.log(c);

        third();
        // 变量或函数提升-作用域之内(调用上级函数或者变量是闭包)
        console.log(e); // error: e is not defined(e申明在下级作用域)
        function third() {
            // let不支持提升
            let d = 'third';
            console.log(d); 
            console.log(e); // undefined: e变量提升(申明当未赋值)
            // 变量通过var支持提升,声明提升
            var e = 'inner';
            console.log(e); // inner
            console.log('test', b) // 作用域向上查找,向下传递
        }
    }
}
first();

//输出结果 global first second third undefined inner test first

为什么要变量提升:因为方便

function fn() {
  // 函数作用域
  let y = 1;
  let z = 2;
  
  if(true) {
    // 块级作用域
    var x = 2;
    let y = 2;
  }
  
  console.log(x);   // 2
  console.log(y);   // 1
}

fn();
console.log(x); // Error: x is not defined
// 函数作用域和块级作用域都能起到变量隔离作用(let const声明),但是使用var的情况下,仅函数作用域可以实现变量隔离
==> js module 模块化基础是函数(自执行函数IIFE

this指针

函数中直接调用 - this指向的是执行全局

function fn() {
    console.log('函数内部', this) // this -> window
    function fn1() {
        console.log('函数内部', this) // this -> window
    }
}

隐式绑定

function fn() {
    console.log('隐式绑定', this.a)
}
const obj = {
    a: 1,
    fn 
}

obj.fn = fn;
obj.fn();  // obj 调用fn

// 输出结果: 1

拓展

const foo = {
    bar: 10,
    fn: function() {
        console.log(this.bar);
        console.log(this);
    }
}
// 取出
let fn1 = foo.fn;
// 独立执行,所以 this-> window
fn1();

// 追问1: 如何改变属性指向
const o1 = {
    text: 'o1',
    fn: function(){
        // 直接使用上下文 - 传统派活
        console.log('o1fn_this', this);
        return this.text;
    }
}

const o2 = {
    text: 'o2',
    fn: function() {
        // 呼叫领导执行 —— 部门协作 (任然是o1调用)
        console.log('o2fn_this', this);
        return o1.fn();
    }
}

const o3 = {
    text: 'o3',
    fn: function() {
        // 直接内部构造 —— 公共人 (取出了o1的方法,全局调用)
        console.log('o3fn_this', this);
        let fn = o1.fn;
        return fn();
    }
} 

console.log('o1fn', o1.fn());
console.log('o2fn', o2.fn());
console.log('o3fn', o3.fn());

追问:现在我要将console.log('o2fn', o2.fn())的结果是o2

// 1. 人为干涉,改变this - bind / call / apply
o1.fn.call(o2);
// 2. 不需人为改变
 const o1 = {
    text: 'o1',
    fn: function(){
        // 直接使用上下文 - 传统派活
        console.log('o1fn_this', this);
        return this.text;
    }
}

const o2 = {
    text: 'o2',
    fn: o1.fn
}
console.log('o2fn', o2.fn());

追问: call/apply/bind的区别

三者都可以改变this的指向 call传参是依次传入(类数组),apply传参是整个数组 bind也是数组传参,但是返回一个新的函数,需要额外执行

手写函数

原理或者手写函数解题思路

  1. 说明原理,写下注释
  2. 根据注释,补全代码
    // 1. 需求:手写bind => bind位置(挂载在哪里)=> Function.prototype
    Function.prototype.newBind = function() {
        // 调用 bind 的不是函数,需要抛出异常
        if (typeof this !== "function") {
          throw new Error("Function.prototype.newbind is not a function");
        }
        // 2. bind是什么?
        // 改变this
        const _this = this;
        // 接受参数args,第一项参数是新的this,第二项到最后一项是函数传参
        const args = Array.prototype.slice.call(arguments);
        const newThis = args.shift();

        // 3. 返回值
        return function() {
            return _this.newApply(newThis, args);
        }
    }
    
    // 以上bind如果通过new方法构造对象内部this指向无法获取,需要增加逻辑判断是否空函数对象,返回原始this
    // var fNOP = function () {};
    // var fBound = function () {
    //    return _this.newApply(this instanceof fNOP ? this : newThis, args);
    // }
    
    // 空对象的原型指向绑定函数的原型
    // fNOP.prototype = this.prototype;
    // 空对象的实例赋值给 fBound.prototype
    // fBound.prototype = new fNOP();
    // return fBound;

    Function.prototype.newApply = function(context) {
        if (typeof this !== "function") {
          throw new Error("Function.prototype.newApply is not a function");
        }
        context = context || window;

        // 挂载执行函数
        context.fn = this;

        let result = arguments[1]
            ? context.fn(...arguments[1])
            : context.fn();
        
        delete context.fn;
        return result;
    }

闭包

一个函数和他周围状态的引用捆绑在一起的组合

// 函数作为返回值的场景
function mail() {
    let content = '信';
    return function() {
        console.log(content);
    }
}
const envelop = mail()
envelop()

// 函数作为参数的时候
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()();

// 立即执行函数 => js模块化的基石
let count = 0;
(function immediate(args) {
    if (count === 0) {
        let count = 1;

        console.log(count);
    }
})(args);

// 实现私有变量
function createStack() {
    const items = [];
    return {
        push(item) {
            items.push(item);
        }
    }
}