this指针、作用域&闭包

113 阅读5分钟

项目开发

image.png

创建作用域链:当前变量、所有父级变量

创建变量:包括参数、变量声明(可变参数)、函数

创建上下文(context): this 当前环境下默认指向

执行过程中:

  1. 变量的赋值 -> 查找作用域链
  2. 变量的使用 -> 查找作用域链
  3. 函数的引用

JS 的单线程操作

回调栈 call stack

image.png

作用域 + 上下文

作用域链 - 找祖宗的过程

面试题:

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

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

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

      teacher();
      function teacher() {
        let d = 'yy';
        console.log(d);

        console.log('test2', b); // 是否能找到?
      }
    }
  }

  console.log('test2', b);
  course();

test1 肯定可以,创建的时候有b test2 不可以,因为在外层

    1. 对于作用域链,我们可以直接通过创建态来定位作用域链
    1. 手动取消全局,使用块级作用域
if (true) {
  let e = 111;
  console.log(e);
}
console.log('test3', e)
// 减少window 挂载

this 上下文 context

  • 我家门前有条河,门前的河上有座桥,河里有群鸭

  • 我家门前有条河,「这河」上有座桥,「这河」里有群鸭 -> this

  • this 是在执行时动态读取上下文决定的,而不是在创建时!!!

考察重点

函数的直接调用中 - this 指向的是 window => 变种:函数表达式、嵌套函数、匿名函数

  function foo() {
    console.log('函数内部this', this)
  }

  foo(); // 函数内部指向全局window

隐式绑定 -> 链接和绑定 - this 的指向是调用堆栈的上一级 => 变种:对象、数组等引用等关系逻辑

  function fn() {
    console.log('隐式绑定', this.a)
  }

  const obj = {
    a: 1
  }

  obj.fn = fn;
  obj.fn(); // 1

回到刚刚的例子

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

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

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

      teacher();
      function teacher() {
        let d = 'yy';
        console.log(d);

        console.log('test2', b); // 是否能找到?
      }
    }
  }

  console.log('test2', b);
  course();

image.png

作用域链查找:底层 -> 上层,且是创建态

image.png

obj: {
  fn
}

obj 引用 fn 事实上引用的是 fn 的地址,地址中存的值是fn;那么在隐式绑定中,this 指向引用地址的人

面试题:

  const foo = {
    bar: 10,
    fn: function () {
      console.log(this.bar);
      console.log(this);
    }
  }

  // 1. 取出
  let fn1 = foo.fn;
  // 2. 执行
  fn1(); // 执行并没有通过foo执行
  // 执行环境会变!!!

追问1:如何改变指向?

  const o1 = {
    text: 'o1',
    fn: function() {
      // 直接使用上下文 - 传统分活
      return this.text;
    }
  }
  
  const o2 = {
    text: 'o2',
    fn: function() {
      // 呼叫领导执行 - 部门协作
      return o1.fn();
    }
  }

  const o3 = {
    text: 'o3',
    fn: function() {
      // 直接内部构造 - 公共人
      let fn = o1.fn;
      return fn();
    }
  }

  console.log('o1fn', o1.fn());
  console.log('o2fn', o2.fn()); // o1, 还是会指向o1
  console.log('o3fn', o3.fn()); // undefined 
    1. 在执行函数时,函数被上一级调用,上下文指向上一级
    1. or 直接变成公共函数,指向 window

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

  1. 人为干涉,改变 this - bind/call/apply
  2. 不许改变 this
  const o1 = {
    text: 'o1',
    fn: function() {
      return this.text;
    }
  }
  
  const o2 = {
    text: 'o2',
    fn: o1.fn
  }

  console.log('o2fn', o2.fn()); // o2, 这里this 指向最后调用它的对象,在执行 fn 时,o1.fn 抢过来直接挂载在自己的 o2fn 上即可

显式绑定 (bind | apply | call)

  function foo() {
    console.log('函数内部 this ', this);
  }

  foo();

  // 使用
  foo.call( {a: 1} );
  foo.apply( {a: 1} );

  const bindFoo = foo.bind( {a: 1} );
  bindFoo();

追问:call, apply, bind 的区别

    1. call < = > apply 传参不同,依次传入/数组传入
    1. bind 直接返回不同,返回可执行函数,需要再执行一下

new - this 指向的是 new 之后得到的实例

  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

追问:类中异步方法,this 有区别吗?

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

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

    asyncTest() {
      console.log('异步方法外的 this', this);
      setTimeout(function() {
        console.log('异步方法内的 this', this)
      })
    }
  }

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

setTimeout 内部有单独执行的函数指向window

    1. 执行setTimeout时,匿名方法执行时,效果和全局执行函数效果相同
    1. 追问,如何解决?-> 利用箭头函数(不会影响this 指向)

bind 原理/ 手写bind

  • 原理或者手写类题目,解题思路:
    1. 说明原理,写下注释
    1. 根据注释,补齐代码
  // 辅助函数
  function sum (a, b, c) {
    console.log('sum', this);
    return a + b + c;
  }
  // 1. 需求:手写bind => bind 位置(挂在哪里)=> 挂在 Function.prototype 上
  // 2. bind 是什么?=> a. 返回一个未执行函数 b. 返回原函数执行结果 c. 传参保持不变

  Function.prototype.newBind = function () {
    // 拿到当前上下文
    const _this = this;
    const args = Array.prototype.slice.call(arguments);
    // args 特点,第一项是新的 this , 第二项 ~ 最后一项为函数传参
    const newThis = args.shift();

    // a. 返回一个未执行函数
    return function () {
      // b. 返回原函数执行结果 c. 传参保持不变
      return _this.apply(newThis, args);
    }
  }

实现 apply

  Function.prototype.newApply = function (context) {
    // 边缘检测
    // 函数检测 防止显式call 调用 Function.prototype
    if (typeof this !=='function') {
      throw new TypeError('Error');
    }
    // 参数检测
    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);

模块作用单一职责原理,envelop 管理数据,mail 操作

函数嵌套

对外只暴露outerFn,内部有innerFn方法,但是内部逻辑看不见

  let counter = 0;

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

事件处理(异步执行)的闭包

  let lis = document.getElementByTagNames('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); // 0
    })(1);
  })(0);

立即执行遇上块级作用域

  let count = 0;

  (function immediate() {
    if (count === 0) {
      let count = 1;

      console.log(count);
    }
    console.log(count);
  })();
  // 信封套起来,内层信封读内层信,外层信封读外层信

拆分执行

  function createIncrement(){
    let count = 0;

    function increment() {
      count ++;
    }

    let message = `count is ${count}`;

    function log() {
      console.log(message);
    }

    return [increment, log]
  }

  const [increment, log] = createIncrement();

  increment();
  increment();
  increment();
  log(); // count is 0

  // 外部控制内部的功能

实现私有变量

  1. 什么是私有变量? - 让当前变量隐藏起来
  2. js 中有没有私有变量?
  3. 如何实现?
  function createStack() {
    return {
      items: [],
      push(item) {
        this.item.push(item);
      }
    }
  }

  const stack = {
    items: [],
    push: function () {

    }
  }

  function createStack() {
    const items = [];
    // 内部维护,外部只读
    return {
      items: [],
      push(item) {
        items.push(item);
      }
    }
  }
  // Vuex store 通过暴露的方法dispatch 修改值,但是不能直接改