【一天一个知识点】:五种绑定弄懂this

162 阅读6分钟

this默认绑定

函数调用时无任何调用前缀的情景,我们称为默认绑定。默认绑定时,this指向全局对象(非严格模式)

function fn1() {
    let fn2 = function() {
        console.log(this);    // window
    }
    console.log(this);    // window
    fn2()
}

fn1();

注意:在严格模式环境中,默认绑定的this指向undefined;但在严格模式下调用不在严格模式中的函数,并不会影响this指向

function fn() {
    console.log(this);    //window
    console.log(this.name);
};

function fn1() {
    "use strict";
    fn();    // 调用不在严格模式中的函数
    console.log(this);    //undefined
    console.log(this.name);
}

var name = "心有林夕";

fn();    // "心有林夕"
fn1();   /* window
            "心有林夕"
            undefined
            TypeError: Cannot read property 'name' of undefined
         */

this隐式绑定

当函数调用时,前面存在调用它的对象,那么this就会隐式绑定到这个对象上,我们称之为隐式绑定

function fn() {
    console.log(this.name);
}
let obj = {
    name: '心有林夕'func: fn
}

obj.func();    // '心有林夕'

如果函数调用前存在多个对象,this指向距离调用自己最近(最内层)的对象。

function fn() {
    console.log(this.name)
};
let obj = {
    name: '心有林夕'func: fn
};
let obj1 = {
    name: '心有灵犀'obj: obj
};
obj1.obj.func();    // 心有林夕'

思考一个问题:如果此时我们将obj中的name删掉,那么会输出什么?

a. undefinded; b. '心有灵犀'

当然是undefined。obj对象虽然是obj1的属性,但是它两原型链并不相同,不是父子关系,所以当obj未提供name属性,会输出undefined。

function Fn() {};
Fn.prototype.name = '心有林夕';

function fn() {
    console.log(this.name);
};

const obj = new Fn();
obj.func = fn;

const obj1 = {
    name: '心有灵犀',
    o: obj
};

obj1.o.func();    // '心有林夕'

虽然obj对象并没有name属性,但顺着原型链找到了产生自己的构造函数Fn,进而找到Fn原型链中存在的name属性。

作用域链与原型链的区别: 当访问一个变量时,解释器会先在当前的作用域查找标识符,如果没有找到就去父作用域找,作用域链顶端时全局对象window,如果window都没有这个变量则报错

当在对象上访问某属性时,首先会查找当前对象,如果没有就顺着原型链往上找,原型链顶端时null,如果全程没有找到则返回一个undefined

隐式丢失:在特定情况下会存在隐式绑定丢失的问题,最常见的就是作为参数传递以及变量赋值

// 参数传递
const name = '心有林夕'const obj = {
    name: '心有灵犀',
    fn: function() {
        console.log(this.name);
    }
}

funciton fn1(func) {
    func();
}

fn1(obj.fn);    // '心有林夕'

在这个例子中,我们将obj.fn作为参数传递进fn1中执行,this并没有跟函数绑定在一起,所以this丢失,指向了window。

// 变量赋值
const name = '心有林夕'const obj = {
    name: '心有灵犀',
    fn: function() {
        console.log(this.name);
    }
}
let fn1 = obj.fn;

fn1();    // '心有林夕'

注意隐式绑定丢失并不是都会指向全局对象

const name = '心有林夕';
const obj = {
    name: '心有灵犀',
    fn: function() {
        console.log(this.name);
    }
}
const obj1 = {
    name: '心有灵犀一点通'
};

obj1.fn1 = obj.fn;
obj1.fn1();    // '心有灵犀一点通'

虽然丢失了obj的隐式绑定,但是在赋值的过程中,又建立了新的隐式绑定,此时this指向对象obj1。

this显式绑定

通过callapply以及bind方法改变this的指向,我们称为显式绑定

let obj1 = {name: '心有林夕'};
let obj2 = {name: '心有灵犀'};
let obj3 = {name: '心有灵犀一点通'};const name = '一点通';

function fn() {
    console.log(this.name);
};

fn();    // 一点通
fn.call(obj1);    // 心有林夕
fn.apply(obj2);    // 心有灵犀
fn.call(obj3)();    // 心有灵犀一点通

在js中,当我们调用一个函数时,我们习惯称之为函数调用,函数处于一个被动的状态;而call与apply让函数从被动变主动,函数能主动选择自己的上下文,所以这种写法又称为函数应用

注意: 如果在使用call之类的方法改变this指向时,指向参数提供的是null或者undefined,那么this将指向全局对象

let obj1 = {name: '心有林夕'};
let obj2 = {name: '心有灵犀'};
let obj3 = {name: '心有灵犀一点通'};const name = '一点通'function fn() {
    console.log(this.name);
};

fn();    // 一点通
fn.call(undefined);    // 一点通
fn.apply(null);    // 一点通
fn.call(undefined)();    // 一点通

call、apply与bind有什么区别?

  1. call、apply和bind都用于改变this绑定,但call、apply改变this指向的同时还会执行函数,而bind在改变this后返回一个全新的boundFunction绑定函数,这也是为什么上方例子中bind后面还加了一堆括号的原因。
  2. bind属于硬绑定,返回的bound Function的this指向无法再次通过bind、apply或call修改;call与apply的绑定只适用当前调用,调用完就没了,下次要用还要再次绑定。
  3. call与apply功能完全相同,唯一不同的call方法传递函数调用形参是以散列形式,而apply方法的形参是一个数组。在传参的情况下,call的性能要高于apply,因为apply在执行时还要多以不解析数组。
// 区别二:
let obj1 = {
    name: '心有林夕'
};
let obj2 = {
    name: '心有灵犀'
};
var name = '一点通';

function fn() {
    console.log(this.name);
};

fn.call(obj1); // 心有林夕
fn(); // 一点通
fn.apply(obj2); // 心有灵犀
fn(); // 一点通
let boundFn = fn.bind(obj1);// 心有林夕
boundFn.call(obj2);//心有林夕
boundFn.apply(obj2);//心有林夕
boundFn.bind(obj2)();//心有林夕

//区别三:
let obj = {
    name: '心有林夕'
};

function fn(age, desc) {
    console.log(`我是${this.name}, 今年${age}岁, 我很${desc}!`)
}

fn.call(obj, 18, '帅');
fn.apply(obj, [18, '帅']);

应用

// 获取数组中最大值和最小值
Math.max(1, 10); //10
Math.min(1, 10); //1
Math.max.apply(null, [1, 2, 10]); //10
Math.min.apply(null, [1, 2, 10]); //1
// 判断一个对象是不是数组或者函数
let a = [];
let b = function () {};
Object.prototype.toString.call(a) === "[object Array]";//true
Object.prototype.toString.call(b) === "[object Function]";//true

new绑定

new一个函数大致分为三步:

  1. 以构造器的prototype属性为原型,创建新对象;
  2. 将this(可以理解为第一步创建的新对象)和调用参数传给构造器,执行;
  3. 如果构造器没有手动返回对象,则返回第一步创建的对象。 new过程中会新建对象,此对象会继承构造器的原型与原型上的属性,最后它会被作为实例返回这样一个过程,这个过程我们称之为构造调用
function Fn() {
    this.name = '心有林夕';
}

const echo = new Fn();
console.log(echo.name);    // 心有林夕    

this绑定的优先级

显示绑定 > 隐式绑定 > 默认绑定

new绑定 > 隐式绑定 > 默认绑定

不存在显示绑定和new绑定同时生效的情景,同时出现会报错。

箭头函数的this

箭头函数中的this不适用上面介绍的四种绑定规则。准确来说,箭头函数没有this,箭头函数的this指向取决于外层作用域中的this,外层作用域或函数的this指向谁,箭头函数中的this便指向谁

function fn() {
    return () => {
        console.log(this.name);
    };
}
let obj1 = {
    name: '心有林夕'
};
let obj2 = {
    name: '心有灵犀'
};
let bar = fn.call(obj1); // fn this指向obj1
bar.call(obj2); //心有林夕

那么问题来了,为什么第一次绑定this返回箭头函数后,再次改变this指向没生效呢?

因为箭头函数this还有一个特性,那就是一旦箭头函数的this绑定成功,也无法被再次修改。但是我们可以通过修改外层函数this指向达到间接修改箭头函数this的目的。

function fn() {
    return () => {
        console.log(this.name);
    };
}
let obj1 = {
    name: '心有林夕'
};
let obj2 = {
    name: '心有灵犀'
};
fn.call(obj1)(); // 心有林夕
fn.call(obj2)(); // 心有灵犀