JS进阶 - 详解this

210 阅读8分钟

this是 JavaScript 中的关键字,与传统面向对象的语言相比, JavaScript 中的this更加灵活,涉及到它出现的位置和代表的含义。在讲述之前,我们先来看一下不使用 this 的情况。

this 的基本使用

  • 现在有一个需求,需要在对象的方法中输出一些信息,如下

    var obj = {
      name: "zhangsan",
      study: function () {
        console.log(obj.name + " study");
      },
    };
    

    上述代码的缺点:如果将 obj 名称改成 info,那么所有方法中的obj.name都需要换成info.name

  • 在实际开发中,我们会通过this关键字进行优化,如下

    var obj = {
      name: "zhangsan",
      study: function () {
        console.log(this.name + " study");
      },
    };
    

    经过上述修改后,方法中通过this引用,解耦了对变量名的依赖。修改对象变量名时,无需修改方法中引用的名称。

this 的指向

在全局作用域下,this 指向什么?

console.log(this); // window

var name = "zhangsan";

console.log(this.name); // zhangsan

console.log(window.name); // zhangsan

console.log(this === window); // true
  • 在浏览器环境中,全局作用域下,this 指向 window

在实际开发中,很少会直接在全局作用域下使用 this,通常都是在函数中使用。函数在被调用时,都会创建一个执行上下文,用于记录

  • 函数的调用栈、调用方式、传入参数等信息
  • this 也是其中的一个属性

定义一个函数、通过 3 种方式进行调用,产生了 3 种不同结果

// 定义一个函数
function foo() {
  console.log(this);
}

foo(); // window对象

var obj = {
  name: "zhangsan",
  foo: foo,
};

obj.foo(); // {name: "zhangsan", foo: ƒ}

foo.call("lisi"); // String {"lisi"}
  1. 函数在调用时,JavaScript 会默认给this绑定一个值
  2. this的绑定和函数定义的位置无关
  3. this的绑定和函数调用方式及调用位置有关
  4. this是在运行时绑定的

那么,this的绑定规则是怎样的呢?

this 绑定规则

1、默认绑定

独立的函数调用,可以理解成函数没有被绑定到某个对象上的调用

案例 1:普通函数调用
  1. 函数直接调用,没有与任何对象关联

  2. 这种独立的函数调用就会使用默认绑定,通常情况下,函数中的 this 指向全局对象window

    function foo() {
      console.log(this);
    }
    
    foo(); // window
    
案例 2:函数调用链
  1. 所有函数调用都没有绑定到对象上

    function foo() {
      console.log(this);
      bar(); // window
    }
    
    function bar() {
      console.log(this);
      baz(); // window
    }
    
    function baz() {
      console.log(this);
    }
    
    foo(); // window
    
案例 3:将函数作为另一个函数的参数进行调用
  1. 将函数作为参数,传入另一个函数中,在另一函数中直接调用该函数

    function foo(fn) {
      fn();
    }
    
    function bar() {
      console.log(this);
    }
    
    foo(bar); // window
    
案例 4:将对象中的一个方法传入另一个函数中进行调用
  1. 将对象中的方法传入到另一个函数中,另一个函数直接调用

    function foo(fn) {
      fn();
    }
    
    var obj = {
      name: "zhangsan",
      bar: function () {
        console.log(this);
      },
    };
    
    foo(obj.bar); // window
    
    • 上例中的 this 之所以是 window,是因为在真正调用函数的位置,并没有与对象进行关联,是一个独立的函数调用

2、隐式绑定

比较常见的调用方式,通过某个对象进行调用。也就是它的调用位置中,是通过某个对象发起的函数调用

案例 1:通过对象调用函数
  1. foo 的调用位置是obj.foo()方式进行调用的
  2. foo 调用时的 this 就会绑定到 obj 对象上
function foo() {
  console.log(this); // obj
  console.log(this === obj); // true
}

var obj = {
  name: "zhangsan",
  foo: foo,
};

obj.foo();
案例 2:多层对象调用函数
  1. 通过 obj2 引用了 obj1,再通过 obj1 对象调用 foo 函数
  2. foo 调用时的 this 还是绑定了 obj1 对象上
function foo() {
  console.log(this); // obj1
  console.log(this === obj1); // true
}

var obj1 = {
  name: "zhangsan",
  foo: foo,
};

var obj2 = {
  name: "zhangsan",
  obj1: obj1,
};

obj2.obj1.foo();
案例 3:隐式丢失
  1. foo 被调用的位置是 bar,而 bar 在调用时没有绑定任何对象,是独立函数调用。
function foo() {
  console.log(this); // window
  console.log(this === window); // true
}

var obj = {
  name: "zhangsan",
  foo: foo,
};

var bar = obj.foo;

bar();

隐式绑定的前提条件是

  • 必须在调用的对象内部有一个对函数的引用
  • 如果没有这样的引用,在进行调用时,会报错找不到该函数
  • 正是通过这个引用,间接将 this 绑定到这个对象上。

如果不希望对象内部包含这个属性,同时又希望在这个对象上强制调用,可以通过 callapply进行显式绑定

3、显式绑定

JavaScript 所有函数都可以使用 call()apply() 方法

  • 二者的区别在于第 2 个参数不同。call() 是参数列表、apply() 是数组.
  • 二者的第 1 个参数,是在函数调用时绑定到的 this 对象
案例 1:通过 call、apply 绑定 this
  1. 显式绑定后,this 就会明确的指向绑定的对象
function foo() {
  console.log(this);
}

var obj = {
  name: "zhangsan",
};

foo.call(window); // window
foo.call(obj); // obj {name: "zhangsan"}
foo.call("lisi"); // String {"lisi"}
案例 2:通过 bind 函数
  1. 通过 bind() 绑定 this 后,调用 bind() 方法返回的函数,该函数的 this 始终是传入 bind 函数的参数
function foo() {
  console.log(this);
}

var obj = {
  name: "zhangsan",
};

var bar = foo.bind(obj);

bar(); // obj {name: "zhangsan"}
bar(); // obj {name: "zhangsan"}

4、new 绑定

JavaScript 中的函数,可以通过 new 关键字当作一个类的构造函数来使用。通过 new 关键字调用函数时,会执行如下操作:

  1. 创建一个新对象
  2. 这个新对象会执行 prototype 连接
  3. 新对象会绑定到函数调用的 this
  4. 如果函数没有返回其他对象,则会返回这个新对象
function Person(name) {
  console.log(this); // Person {}
  this.name = name;
}

var p = new Person("zhangsan");

console.log(p); // Person {name: "zhangsan"}

this 绑定优先级

上面介绍了this绑定的四种规则,在开发中我们只需要查找函数的调用应用了哪条规则即可。但如果一个函数的调用位置应用了多条规则,哪条规则的优先级更高呢?

默认规则的优先级最低

默认规则的优先级是最低的,因为存在其它规则时,就会通过其它规则的方式绑定 this

显式绑定优先级高于隐式绑定

隐式绑定和显式绑定同时存在时,最终的this指向显式绑定传入的值,说明显式绑定的优先级高于隐式绑定

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

var obj1 = {
  name: "zhangsan",
  foo: foo,
};

var obj2 = {
  name: "lisi",
  foo: foo,
};

// 隐式绑定
obj1.foo(); // obj1 {name: "zhangsan", foo: ƒ}
obj2.foo(); // obj2 {name: "lisi", foo: ƒ}

// 隐式绑定和显式绑定同时存在
obj1.foo.call(obj2); // obj2 {name: "lisi", foo: ƒ}

new 绑定高于隐式绑定

new 绑定和隐式绑定同时存在时,最终的this指向 new 创建出来的对象,所以 new 绑定的优先级高于隐式绑定

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

var obj = {
  name: "zhangsan",
  foo: foo,
};

new obj.foo(); // foo {}

new 绑定高于 bind 绑定

  • new 绑定不能和 call()apply() 同时使用,所以不存在谁的优先级更高
function foo() {
  console.log(this);
}

var obj = {
  name: "zhangsan",
  foo: foo,
};

var obj2 = new obj.foo.call(obj); // Uncaught TypeError: obj.foo.call is not a constructor
  • new 绑定可以和 bind 一起使用,但最终this指向新创建出的对象,说明 new 绑定的优先级高于bind 绑定
function foo() {
  console.log(this);
}

var obj = {
  name: "zhangsan",
  foo: foo,
};

var bar = foo.bind(obj);

new bar(); // foo {}

绑定优先级

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

this 绑定规则特殊应用

忽略显示绑定

如果在显式绑定中,传入了nullundefined,那么这个显式绑定就会被忽略,使用默认规则。

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

var obj = {
  name: "zhangsan",
};

foo.call(obj); // obj
foo.call(null); // window
foo.call(undefined); // window

var bar = foo.bind(null);
bar(); // window

var baz = foo.bind(undefined);
baz(); // window

间接函数引用

创建一个函数的间接引用,这种情况使用默认规则。

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

var obj1 = {
  name: "zhangsan",
  foo: foo,
};

var obj2 = {
  name: "lisi",
};

obj1.foo(); //obj
(obj2.foo = obj1.foo)(); // window

ES6 中的箭头函数

在 ES6 中的箭头函数,不使用this的四种绑定规则,而是根据外层作用域的this来决定。

1、模拟一个网络请求
  • 使用setTimeout模拟网络请求,请求到数据后如何可以存放到 data 中呢?

  • 需要拿到 obj 对象,设置 data

  • 直接拿到的 this 是 window,我们需要在外层定义:var _this = this

  • setTimeout的回调函数中使用_this 就代表了 obj 对象

    var obj = {
      data: [],
      getData: function () {
        var _this = this;
        setTimeout(function () {
          // 模拟获取到的数据
          var res = ["abc", "cba", "nba"];
          _this.data.push(...res);
        }, 1000);
      },
    };
    
    obj.getData();
    

上例的代码在 ES6 之前是我们最常用的方式,从 ES6 开始,我们会使用箭头函数:

  • 为什么在setTimeout的回调函数中可以直接使用this呢?

  • 因为箭头函数并不绑定this对象,那么this引用就会从上层作用域中找到对应的this

    var obj = {
      data: [],
      getData: function () {
        setTimeout(() => {
          // 模拟获取到的数据
          var res = ["abc", "cba", "nba"];
          this.data.push(...res);
        }, 1000);
      },
    };
    
    obj.getData();
    

上例中如果 getData 也是一个箭头函数,那么setTimeout中的回调函数中的this指向谁呢?

  • 答案是window
  • 依然是不断的从上层作用域找,那么找到了全局作用域;
  • 在全局作用域内,this代表的就是window
var obj = {
  data: [],
  getData: () => {
    setTimeout(() => {
      console.log(this); // window
    }, 1000);
  },
};

obj.getData();

下一篇,梳理JavaScript中的函数式编程

  • 如果认为本篇知识点梳理的尚可,欢迎点赞
  • 如果有需要补充、指正的地方,也欢迎评论留言哦~