likes
comments
collection
share

this的四个绑定规则

作者站长头像
站长
· 阅读数 53

this指向

  • 全局作用域下的this指向window
  • 如果给元素的事件行为绑定函数,那么函数中的this指向当前被绑定的那个元素
  • 函数中的this,要看函数执行前有没有., 有.的话,点前面是谁,this就指向谁,如果没有点,指向window
  • 自执行函数中的this永远指向window
  • 定时器中函数的this指向window
  • 构造函数中的this指向当前的实例
  • call、apply、bind可以改变函数的this指向
  • 箭头函数中没有this,如果输出this,就会输出箭头函数定义时所在的作用域中的this

this的四个绑定规则

1、默认绑定

规则:非严格模式默认指向window,严格模式指向undefined。

最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其他规则时的默认规则。

  • 不带任何修饰的函数引用进行调用(独立函数调用),指向window。
function foo() {
  console.log(this.a)
}
var a = 2
foo() // 2

解析:foo() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则foo 是属于独立函数调用的,触发了默认绑定,从而指向全局window

  • 函数调用链(一个函数又调用另外一个函数)
  • 将函数作为参数,传入到另一个函数中,作为函数的参数,指向window

2、隐式绑定

隐式绑定的 this,指向调用函数的上下文对象

2.1 一般的对象调用

规则:会把函数调用中的 this 绑定到这个上下文对象

function foo() {
  console.log(this.a)
}

const obj = {
  a: 2,
  foo: foo
}

// 通过 obj 对象调用 foo 函数
obj.foo() // 2

2.2 对象属性引用链

规则:对象属性引用链中只有上一层或者说最后一层在调用位置中起作用。

function foo() {
  console.log(this.a)
}

var obj2 = {
  a: 2,
  foo: foo
}

var obj1 = {
  a: 1,
  obj2: obj2
}

obj1.obj2.foo() // 2

2.3 隐式绑定丢失问题

  • ①将对象里的函数赋值给一个变量
function foo() {
  console.log(this.a)
}

var obj = {
  a: 2,
  foo: foo
}

var bar = obj.foo // 函数别名!

var a = 'global' // a 是全局对象的属性
bar() // "global"

虽然 bar 是 obj.foo 的一个引用,但是实际上,它引用的是 foo 函数本身,因此此时的 bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定

  • ②传入回调函数时
function foo() {
  console.log(this.a)
}

function doFoo(fn) {
  // fn 其实引用的是 foo
  fn() // <-- 调用位置!
}
var obj = {
  a: 2,
  foo: foo
}
var a = 'global'  // a 是全局对象的属性
doFoo(obj.foo)    // "global"

参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一 个例子一样。

3、显式绑定

直接指定 this 的绑定对象,因此我们称之为显式绑定

3.1 使用 call(...) 和 apply(...)

如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢? 可以使用函数的 call(..) 和 apply(..) 方法

JavaScript 提供的绝大多数函数以及你自 己创建的所有函数都可以使用 call(..) 和 apply(..) 方法。

function foo() {
  console.log(this.a)
}
var obj = {
    a: 2
}
foo.call(obj) // 2

3.2 硬绑定-bind

硬绑定是指一个函数总是显示的绑定到一个对象上

由于硬绑定是一种非常常用的模式,所以 ES5 提供了内置的方法 Function.prototype.bind, 它的用法如下

function foo(num) {
    console.log(this.a, num)
    return this.a + num
}

var obj = {
a: 2
}
// 调用 bind() 方法,返回一个函数,那么这个新函数的 `this`,永远指向我们传入的`obj`对象
var bar = foo.bind(obj)
var b = bar(3) // 2 3
console.log(b) // 5

调用 bind(...) 方法,会返回一个新函数,那么这个新函数的 this,永远指向我们传入的obj对象

3.3 API调用的 “上下文(内置函数)

第三方库的许多函数,以及 JavaScript 语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和 bind(..) 一样,确保你的回调函数使用指定的 this。 例如:

  • 数组方法 forEach()
function foo(el) {
  console.log(el, this.id)
}

var obj = {
  id: 'bin'
};
[1, 2, 3].forEach(foo, obj)

// 输出:
// 1 bin 
// 2 bin 
// 3 bin

调用 foo(..) 时把 this 绑定到 obj 身上

  • setTimeout() 的回调函数中的this一般指向 window

4、new绑定

new 绑定的 this,都指向通过 new 调用的函数的实例对象。

function foo(){
    this.a = 10;
    console.log(this);
}
foo();                    // window对象
console.log(window.a);    // 10   默认绑定

var obj = new foo();      // foo{ a : 10 }  创建的新对象的默认名为函数名
                          // 然后等价于 foo { a : 10 };  var obj = foo;
console.log(obj.a);       // 10    new绑定

使用new调用函数后,函数会 以自己的名字 命名 和 创建 一个新的对象,并返回。

特别注意 : 如果原函数返回一个对象类型,那么将无法返回新对象,你将丢失绑定this的新对象

function foo(){
    this.a = 10;
    return new String("捣蛋鬼");
}
var obj = new foo();
console.log(obj.a);       // undefined
console.log(obj);         // "捣蛋鬼"

绑定规则的优先级

它们之间的优先级关系为:

默认绑定 < 隐式绑定 < 显示绑定(bind) < new绑定

1、隐式绑定和显式绑定的优先级比较

function foo() {
  console.log(this.a)
}

var obj1 = {
  a: 1,
  foo: foo
}

var obj2 = {
  a: 2,
  foo: foo
}

// 属性调用是隐式 call是显式
obj1.foo.call(obj2) // 2

可以看到,输出的结果为 2,说明 foo 函数内 this 指向的是 obj2,而 obj2 是通过显示绑定调用的,所以:显示绑定的优先级更高

2、隐式绑定和 new 绑定的优先级比较

function foo() {
  console.log(this);
}

var obj = {
  title: "juejin",
  foo: foo
}

// 同时使用隐式绑定和new绑定
new obj.foo(); // foo对象

最后 foo 函数输出的 this 为 foo 对象,说明new绑定优先级更高(否则应该输出 obj 对象),所以:new 绑定的优先级更高

3、new 绑定和显示绑定的优先级比较

new绑定和call、apply是不允许同时使用的,只能和 bind 相比较,如下:

function foo() {
  console.log(this)
}

var obj = {
  title: "juejin"
}

var foo = new foo.call(obj);  // 直接报错

但是 new 绑定可以和 bind 方法返回后的函数一起使用

function foo() {
  console.log(this);
}

var obj = {
  title: "juejin"
}

var bar = foo.bind(obj);

var foo = new bar(); // foo 对象, 说明使用的是new绑定

最后 foo 函数输出的 this 为 foo 对象,说明new绑定优先级更高(否则应该输出 obj 对象),所以:new 绑定的优先级更高

绑定例外

1、箭头函数

箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this。(根据父级作用域决定)

2、null/undefined显式绑定this

如果你把null或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值在调用时会被忽略,实际应用的是默认绑定规则:

function foo() {
  console.log(this.a)
}

var a = 2

foo.call(null) // 2
foo.call(undefined) // 2
foo.bind(null)();//2

最后输出的结果都是 2,说明 this 指向的是全局 window

3、间接引用-函数的赋值

创建一个函数的间接引用,在这种情况下,调用这个函数会应用默认绑定规则。 间接引用最容易在赋值时发生:

function foo() {
  console.log(this.a)
}
var a = 2
var o = { a: 3, foo: foo }
var p = { a: 4 }

o.foo(); // 3

// 函数赋值
(p.foo = o.foo)()  // 2

赋值表达式 p.foo = o.foo 的返回值是目标函数的引用,因此调用位置是 foo() 属于独立函数调用,而不是 p.foo() 或者 o.foo()。根据我们之前说过的,这里会应用默认绑定

总结

如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面这四条规则来判断 this 的绑定对象。

  1. 由 new 调用?绑定到新创建的对象。
  2. 由 call 或者 apply(或者 bind)调用?绑定到指定的对象。
  3. 由上下文对象调用?绑定到那个上下文对象。
  4. 如果以上都不是,那么使用默认绑定。默认:在严格模式下绑定到 undefined,否则绑定到全局对象。
  5. 如果把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind, 这些值在调用时会被忽略,实际应用的是默认绑定规则。
  6. 箭头函数没有自己的 this, 它的this继承于上一层代码块的this。

习题

例题一

var name = "window";
var person = {
  name: "person",
  sayName: function () {
    console.log(this.name);
  }
};
function sayName() {
  var sss = person.sayName;
  sss(); 
  person.sayName(); 
  (person.sayName)(); 
  (b = person.sayName)(); 
}
sayName();

解析:

function sayName() {
  var sss = person.sayName;
  // 独立函数调用,没有和任何对象关联
  sss(); // window
  // 关联
  person.sayName(); // person -- 隐式绑定
  (person.sayName)(); // person -- 隐式绑定
  (b = person.sayName)(); // window -- 函数赋值
}

例题二

var name = 'window'
var person1 = {
  name: 'person1',
  foo1: function () {
    console.log(this.name)
  },
  foo2: () => console.log(this.name),
  foo3: function () {
    return function () {
      console.log(this.name)
    }
  },
  foo4: function () {
    return () => {
      console.log(this.name)
    }
  }
}

var person2 = { name: 'person2' }

person1.foo1(); 
person1.foo1.call(person2);

person1.foo2();
person1.foo2.call(person2);

person1.foo3()();
person1.foo3.call(person2)();
person1.foo3().call(person2);

person1.foo4()();
person1.foo4.call(person2)();
person1.foo4().call(person2);

解析

// 隐式绑定,肯定是person1
person1.foo1(); // person1
// 隐式绑定和显示绑定的结合,显示绑定生效,所以是person2
person1.foo1.call(person2); // person2

// foo2()是一个箭头函数,不适用所有的规则
person1.foo2() // window
// foo2依然是箭头函数,不适用于显示绑定的规则
person1.foo2.call(person2) // window

// 获取到foo3,但是调用位置是全局作用于下,所以是默认绑定window
person1.foo3()() // window
// foo3显示绑定到person2中
// 但是拿到的返回函数依然是在全局下调用,所以依然是window
person1.foo3.call(person2)() // window
// 拿到foo3返回的函数,通过显示绑定到person2中,所以是person2
person1.foo3().call(person2) // person2

// foo4()的函数返回的是一个箭头函数
// 箭头函数的执行找上层作用域,是person1
person1.foo4()() // person1
// foo4()显示绑定到person2中,并且返回一个箭头函数
// 箭头函数找上层作用域,是person2
person1.foo4.call(person2)() // person2
// foo4返回的是箭头函数,箭头函数只看上层作用域
person1.foo4().call(person2) // person1

例题三

var x = 10;
var obj = {
    x: 20,
    f: function(){
        console.log(this.x);
        var foo = function(){ 
            console.log(this.x);    
            }
        foo();
    }
};
obj.f();

解析 :考点 1.  this默认绑定 2.  this隐性绑定

var x = 10;
var obj = {
    x: 20,
    f: function(){
        console.log(this.x);    // 20
                                // 典型的隐性绑定,这里 f 的this指向上下文 obj ,即输出 20
        function foo(){ 
            console.log(this.x); 
            }
        foo();       // 10
                     //有些人在这个地方就想当然的觉得 foo 在函数 f 里,也在 f 里执行,
                     //那 this 肯定是指向obj 啊 , 仔细看看我们说的this绑定规则 , 对应一下很容易
                     //发现这种'光杆司令',是我们一开始就示范的默认绑定,这里this绑定的是window
    }
};
obj.f();             

例题四

function foo(arg){
    this.a = arg;
    return this
};

var a = foo(1);
var b = foo(10);

console.log(a.a);    // ?
console.log(b.a);    // ?

答案 : undefined 10 解析 :考点 1.  全局污染 2.  this默认绑定

这道题很有意思,问题基本上都集中在第一undefined上,这其实是题目的小陷阱,但是追栈的过程绝对精彩 让我们一步步分析这里发生了什么:

  • foo(1)执行,应该不难看出是默认绑定吧 , this指向了window,函数里等价于 window . a = 1,return window;
  • var a = foo(1) 等价于 window . a = window , 很多人都忽略了var a 就是window.a ,将刚刚赋值的 1 替换掉了。
  • 所以这里的 a 的值是 window , a . a 也是window , 即window . a = window ; window . a . a = window;
  • foo(10) 和第一次一样,都是默认绑定,这个时候,将window.a 赋值成 10 ,注意这里是关键,原来window.a = window ,现在被赋值成了10,变成了值类型,所以现在 a.a = undefined。(验证这一点只需要将var b = foo(10);删掉,这里的 a.a 还是window)
  • var b = foo(10); 等价于 window.b = window;

本题中所有变量的值,a = window.a = 10 , a.a = undefined , b = window , b.a = window.a = 10;


例题五

var x = 10;
var obj = {
    x: 20,
    f: function(){ console.log(this.x); }
};
var bar = obj.f;
var obj2 = {
    x: 30,
    f: obj.f
}
obj.f();
bar();
obj2.f();

答案:20 10 30 解析:传说中的送分题,考点,辨别this绑定

var x = 10;
var obj = {
    x: 20,
    f: function(){ console.log(this.x); }
};
var bar = obj.f;
var obj2 = {
    x: 30,
    f: obj.f
}
obj.f();    // 20
            //有上下文,this为obj,隐性绑定
bar();      // 10
            //'光杆司令' 默认绑定  ( obj.f 只是普通的赋值操作 )
obj2.f();   //30
            //不管 f 函数怎么折腾,this只和 执行位置和方式有关,即我们所说的绑定规则
            

例题六

function foo() {
    getName = function () { console.log (1); };
    return this;
}
foo.getName = function () { console.log(2);};
foo.prototype.getName = function () { console.log(3);};
var getName = function () { console.log(4);};
function getName () { console.log(5);}
 
foo.getName ();                // ?
getName ();                    // ?
foo().getName ();              // ?
getName ();                    // ?
new foo.getName ();            // ?
new foo().getName ();          // ?
new new foo().getName ();      // ?

答案:2 4 1 1 2 3 3 解析:考点 1. new绑定 2.隐性绑定 3. 默认绑定 4.变量污染

function foo() {
    getName = function () { console.log (1); }; 
        // 因为该函数返回this,而此时的this指向window
        //这里的getName 将创建到全局window上
    return this;
}
foo.getName = function () { console.log(2);};   
        //这个getName和上面的不同,是直接添加到foo上的
foo.prototype.getName = function () { console.log(3);}; 
        // 这个getName直接添加到foo的原型上,在用new创建新对象时将直接添加到新对象上 
var getName = function () { console.log(4);}; 
        // 和foo函数里的getName一样, 将创建到全局window上
function getName () { console.log(5);}    
        // 同上,但是这个函数不会被使用,因为函数声明的提升优先级最高,所以上面的函数表达式将永远替换
        // 这个同名函数,除非在函数表达式赋值前去调用getName(),但是在本题中,函数调用都在函数表达式
        // 之后,所以这个函数可以忽略了
        
        // 通过上面对 getName的分析基本上答案已经出来了

foo.getName ();                // 2
                               // 下面为了方便,我就使用输出值来简称每个getName函数
                               // 这里有小伙伴疑惑是在 2 和 3 之间,觉得应该是3 , 但其实直接设置
                               // foo.prototype上的属性,对当前这个对象的属性是没有影响的,如果要使
                               // 用的话,可以foo.prototype.getName() 这样调用 ,这里需要知道的是
                               // 3 并不会覆盖 2,两者不冲突 ( 当你使用new 创建对象时,这里的
                               // Prototype 将自动绑定到新对象上,即用new 构造调用的第二个作用)
                               
getName ();                    // 4 
                               // 这里涉及到函数提升的问题,不知道的小伙伴只需要知道 5 会被 4 覆盖,
                               // 虽然 5 在 4 的下面,其实 js 并不是完全的自上而下,想要深入了解的
                               // 小伙伴可以看文章最后的链接
                               
foo().getName ();              // 1 
                               // 这里的foo函数执行完成了两件事, 1. 将window.getName设置为1,
                               // 2. 返回window , 故等价于 window.getName(); 输出 1
getName ();                    // 1
                               // 刚刚上面的函数刚把window.getName设置为1,故同上 输出 1
                               
new foo.getName ();            // 2
                               // new 对一个函数进行构造调用 , 即 foo.getName ,构造调用也是调用啊
                               // 该执行还是执行,然后返回一个新对象,输出 2 (虽然这里没有接收新
                               // 创建的对象但是我们可以猜到,是一个函数名为 foo.getName 的对象
                               // 且__proto__属性里有一个getName函数,是上面设置的 3 函数)
                               
new foo().getName ();          // 3
                               // 这里特别的地方就来了,new 是对一个函数进行构造调用,它直接找到了离它
                               // 最近的函数,foo(),并返回了应该新对象,等价于 var obj = new foo();
                               // obj.getName(); 这样就很清晰了,输出的是之前绑定到prototype上的
                               // 那个getName  3 ,因为使用new后会将函数的prototype继承给 新对象
                               
new new foo().getName ();      // 3
                               // 哈哈,这个看上去很吓人,让我们来分解一下:
                               // var obj = new foo();
                               // var obj1 = new obj.getName(); =>  var obj1 = new (obj.getName)();
                               // 好了,仔细看看, 这不就是上两题的合体吗,obj 有getName 3, 即输出3
                               // obj 是一个函数名为 foo的对象,obj1是一个函数名为obj.getName的对象


例题七 箭头函数

function foo(){
    return ()=>{
        console.log(this.a);
    }
}
foo.a = 10;

// 1. 箭头函数关联父级作用域this

var bar = foo();            // foo默认绑定
bar();                      // undefined 哈哈,是不是有小伙伴想当然了

var baz = foo.call(foo);    // foo 显性绑定
baz();                      // 10 

// 2. 箭头函数this不可修改
//这里我们使用上面的已经绑定了foo 的 baz
var obj = {
    a : 999
}
baz.call(obj);              // 10

例题八 箭头函数

var people = {
    Name: "海洋饼干",
    getName : function(){
        return ()=>{
            console.log(this.Name);
        }
    }
};
var bar = people.getName(); //获得一个永远指向people的函数,不用想this了,岂不是美滋滋?

bar();    // 海洋饼干 
var obj= {
    that : this,
    bar : function(){
        return ()=>{
            console.log(this);
        }
    },
    baz : ()=>{
        console.log(this);
    }
}
console.log(obj.that);  // window
obj.bar()();            // obj
obj.baz();              // window
  • 我们先要搞清楚一点,obj的当前作用域是window,如 obj.that === window。
  • 如果不用function(function有自己的函数作用域)将其包裹起来,那么默认绑定的父级作用域就是window。
  • 用function包裹的目的就是将箭头函数绑定到当前的对象上。函数的作用域是当前这个对象,然后箭头函数会自动绑定函数所在作用域的this,即obj。

摘录好文:

转载自:https://juejin.cn/post/7144332319279546404
评论
请登录