this看这篇就够了

83 阅读7分钟

贪安稳就没有自由,要自由就要历些危险。只有这两条路。 --鲁迅

this是什么

this是在运行时绑定的,并不是在编写时绑定(大部分情况),它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。

当函数被调用时,会创建一个活动记录(有时也成为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用凡是。传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到。

function foo(){
  var a = 10;
  console.log(this.a);
}
var a = 20;
foo() // 20

解析: this 和 作用域注意区分,作用域是词法的与函数声明位置有关,但是this不是,this和函数的调用位置有关。非严格模式下foo中的this指向window,所以输出的a为20。至于为什么foo的this 是指向window,我们稍后解析。

this绑定规则

默认绑定

独立函数调用。可以把这条规则看做是无法应用其他规则时的默认规则

上述 foo() // 20 例子就是本条规则的代表。在上述代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法使用其他规则。

如果使用严格模式(strict mode),则不能将全局对象用于默认绑定,因此this会被绑定为undefined。

function foo(){
  'use strict';
  var a = 10;
  console.log(this.a);
}
var a = 20;
foo() // TypeError this is undefined

注意📢:只有foo运行在非严格模式下时,默认绑定才能绑定到全局对象;在严格模式下调用foo()则不影响默认绑定:

function foo() {
  console.log(this.a);
}
(function(){
  'use strict';
  foo(); // 20
  var a = 20
})()

隐式绑定

当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。

var obj = {
    a:'test obj this',
    testThis:function(){
        console.log(this.a);
    }
}

var a = "test this";
obj.testThis();   // => test obj this

解析: 将this绑定到了obj上,因此this.a 和 obj.a 是一样的。

隐式丢失

一个最常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,将this绑定到全局对象或则undefined上。

上述隐式绑定示例代码改变一下

var obj = {
    a:'test obj this',
    testThis:function(){
        console.log(this.a);
    }
}

var a = "test this";
var testThis =  obj.testThis;
testThis();

思考一下会输出什么?

结果是输出test this,和聪明的你的答案是一样的么?

解析: testThis()其实是一个不带任何修饰的函数调用,因此应用了默认绑定,this绑定到了全局对象上。

🤔再思考一下,下面代码会输出什么?

var obj = {
    a:'test obj this',
    testThis:function(fn){
        fn();
    }
}

var a = "test this";
function sayA(){
  console.log(this.a);
}
obj.testThis(sayA);

⏱ 思考两秒钟再看答案

结果依然是 test this

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

🤔再思考一下,下面代码会输出什么?

var obj = {
    a:'test obj this',
    testThis:function(){
        console.log(this.a);
    }
}
var a = "test this";
setTimeout(obj.testThis, 0);

聪明的你一定已经知道答案了吧,试着自己分析一下吧~

显示绑定

可以直接指定this的绑定对象,我们称之为显示绑定

显示绑定的几种方式

  1. call
  2. apply
  3. bind(返回绑定this的函数)

硬绑定

先看下面一段代码

⏱ 思考一下会输出什么?

function sayName(){
  console.log(this.name);
}
var obj = {
  name: 'obj'
}
var name = 'global';
function foo(fn){
  fn();
}
foo.call(obj,sayName);

答案是 global

解析: foo函数强制绑定了obj对象,但是fn函数在赋值时还是会发生this指向丢失导致this指向全局变量。

我们对上面代码进行一下变形

⏱ 再思考一下会输出什么?

function sayName(){
  console.log(this.name);
}
var obj = {
  name: 'obj'
}
var name = 'global';
function foo(fn){
  fn.call(this);
}
foo.call(obj,sayName);

答案是obj

解析: 没错, 我们将fn的this硬绑定到foo的this,这种绑定是一种显示的强制绑定,也可以硬绑定

手写 call、apply、bind 实现

// 手写 call
Function.prototype.myCall = function(context , ...args){
    const fn = this;
    if(context){
        context.fn = fn;
        const result = context.fn(...args);
        delete context.fn;
        return result;
    }else{
        return fn(...args)
    }

};
// 手写bind
Function.prototype.myBind = function(){
    const params = [...arguments];
    const context = params.slice(0,1)[0];
    const args = params.slice(1);
    const fn = this;
    return function Fn(){
        const newArgs = args.concat([...arguments]);
        //  如果new了  bind 返回的对象, this指向指向调用函数
        return  this instanceof Fn ? new fn(...arguments) :  fn.apply( context, newArgs);
    }
  }

⏱ 思考一下会输出什么呢?

var obj = {
  name : 'obj'
}

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

var sayName_bind = sayName.bind(obj);

var s = new sayName_bind();

答案是undefined

解析: new 也会改变this指向,this指向调用函数,具体如何改变我们关于new 绑定会介绍,调用函数的this中没有name,所以输出undefined

new 绑定

使用new来调用构造函数时,会构造一个新对象并把它绑定到构造函数调用中的this上。new是一种可以影响函数调用时this绑定行为的方法,所以称之为new 绑定

function foo(a){
  this.a = a ;
}
var f = new foo(2);
console.log(f.a); // 2

new 调用函数都做了什么?

  1. 创建一个全新的对象
  2. 新对象被执行Prototype连接
  3. 新对象会绑定到函数调用的this
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象;如果返回了其他对象则返回其他对象。

手写new

function _new(fn, ...args) {
    if (Object.prototype.toString.call(fn) === '[object Function]') {
        const obj = {};
        obj.__proto__ = fn.prototype;
        const result = fn.apply(obj, args);
        if (result) {
            return result;
        }
        return obj;
    }
  throw ('fn is need to be function')
}

⏱ 思考一下会输出什么?

Function.prototype.a = 'a';
Object.prototype.b = 'b';
function Foo(){
  
}
var f = new Foo();
console.log(f.a);
console.log(f.b);

答案是 undefinedb

解析: 上述介绍了new 的过程都做了什么,其中有创建新对象并将新对象绑定到函数调用的this,所以我们能获取到对象原型链上的b;至于a为什么没有拿到 是因为Foo的prototype指向的是Object.prototype;Foo.proto_ 指向 Foo.prototype,具体它们之间的关系会在下一篇你知道这样的原型链吗?中讲解。

⏱ 再思考一下会输出什么?

function Foo(name){
  this.name = name;
  return {
    name: '1'
  }
}

var f = new Foo('foo');
console.log(f.name);

答案是 1

解析:还是new都做了什么中提到的 如果调用函数没有返回值则返回这个创建的新对象,如果有返回值得话,会返回函数的返回值,所以会输出 1

优先级

this绑定的优先级 new绑定 > 显示绑定 >隐式绑定> 默认绑定

词法this(箭头函数)

箭头函数并不适用上述规则。

箭头函数的this是词法的,不会动态改变,即便是使用call,bind或者apply 去绑定this值, 也不会改变箭头函数最初的this指向。

箭头函数this指向父级作用域。

⏱思考一下会输出什么?

var obj = {
    a : 'obj a ',
    sayThis: function(){
        console.log(this.a)
    },
    sayThisArrow: ()=>{
        console.log(this.a);
    }
}
var a = 'global a ';
obj.sayThis();
obj.sayThisArrow();

答案: obj aglobal a

解析: 箭头函数this是词法的,指向父级作用域。那么当前非严格模式下父级作用域即为全局作用域。

将上述代码做下变型,⏱再来思考一下输出什么?

var obj = {
    a : 'obj a ',
  	sayThis: function(){
        return function () {
          	console.log(this.a)
        }
    },
    sayThisArrow: function () {
        return () =>{
          console.log(this.a);
        }
    }
}
var a = 'global a ';

var s1 = obj.sayThis();
s1();
var s2 = obj.sayThisArrow();
s2();

聪明的你尝试自己解析一下吧 ~

⏱思考一下输出什么?

var foo = () =>{
  console.log(this.a);
}
var a = 'a';
var obj = {
  a: 'a1'
}
foo.call(obj);

答案: a

解析: 箭头函数的this是词法的,只跟函数定义的位置有关,显示绑定隐式绑定默认绑定都不适用。

接下来我们测试下 new绑定适用吗?

⏱思考一下输出什么?

var Foo = (name) => {
  	this.name = name;
}
var f = new Foo('foo');
console.log(f);

答案: TypeError Foo is not a constructor, 没错箭头函数不能当做调用函数(构造函数)使用,所以箭头函数不会出现new绑定。

词法 arguments

箭头函数没有自己的arguments而是继承自父级。

⏱ 思考一下输出什么?

var foo = () => {
  console.log(arguments);
}
foo(1,2);

答案: ReferenceError arguments is not defined;

解析: 箭头函数没有自己的arguments,会报错的原因是因为非赋值变量会进行RHS查询,如果没有再作用域中找到则会报错。详细请看上一篇你不知道的作用域

⏱思考一下输出什么?

function foo() {
  return () =>{
    console.log(arguments);
  }
}

foo(1,2,3)();

答案: 输出类数组 Arguments[1,2,3]