JS中的this指向总结
为什么会出现this
首先在学习this到底指向谁之前,先要知道为什么要有this?实际上从某些角度来说,即便没有this也无伤大雅,我们先看个例子。
const obj = {name: "coderwei",running() {console.log(obj.name + "在跑步");},sleep() {console.log(obj.name + "在睡觉");},study() {console.log(obj.name + "在学习");},};obj.running(); //coderwei在跑步obj.sleep(); //coderwei在睡觉obj.study(); //coderwei在学习
在这个例子中,我们会发现,即便不用this,也不影响我们代码的正常执行,但是如果有一天obj这个对象的名字发生了变化,那么内部的所有obj都要换成对应的名字,否则就找不到这个对象了,这样就会影响我们代码的正常运行。所以我们就需要一个对象,能够指向他自身,那么这个时候this就理所当然的出现了。
const obj = {name: "coderwei",running() {console.log(this.name + "在跑步");},sleep() {console.log(this.name + "在睡觉");},study() {console.log(this.name + "在学习");},};obj.running(); //coderwei在跑步obj.sleep(); //coderwei在睡觉obj.study(); //coderwei在学习
我们会神奇的发现代码照常运行,并没有任何问题,更神奇的是这个this还是动态绑定的,这也是this那么晦涩难懂的原因,他在不同的情况下会有不同的绑定规则。
this的绑定规则
以下绑定规则都是在非严格模式下
如果没有指明在什么环境下,默认都是在浏览器环境下
全局作用域下的this到底指向谁?
首先我们需要知道,this的指向和所处的环境有关,在浏览器下面和在node环境下指向的是不同的东西,具体有什么区别在实际案例中会指出,首先我们先看下面例子:
console.log(this);
我们只是单纯的输出一下this,会发现在浏览器下面运行这段js脚本输出的是window,而在node环境下输出的是一个空对象({}),首先在浏览器下,这个this指向浏览器的window对象,这没什么好说的,在node环境下为什么会指向一个空对象?那是因为node是commonjs的最成功的实践者,他内部使用commonjs的模式开发,他将每个文件都分成一个模块,而这个this就是使用了call绑定到一个变量thisValue,看下面这行图片

那么问题来了,this.exports有是一个什么东西呢?继续看下面这一张图片

显而易见,这个东西就是一个空对象。我的理解是这个空对象实际上就是module.exports,如果我的理解有误,可以随时联系提醒我。
默认绑定
//定义一个函数function foo() {console.log(this);}foo(); //window
我们会发现这里的this在浏览器环境下指向window,而在node环境下这里的this指向global全局对象。其实在判断函数的this指向的时候,我们第一点需要判断的是他是不是一个独立的函数调用,如果是,那么都是指向window的。
看这个例子:
let obj = {name: "coderwei",foo: function () {console.log(this);},};let bar = obj.foo;bar(); // window
一样的,不用去管bar对象是谁给的,怎么给的,就看他是怎么调用的,前面赋值在写的花里胡哨,只需要永远记得,this是在执行的时候动态绑定的,只要你的调用的是一个单独的函数调用,那么这个this就是指向window。
隐式绑定
隐式绑定在实际开发中也很常见,是通过某个对象调用的,这个时候会进行一个隐式绑定。
函数和方法
在js中,方法和函数有什么区别?我们主要看这个函数是不是独立的,如果他是独立的,那么我们将他称之为函数,如若不然,我们将他称之为方法。举个例子,在对象中的函数我们通常称之为方法,因为他始终和这个对象联系在一起,调用也是通过这个对象调用的
看下面例子:
function foo() {console.log(this.name);}foo(); //'',因为window对象上本来就有个name属性,所以打印出来是一个空字符串let obj = {name: "coderwei",foo,};obj.foo(); //coderwei
在执行foo函数的时候,通过obj对象调用的,所以this会动态绑定到obj对象上,这就是一个典型的隐式绑定的例子。所以还是那句话,一定要关注函数调用的位置,他怎么被调用的,this指向的关键在这个位置。
显式绑定
首先我们先看看在js中函数有几种调用方式
function foo() {console.log("函数执行");}foo(); //函数执行foo.call(); //函数执行foo.apply(); //函数执行
我们会发现上面三种调用方式,都会执行函数,那么下面两种方式存在的意义是什么?js发展至今,只要某个语法存在,那么必定有他存在的意义,不然早就被抛弃的。区别在于this指向的问题,下面两种调用方式我们可以手动的绑定一个对象作为this的指向,还有一种bind方法,也是绑定this的,但是他并不会执行这个函数,后续我会单独出一篇文章探讨call、apply和bind方法并且使用js自己实现一下这三个方法。
let obj = {name:'coderwei',}function foo(){console.log(this.name)}foo() // ''foo.call(obj) //coderweifoo.apply(obj) //coderweilet bar = foo.bind(obj) //bind方法只绑定不执行,绑定好后返回一个新的函数,以后这个函数的this//就是bind的第一个参数bar() //coderwei
我们会发现使用call或者是apply的方式调用,输出的结果和直接调用是不用的,他们的this指向的是call函数的第一个参数。当然会有第二个参数,第二个参数也是这两个方法不同的地方。这里我们就不做探讨,一起放到后面专门讲这几个方法的文章中
小提示
如果call、apply、bind绑定了一个null或者是undefined,那么默认会绑定到window,应该是实现这几个函数的时候内部做了边界判断
new绑定
new操作符涉及到面向对象的概念,也是js实现面向对象的基础。首先对于new操作符在内部做了什么,我们就直接上结论,我们主要关注使用new操作符之后,我的this指向谁了?
/*这里我们需要注意一个细节,通常情况下构造函数我们会以大写字母开头,用于和普通的函数做一个区分,因为他们都是用function定义的。果然,不遵守这个规范也无可厚非,从代码运行的层面来说,他并不会影响代码的正常运行,主要是加以区分方便日后的开发者和自己进行维护*/function Person() {}const p1 = new Person();
这里我们需要注意一个细节,通常情况下构造函数我们会以大写字母开头,用于和普通的函数做一个区分,因为他们都是用function定义的。果然,不遵守这个规范也无可厚非,从代码运行的层面来说,他并不会影响代码的正常运行,主要是加以区分方便日后的开发者和自己进行维护
this指向谁了
- 在内存中创建一个对象p1
- 将People的prototype赋值给p1.proto
- 将创建出来的对象p1和构造函数内部的this进行一个绑定
- 处理内部代码
- return 这个对象出去(函数内部没有返回其他对象
new操作符我们主要关注this绑定的问题,在内部会将p1对象的this绑定到函数调用的this上面。
构造函数的this
function Person(name) {this.name = name;}const p1 = new Person("coderwei");console.log(p1.__proto__ === Person.prototype); //true 验证上面第二点console.log(p1.name); //coderwei
内置函数的this
- forEach ```javascript //1. 只传递一个参数 let items = [1, 2, 3, 4, 5]; items.forEach(function (item) { console.log(item, this); // 我们会发现这里的this指向的是window });
//2. 传递两个参数 let items = [1, 2, 3, 4, 5]; items.forEach(function (item) { console.log(item, this); // 我们会发现这里的this指向的是coderwei },’coderwei’);
<br />当然类似的方法也有map、filter等等,需要注意的是这里不能写成箭头函数,因为箭头函数不绑定this的。2.setTimeout```javascriptsetTimeout(function () {console.log(this); // window}, 200);
这个位置的this指向的也是window,但是这里有个特殊的点,看下面代码
"use strict";function foo() {console.log(this);}foo(); //undefined
在严格模式下函数内部的this指向的是undefined。但是放到setTimeout上,事情就变得不一样了。
"use strict";setTimeout(function () {console.log(this); // window}, 200);
诡异吧,这个this指向的还是window,这个点笔者没有办法证明是如何实现的,如果有其他朋友有思路的可以指出。我的猜测是内部处理setTimeout的时候,对这个函数并不是直接执行,而是执行了一个bind方法,这个在翻阅资料的时候看到过一篇文章,在V8的测试文件中关于setTimeout的片段的注释中提到了一嘴bind。
this绑定的优先级
显式绑定vs隐式绑定
首先隐式绑定没有什么好说的,隐式绑定讲道理嘛,他就应该是最低的,他是内部自动绑定的,所以说只要你想改,无论用什么方式,都应该成功的改变this的指向
function foo() {console.log(this);}foo.call("coderwei"); //coderweifoo.apply("coderwei");//coderweiconst bar = foo.bind("coderwei");bar();//coderwei
隐式绑定VS new绑定
let obj = {name: "coderwei",foo: function () {console.log(this);},};obj.foo(); // obj对象new obj.foo(); // foo函数
显式绑定vs new绑定
function foo() {console.log(this);}let bar = foo.bind("coderwei");new bar();// foo函数
这里需要注意一点,call和apply不能被new实例化,所以他们之间无法比较优先级
总结
- new绑定优先级大于显式绑定
- 显式绑定优先级大于隐式绑定
- 隐式绑定优先级大于默认绑定
箭头函数
箭头函数是ES6新增的一种新的编写函数的方式,他会比以往编写函数的方式更加简单。
- 箭头函数是不会绑定this的,他也没有arguments(在函数内部能够拿到的实参列表)
- 箭头函数不能当成构造函数来使用,因为他没有自己的this
因为箭头函数没有this,所以它不适用上面的几钟规则,如果箭头函数内部使用到了this,他就需要去到外层作用域去找this
let obj = {name: "coderwei",foo: function () {console.log(this.name);},};obj.foo(); //coderwei//改造成箭头函数let obj = {name: "coderwei",foo: () => {console.log(this.name);},};obj.foo();//''还是那一点,window下面本来就有个name属性,所以是空字符串。本质上这里指向window
分析几道面试题
第一道
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();// window//coderwei//coderwei//window
这道题总的来说没多大难度,主要关注(person.sayName)()本质上还是通过person调用的,所以他的this还是指向person,(b = person.sayName)()这里他是一个自执行函数,也就是前面讲的独立的函数调用,所以说这里指向了window
第二道
var name = "window";var person = {name: "coderwei",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" };person.foo1();person.foo1.call(person2);person.foo2();person.foo2.call(person2);person.foo3()();person.foo3.call(person2)();person.foo3().call(person2);person.foo4()();person.foo4.call(person2)();person.foo4().call(person2);// coderwei// person2// window// window// window// window// person2// coderwei// person2// coderwei
分析一下每一行的函数调用,
- 在第一次调用的时候,我们直接通过一个对象去调用一个函数,这里会有一个默认绑定,所以结果是coderwei
- 第二次函数调用通过call的方法来调用,所以他会覆盖隐式绑定,以显示绑定为主,所以结果是person2
- 第三次函数调用是调用了一个箭头函数,箭头函数不绑定this,所以他回去外层作用域去找,外层作用域是全局,注意:对象没有作用域,不要把对象那个花括号当成一个作用域。
- 第四次函数调用通过call的方式来调用,但是咱们的箭头函数不绑定this,你给我,我还不要呢,所以他的this依旧去上层作用域找,所以还是window
- 第五次函数调用是返回了一个新的函数,实际上是拿到新的函数直接做一个调用,所以他就是一个独立的函数调用,结果显而易见,依旧是window
- 第六次函数调用是给foo3用call绑定了一个this,然后在执行内部返回的函数。但是我们只是给外部的函数绑定了一个this,关内部函数什么事情,本质上还是一个独立的函数调用,所以返回的还是一个window
- 第七次函数调用是先执行foo3函数,拿到返回的函数之后给这个函数绑定了this,所以结果是person2
- 第八次函数调用式是先调用foo4函数,然后拿到返回的函数后继续调用。但是需要注意的是返回的函数是一个箭头函数,所以他没有自己的this,他要去外层作用域找,他是被一个函数包裹的,函数是有作用域的,也就是说他拿到的是外层的函数的this,也就是我们前面进行隐式绑定的person2
- 第九次函数调用跟第八次一样,都是要去外层寻找this,不过这一次我们给外层的函数指定了this,是person2,所以他拿到的就是person2
- 第十次函数调用是拿到返回的箭头函数之后给这个箭头函数绑定this,箭头函数没有自己的this,也不能绑定this,所以说他依旧是去外层找,外层函数的this隐式绑定的person,也就是coderwei
所以说箭头函数有点像一个二百五,我不管,我就是没有,你给我我也不要,我就要去上层作用域要,他是什么我就拿什么作为我的this。
