this/词法作用域/手写bind和call

51 阅读3分钟

image.png

程序是怎么运行的 ?

传统的来说,编译,是一件很复杂的事情。 QString str = "hello";

  1. 先进行分词。 QString / str / = / "hello" /

  2. 构建 AST (抽象语法树) 给你反推出来,这句话的意思。

  3. 代码生成 最后,转换成为一段可以执行的机器指令。

对 JS 来说,主要分两个阶段

  • 预编译阶段 这个时候,由编译器,将 JS 编译成可执行的代码。
    • 进行变量声明
    • 进行变量提升, 但其值, 是 undefined
    • 所有非表达式的函数声明, 进行提升
  • 代码执行阶段

作用域

作用域,就是根据名称,去查找变量的规则。

词法作用域 / 静态作用域。

词法作用域,就是定义在,词法阶段的作用域。

动态作用域 / 上下文。

在运行时的作用域。

什么情况下,我的 上下文 和 作用域 不一致呢?

闭包的时候。

this

this 到底指向什么?

this 的指向, 是根据我的执行上下文, 动态决定的,通常情况下,谁调用了函数,函数内部的this就指向谁。

  • 在简单调用时,this 默认指向的是 window / global / undefined (浏览器/node/严格模式)
  • 在对象调用时,绑定在对象上
  • 在使用 call / apply / bind 时。绑定在制定的参数上
  • 使用 new 关键字时,绑定到新创建的对象上 ( 以上三条优先级: new > apply/call/bind > 对象调用 )
  • 使用箭头函数,根据外层的规则确定。

练习题

var number = 5;   
var obj = {
    number: 3,
    fn1: (function () {
        var number;  
        this.number *= 2; 
        number = number * 2;
        number = 3;
        return function () {
            var num = this.number; 
            this.number *= 2;
            console.log(num);  
            number *= 3; 
            console.log(number);  
        }
    })()
}

var fn1 = obj.fn1; 
fn1.call(null);  
obj.fn1();
console.log(window.number); 
// --------------答案------------
// 10、9、3、27、20

考察知识点:
- IIFE
- 静态/词法作用域(寻找变量)、动态作用域(this)
- call函数的第一个参数为null时,内部this指向谁?
- 闭包
const foo = {
    bar: 10,
    fn: function() {
        console.log(this);
        console.log(this.bar);
    }
}

var fn1 = foo.fn
fn1.call(foo);

// ------------答案-------------
// foo
// 10

const student = {
    name: 'luyi',
    fn: function() {
        return this;
    }
}

console.log(student.fn() === student)
// ------------答案-------------
// true


const o1 = {
    text: 'o1',
    fn: function() {
        return this.text
    }
}
const o2 = {
    text: 'o2',
    fn: function() {
        const m = o1.fn()
        return m;
    }
}
const o3 = {
    text: 'o3',
    fn: function() {
        // var fn = o1.fn
        // return fn()

        var fn = function() {
            return this.text
        };
        const res =    fn();
        return res;

    }
}

console.log(o1.fn())
console.log(o2.fn())
console.log(o3.fn());
// ----------------答案---------------
//  o1
// o1
// undefined

手写一个 bind / call

实现call/apply的关键

  • 原函数有的功能call也必须有,比如原函数接收参数、有返回值。那么call函数也要能接收参数,也要有返回值。
  • 除此之外就是要实现改变this指向的功能,这个我们要借助一句话:通过对象的方法调用函数时,函数内部的this会指向该对象。所以我们要把函数先挂载到我们要绑定的对象上,然后再通过该对象调用此方法,就可以实现改变this指向。

call方法的使用场景

  • a对象上没有某个方法,b对象上有对应的方法。a对象想调用b对象上的方法。

比如arguments上没有数组的slice方法,但是数组的原型上面有这个方法,那就可以借助call方法让arguments也拥有数组的方法,Array.prototype.slice.call(arguments,1)

function called(context, ...rest) {
   context.fn = this;
   console.log('context.fn',this)
   if(context) {
       const result = context.fn(...rest);
       delete context.fn;
       return result
   } else {
       this(...rest);
   }
}
o1.fn.called()

实现bind的关键

  • bind和call/apply的区别就是,bind不会直接调用函数,而需要手动调用。即bind的返回值是一个函数。
  • bind函数可以接收参数,bind函数的返回值也可以接收参数。
Function.prototype.bound = function (context, ...rest) {
    // context 使我们要被绑定的对象
    // this 就是我的 fn。谁调用了bound,this就指向谁。
    const fn = this;
    return function(...innerArgs) {
        return fn.apply(context, [...rest, ...innerArgs]);
    }
}
const o1 = {
    text: 'o1',
    fn: function(...rest) {
        console.log(this.text, ...rest)
    }
}
const o2 = {
    text: 'o2',
}

const m = o1.fn.bound(o2, 'luyi', 'xianzao')
m('yunyin', 'daxiong');

补充

node 、 浏览器是两个环境。(宿主环境,给你提供全局对象)