为什么需要this
this 关键字并不是 JavaScript 语言独有的,在很多常见的编程语言中你几乎都可以看到这个身影。例如:C++、Java等。
为什么需要有 this 这一个关键字呢?或是说 this 的由来?
JavaScript 允许在函数体中,引用当前环境或是外部环境的其他变量。
var name = 'mjy'
var foo = function() {
console.log(name, this)
}
foo()
当我们想要指定 foo 函数中 name 只在自身函数体的作用域中去寻找时。我们就需要搞清楚函数体中的运行环境了,或是给 foo 函数指定运行环境。
var name = 'mjy'
var foo = function() {
console.log(name, this)
}
foo.call(foo) // 通过显示绑定为函数绑定当前环境为foo的函数体
this指向什么
全局作用域中this指向
我们知道在全局中直接输出 this 。this 指向的是 window。(浏览器中全局 this 指向 window,node环境中,全局 this 指向空对象 { })
console.log(this) // window
函数中this指向
我们先来看下面这个例子。定义一个函数,采用不同的方式来对它进行调用:
var name = "window_name"
function foo() {
var name = "mjy"
console.log(this.name, this)
}
// 对象obj中foo属性指向foo函数
var obj = {
name: "mjy",
foo: foo
}
// 直接调用
foo() // window_name Window对象
// 在对象中将其调用
obj.foo() // mjy obj对象
// 通过call显示绑定进行调用
foo.call("abc") // undefined String {'abc'}对象
通过这个案例我们可以看出。js 中 this 的指向是十分灵活的,但无非绑定的都是一个对象。万物有道,我们只要了解了其中 this 绑定的规则,那么肯定能将其指向给掌握。
ES6箭头函数中this指向
箭头函数(arrow)是 ES6 中新增的一个函数类型。
这里我们不会介绍箭头函数的语法和用法。我们直接来看例子。
var name = "window_name"
// 直接调用
var foo = () => {
console.log(this.name, this)
}
foo() // window_name window对象
// 在对象中将其调用
var obj = {
name: "mjy",
foo: foo
}
obj.foo() // window
// 通过call显示绑定进行调用
foo.call("abc") // window_name window对象
// 定时器中
setTimeout(() => {
console.log(this.name, this) // window_name window对象
}, 1000)
var obj = {
name: "mjy",
getData: function() {
setTimeout(() => {
console.log(this.name, name)
}, 1000);
}
}
obj.getData() // mjy obj对象
var obj = {
name: "mjy",
getData: function() {
setTimeout(() => {
console.log(this.name, name)
}, 1000);
}
}
obj.getData() // window_name window对象
通过上面的例子我们可以看出,箭头函数并没有自己的this指向,也就是箭头函数不绑定 this。而是根据外层作用域来决定 this 的指向。
通过以上三个案例,我们可以总结出这样一个启示:
- 函数在调用时,JavaScript 会默认给this绑定一个值;
- this 的绑定和定义的位置没有关系,或是说和书写的位置没有关系;
- this 的绑定和调用的方式以及调用的位置有关系;
- this 是在运行时被绑定的;
- 箭头函数不绑定 this,由外层作用域决定 this 指向;
this绑定规则
在上面我们说到:万物有道,我们只要了解了其中 this 绑定的规则,那么肯定能将其指向给掌握。
接下来我们就来了解 this 的绑定规则。
默认绑定
什么情况下使用默认绑定呢?当函数作为独立函数调用时。
独立的函数调用我们可以理解成函数没有被绑定到某个对象上进行调用。
例如:
// 普通函数调用
function foo() {
console.log(this); // window
}
foo();
// 函数链调用
function test1() {
console.log(this); // window
test2();
}
function test2() {
console.log(this); // window
test3()
}
function test3() {
console.log(this); // window
}
test1()
// 函数当作参数时,其实这样也是独立在调用
function foo(func) {
func() // 独立调用
}
var obj = {
name: "mjy",
bar: function() {
console.log(this); // window
}
}
foo(obj.bar);
隐式绑定
隐式绑定的调用方式是通过某个对象进行调用的:也就是它的调用位置中,是通过某个对象发起的函数调用。
例如:通过obj对象进行调用,就相当于隐式绑定了obj对象。
function foo() {
console.log(this); // obj对象
}
var obj = {
name: "mjy",
foo: foo
}
obj.foo(); // obj对象
显示绑定
显示绑定通过 call 和 apply 和 bind 三个方法,通过传递绑定对象的形式,对函数的 this 进行绑定。
call 和 apply 函数:
call 和 apply 的使用方法很简单:
JavaScript所有的函数都可以使用call和apply方法,第一个参数是相同的,后面的参数,apply为数组,call为参数列表。
在调用这个函数时,会将this绑定到这个传入的对象上
例如:
function sum(num1, num2) {
console.log(num1 + num2, this)
}
sum.call("aaa", 10, 20) // 30 String {'aaa'}
sum.apply("aaa", [10, 30]) // 40 String {'aaa'}
bind 函数
如果我们希望一个函数总是显示的绑定到一个对象上,就可以使用bind方法
function sum(num1, num2) {
console.log(num1 + num2, this)
}
var fn = sum.bind("aaa")
fn(10, 10) // 20 String {'aaa'}
fn(10, 20) // 30 String {'aaa'}
fn(10, 30) // 40 String {'aaa'}
new绑定
JavaScript 中的函数可以当做一个类的构造函数来使用,也就是使用new关键字。
使用 new 关键字来调用函数时,其实就相当于做了下面这个操作:
创建一个全新的对象,这个新对象会绑定到函数调用的this上(this的绑定在这个步骤完成)。
function Person(name) {
console.log(this) // this指向被构建出来的 p
this.name = name // p中name在构建时被赋值”mjy“
}
var p = new Person("mjy")
console.log(p) // 由 Person 构建出来的 p对象
规则优先级
- new绑定 > 显示绑定(bind > call 与 apply)> 隐式绑定 > 默认绑定
面试题
题1:
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
}
题目2:
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
题3:
var name = 'window'
function Person (name) {
this.name = name
this.foo1 = function () {
console.log(this.name)
},
this.foo2 = () => console.log(this.name),
this.foo3 = function () {
return function () {
console.log(this.name)
}
},
this.foo4 = function () {
return () => {
console.log(this.name)
}
}
}
var person1 = new Person('person1')
var person2 = new Person('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.foo1() // peron1
// 显示绑定优先级大于隐式绑定
person1.foo1.call(person2) // person2
// foo是一个箭头函数,会找上层作用域中的this,那么就是person1
person1.foo2() // person1
// foo是一个箭头函数,使用call调用不会影响this的绑定,和上面一样向上层查找
person1.foo2.call(person2) // person1
// 调用位置是全局直接调用,所以依然是window(默认绑定)
person1.foo3()() // window
// 最终还是拿到了foo3返回的函数,在全局直接调用(默认绑定)
person1.foo3.call(person2)() // window
// 拿到foo3返回的函数后,通过call绑定到person2中进行了调用
person1.foo3().call(person2) // person2
// foo4返回了箭头函数,和自身绑定没有关系,上层找到person1
person1.foo4()() // person1
// foo4调用时绑定了person2,返回的函数是箭头函数,调用时,找到了上层绑定的person2
person1.foo4.call(person2)() // person2
// foo4调用返回的箭头函数,和call调用没有关系,找到上层的person1
person1.foo4().call(person2) // person1
题4:
var name = 'window'
function Person (name) {
this.name = name
this.obj = {
name: 'obj',
foo1: function () {
return function () {
console.log(this.name)
}
},
foo2: function () {
return () => {
console.log(this.name)
}
}
}
}
var person1 = new Person('person1')
var person2 = new Person('person2')
person1.obj.foo1()()
person1.obj.foo1.call(person2)()
person1.obj.foo1().call(person2)
person1.obj.foo2()()
person1.obj.foo2.call(person2)()
person1.obj.foo2().call(person2)
// obj.foo1()返回一个函数
// 这个函数在全局作用于下直接执行(默认绑定)
person1.obj.foo1()() // window
// 最终还是拿到一个返回的函数(虽然多了一步call的绑定)
// 这个函数在全局作用于下直接执行(默认绑定)
person1.obj.foo1.call(person2)() // window
person1.obj.foo1().call(person2) // person2
// 拿到foo2()的返回值,是一个箭头函数
// 箭头函数在执行时找上层作用域下的this,就是obj
person1.obj.foo2()() // obj
// foo2()的返回值,依然是箭头函数,但是在执行foo2时绑定了person2
// 箭头函数在执行时找上层作用域下的this,找到的是person2
person1.obj.foo2.call(person2)() // person2
// foo2()的返回值,依然是箭头函数
// 箭头函数通过call调用是不会绑定this,所以找上层作用域下的this是obj
person1.obj.foo2().call(person2) // obj
// window
// 最终还是拿到一个返回的函数(虽然多了一步call的绑定)
// 这个函数在全局作用于下直接执行(默认绑定)
person1.obj.foo1.call(person2)() // window
person1.obj.foo1().call(person2) // person2
// 拿到foo2()的返回值,是一个箭头函数
// 箭头函数在执行时找上层作用域下的this,就是obj
person1.obj.foo2()() // obj
// foo2()的返回值,依然是箭头函数,但是在执行foo2时绑定了person2
// 箭头函数在执行时找上层作用域下的this,找到的是person2
person1.obj.foo2.call(person2)() // person2
// foo2()的返回值,依然是箭头函数
// 箭头函数通过call调用是不会绑定this,所以找上层作用域下的this是obj
person1.obj.foo2().call(person2) // obj
总结
this是 动态 绑定的,在运行时绑定的。(或是说在调用时执行时才会进行绑定)所以this的绑定是依赖着当时的调用方式的。
声明
- 本文属于读书笔记一类,是作者在学习 《深入JavaScript高级语法》 coderwhy老师课程途中,以视频中内容为蓝本,辅以个人微末的道行“填写”完成,推荐购买原课程观看,定有收获
- 欢迎大佬斧正