this

133 阅读7分钟

this 绑定

前沿:

当一个函数调用时,会创建一个执行上下文,包含函数调用的一些信息(调用栈,传入参数,调用方式),this 指向的就是这个上下文。this 不是静态的,不是在编写的时候绑定的,而是在函数运行的时候绑定的,它的绑定和函数声明的位置没有关系,只取决于函数的调用方式。

一. 默认绑定

默认绑定通常是函数独立调用,不涉及其他绑定

  • 1.1 -- 严格模式,this 指向 undefined.
"use strict"
var bar = 123;
function print(){
    console.log(this); //undefined
    console.log(window.bar) // 123
}
print();
  • 1.2 -- 非严格模式,this 指向 window
  • let,const, 声明定义变量时,存在暂时性死期,而且不会挂在在 window 对象上。
let a = "let";
const b = "const";
var bar = 123;
function print(){
    console.log(this); // window
    console.log(this.bar); //123
    console.log(window.bar); //123
    console.log(this.a); // undefined
    console.log(this.b); // undefined
}
print();

-1.3 -- 对象内执行

var a = 123;
function foo(){
    console.log(this.a);
}
var bar = {
    a: 456,
    fun(){
        foo(); 
    }
}
bar.fun()  // 123
//foo 虽然是在 bar 的 fun 函数内运行,但 foo 仍然是独立运行的,所以 this 指向 window。
  • 1.4 -- 函数内执行
var str = "outer";
var fun = function(){
    var str = "inner";
    function innerfun(){
        console.log(this.str);
    }
    innerfun(); // outer
}
fun();
//同上
  • 1.5 -- 自执行函数

自执行函数只要( js 代码)执行就会运行,并且只会执行一次。默认情况下,this 指向 window。

var str = "123";
(function(){
    console.log(this.str); //123
})()

二. 隐式绑定

  • 如果函数的调用是从某个对象上出发的,通俗点说就是“ XXX.func() ”这种调用模式,此时 this 指向XXX。如果存在链式调用,例如“ AAA.BBB.CCC.fun() ”,记住一个原则:this 永远指向离它最近的那个对象( CCC )。

  • 只要 func 前面什么都没有, 肯定不是隐式绑定

var a = 1;
function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo,
};
obj.foo(); // 2
//obj 是通过 var 定义的,obj 会挂载到 window 之上的, obj.foo() 就相当与 window.obj.foo(),这也印证了上面的原则。

三. 隐式绑定丢失

隐私绑定丢失之后,this 的指向会启用默认绑定。

  • 3.1 使用一个变量存放指向一个函数的指针,直接使用这个变量执行
var a = 1;
var obj = {
    a: 2,
    foo: function(){
        console.log(this.a)
    }
}
var foo = obj.foo;
obj.foo(); //2
foo(); // 1
// foo 真正执行时,相当于直接执行堆内存里的函数,与 obj 已经没有关系了。

var otherObj = {
    a: 3,
    foo: obj.foo
}
otherObj.foo() // 3
// otherObj.foo 指向 obj.foo 的堆内存,此后执行与 obj 无关(除非使用 call/apply 改变 this 指向)。
  • 3.2 函数作为参数传递
var foo = funcion(){
    console.log(this.a);
}
var doFoo = function(fn){
    console.log(this);
    fn();
}
var a = 1;
var obj1 = {
    a: 2,
    foo,
};
doFoo(obj1.foo); // window 1
//1.找出形参和变量声明,值赋予 undefined,2.将实参的值赋予形参。
//obj1.foo 作为实参,将指向 foo 地址的指针复制给形参 fn,此时 fn 执行已经和 Obj1 不会产生任何关系了,fn 为默认绑定 
 
var obj2 = {
    a: 3,
    doFoo,
}
obj2.doFoo(foo); // obj2 1
//obj2.doFoo 符合 XXX.fn 格式(隐式绑定),this 为 obj2
//(同上)
  • 3.3 回调函数
var name = "zhao";
var introduce = function(){
    console.log(this);
    console.log(this.name);
}
var info = {
    name: "qian",
    introduce1: function(){
        setTimeout(function(){
            console.log(this);
            console.log(this.name);
        }, 0)
    },
    introduce2: function(){
        setTimeout(()=>{
            console.log(this);
            console.log(this.name);
        }, 0)
    }
}
const Mary = {
    name: "Mary",
    introduce,
};
const Lisa = {
    name: "Lisa",
    introduce,
};
info.introduce1(); //window zhao
// setTimeout的第一个参数是回调函数,this 是默认绑定,指向 window

info.introduce2(); //info qian
// setTimeout的第一个参数是回调函数,this 是默认绑定,但是是一个箭头函数,它本身没有 this,如果函数内用到 this,则取决于它的上层作用域,introduce2 函数又属于 xxx.fn,所以。。。

setTimeout(Mary.introduce, 100); //window zhao
// setTimeout的第一个参数是回调函数,this 是默认绑定,指向 window

setTimeout(function(){
   Lisa.introduce();
}, 100)   // Lisa  "Lisa"
//符合  xxx.fn 模式

四. 显示(硬)绑定

fn.call(null) 或者 fn.call(undefined) 都相当于 fn()

  • 4.1 -- call

强行改变函数的 this 的指向,参数为若干列表,函数直接执行。

Function.propotype.call = function(){
    let contxt = Object(arguments[0]) || window;
    contxt.fn = this;
    let args = [];
    for(let i = 1; i < arguments.length; i++){
        args.push(arguments[i])
    }
    let result = contxt.fn(...args);  // xxx.fn, this 指向第一个参数
    delete contxt.fn;
    return result;
}
let name = "call"
let obj = {
    name: "123",
}
function foo(){
    console.log(this.name)
}
foo.call(obj) // "123"

  • 4.2 -- apply

强行改变函数的 this 的指向,参数数组列表,函数直接执行。

Function.prototype.apply = function(contxt, args){
    var contxt = Object(contxt) || window;
    contxt.fn = this;
    let result;
    if(args) {
        result = contxt.fn(...args);  // xxx.fn, this 指向第一个参数
    }else {
        result = contxt.fn();
    }
    delete contxt.fn;
    return result;
}
let name = "apply"
let obj = {
    name: "123",
}
function foo(){
    console.log(this.name)
}
foo.apply(obj) // "123"
  • 4.3 -- bind

强行改变函数的 this 的指向,参数是一个列表,返回一个新的函数。

//方法一
Function.prototype.bind = function(){
    var contxt = Object(arguments[0]) || window;
    contxt.fn = this;
    var args = [];
    for(let i = 1; i < arguments.length; i++){
        args.push(arguments[i])
    }
    return function(){
        contxt.fn(...args, ...arguments);
    }
}
var name = "bind";
var obj = {
    name: "123",
}
function foo(){
    console.log(this.name);
}
var fun = foo.bind(obj);
fun(); // "123"

//方法二
Function.prototype.myBind = function(){
    var first = [].shift.apply(arguments, arguments);
    var fn = this;
    var args = [...arguments];
    return function(){
        fn.apply(first, [...args, ...arguments])
    }
}

//求最小值
const arr = [2,3,5,1,5,6];
Math.min.apply(null, arr);

四. new 绑定

使用 new 来构建函数,会执行如下四部操作:

  1. 创建一个新的空对象,即 {}。
  2. 为空对象添加属性 proto , 将该属性链接至构造函数的原型对象。
  3. 执行构造函数,并将新对象作为 this 的上下文。
  4. 如果结果不是对象,则返回这个新对象。
function Foo() {
    getName = function () {
        console.log(1);
    };
    return this;
}
Foo.getName = function () {
    console.log(2);
};
Foo.prototype.getName = function () {
    console.log(3);
};
getName(); // 5 函数声明提升

var getName = function () {
    console.log(4);
};

getName(); // 4 

function getName() {
    console.log(5);
}

console.log(new Foo());
Foo.getName(); // 2
getName(); // 1 执行了new Foo, getName 重新赋值了
Foo().getName(); // 1  window 上面挂载的 getName 方法发生了替换
getName(); // 1 同上

let foo1 = new Foo.getName(); // 2
console.log(foo1.constructor); // Foo.getName
// 首先从左往右看: new Foo 属于不带参数列表的 new(优先级19),Foo.getName 属于成员访问(优先级20), getName() 属于函数调用(优先级20),同样优先级遵循从左向右执行
// 1. Foo.getName 执行,获取到 Foo 上的 getName 属性
// 2. 此时原表达式为 new (Foo.getName)(), new (Foo.getName) 为带参数列表, (Foo.getName)()属于函数调用, 从左往右执行

new Foo().getName(); // 3
// 首先从左往右看: new Foo() 属于带参数列表的 new(优先级20),Foo().getName 属于成员访问(优先级20), getName() 属于函数调用(优先级20)同样优先级遵循从左向右执行
// 先执行 new Foo(), 返回一个以 Foo 为实例的构造函数,
// Foo 实例上没有 getName 方法, 沿原型链找到 __proto__ 找到 Foo.prototype.getName 方法,打印 3

let foo2 = new new Foo().getName(); // 3
console.log(foo2.constructor); Foo.prototype.getName
// 首先从左往右看,第一个 new 不带参数列表(优先级19),new Foo()带参数列表(优先级20),剩下的属性访问,函数调用优先级都是20
// new Foo() 优先执行, 返回一个以 Foo 为构造函数的实例
// 执行 new (new Foo()).getName(), 返回一个以 Foo.prototype.getName 为构造函数的实例,打印 3

五. 箭头函数

  1. 箭头函数没有 this, 它里面的 this 取决于它的外层作作用域。此时 this 不取决于它执行时的环境,而取决于它定义时的环境。

  2. 不能通过显示绑定改变 this 的指向。箭头函数的底层逻辑就是绑定了它的外层作作用域,即 bind(外层作作用域)。

  3. 应该避免用箭头函定义对象方法,事件中的回调函数,DOM事件的回调函数中 this 已经封装指向了调用元素,如果使用箭头函数,会是其 this 指向 window 对象。

// 当 new 碰上箭头函数时
function User(name, age) {
    this.name = name;
    this.age = age;
    this.intro = function () {
        console.log("My name is " + this.name);
    };
    this.howOld = () => {
        console.log("My age is " + this.age);
    };
}
User.prototype.howDo = () => {
    console.log("My do " + this.age);
};
var name = "Tom",
    age = 18,
    zc = new User("ZC", 28);
zc.intro(); // "My name is ZC"
zc.howOld(); // "My age is 28"
// 箭头函数指向 User 函数定义时的this,外层作用域为 User, this 指向 zc
zc.howDo(); // "My do 18"

压轴题

var number = 5;
var objNum = {
    number: 3,
    fn: (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 myFun = objNum.fn;

myFun.call(null);
objNum.fn();
console.log(window.number);

//10, 9, 3, 27, 20

/*
    1, obj.fn 为立即执行函数: 默认绑定, this 指向 window
    此时的 obj 可以类似的看成以下代码(注意存在闭包)
    var objNum = {
    number: 3,
    fn: function () {
        var num = this.number;
        this.number *= 2;
        console.log(num);
        number *= 3;
        console.log(number);
    };
    var number: 立即执行函数的 AO(临时作用域) 中添加 number 属性,值为 undefined
    this.number *= 2: windo.number = 10
    number = number * 2: 立即执行函数的 AO(临时作用域)中 number 值为 undefined, number 赋值 NAN
    number = 3: number 赋值 3
    返回匿名函数,形成闭包

    2, myFun.call(null): 相当于 MyFun(), 隐式绑定丢失,MyFun 的 this 指向 woindow
    var num = this.number, this指向 window,num = window.number = 10;
    this.number *= 2: window.number = 20
    console.log(num); 打印 10
    number *= 3: 当前 AO 中没有 number 属性,沿作用域链可在立即执行函数的 AO 中查到 number 属性, 修改其值为 9, 打印 9

    3, obj.fn(): 隐式绑定,fn 的 this 指向 obj
    var num = this.number; this.number == obj.number  = 3, num = 3;
    this.number *= 2: obj.number = 3 * 2 = 6
    console.log(num): 打印 3
    number *= 3: 当前 A0(临时作用域)中不存在 number, 继续修改立即执行函数 AO(临时作用域)中的 number 9, number = 9 * 3 = 27;
    console.log(number): 打印 27

    4, console.log(window.number);
        window.number = 20  打印 20
*/

六. 优先级

显示绑定 > 隐式绑定

function foo(){
    console.log(this.num)
}
let obj1 = {
    num: 1,
    foo,
}
let obj2 = {
    num: 2,
    foo,
}
obj1.foo(); // 1
obj2.foo(); // 2

obj1.foo.call(obj2); // 2
obj2.foo.call(obj1); // 1

new 绑定 > 隐式绑定

function foo(str){
    this.a = str
}
var obj1 = {
    foo,
};
obj1.foo(2);
obj1.a; // 2

var bar = new obj1.foo(4);
bar.a; 4

new 绑定 > 显示绑定

function foo(str){
    this.a = str
}
var obj = {};
var bar = foo.bind(obj);
bar(2);
console.log(obj.a) // 2

var baz = new bar(3);
console.log(obj.a) // 2
console.log(baz.a) // 3
// 显示绑定函数如果被 new 调用,就会用使用创建的 this 替换硬绑定的 this。