1、理解this
1.1为什么要使用this
- 在方法中,为了能够获取到name名称,必须通过obj的引用(变量名称)来获取。
- 但是这样做有一个很大的弊端:如果我将obj的名称换成了info,那么所有的方法中的obj都需要换成info。
var obj = {
name: 'why',
running: function () {
// 这个this指向的obj
//console.log(obj.name + "running")
console.log(this.name + "running")
},
eating: function () {
console.log(this.name + "eating")
}
}
obj.running()
1.2 this的指向
- 在全局作用域下,我们可以认为this就是指向的window
- 但是,开发中很少直接在全局作用域下去使用this,通常都是在函数中使用。
- 所有的函数在被调用时,都会创建一个执行上下文:
- 这个上下文中记录着函数的调用栈、函数的调用方式、传入的参数信息等;
- this也是其中的一个属性;
// this在全局作用域下指向的是window
console.log(this) //window
var name = 'zz'
// 一般不在全局作用域下使用this,都在函数中使用
console.log(this.name) //zz
function foo() {
console.log(this)
}
// 调用方式一
foo() //window
// 调用方式二
// 将foo放入一个对象中调用
var obj = {
name: 'why',
foo: foo
}
obj.foo() //obj对象
// 调用方式三
// 通过call/apply来调用
foo.call("abc") //string {"abc"}对象
- 函数在调用时,JavaScript会默认给this绑定一个值;
- this的绑定和定义的位置(编写的位置)没有关系;
- this的绑定和调用方式以及调用的位置有关系;
- this是在运行时被绑定的;
2、this的绑定规则
2.1. 默认绑定
什么情况下使用默认绑定呢?独立函数调用。
独立的函数调用我们可以理解成函数没有被绑定到某个对象上进行调用;
案例一:普通函数调用
- 该函数直接被调用,并没有进行任何的对象关联;
- 这种独立的函数调用会使用默认绑定,通常默认绑定时,函数中的this指向全局对象(window);
// 1.普通函数调用
function foo() {
console.log(this) //window
}
foo()
案例二:函数调用链(一个函数又调用另外一个函数)
- 所有的函数调用都没有被绑定到某个对象上;
// 2.案例二:
function test1() {
console.log(this); // window
test2();
}
function test2() {
console.log(this); // window
}
test1();
案例三:将函数作为参数,传入到另一个函数中
function foo(func) {
func()
}
function bar() {
console.log(this); // window
}
foo(bar);
- 这里的结果依然是window,为什么呢?
- 原因非常简单,在真正函数调用的位置,并没有进行任何的对象绑定,只是一个独立函数的调用;
function foo(func) {
func()
}
var obj = {
name: "why",
bar: function() {
console.log(this); // window
}
}
foo(obj.bar);
2.2 隐式绑定
案例一:通过对象调用函数
- foo的调用位置是obj.foo()方式进行调用的
- 那么foo调用时this会隐式的被绑定到obj对象上
function foo() {
console.log(this)
}
var obj = {
name: "why",
foo: foo
}
obj.foo()
- 通过obj2又引用了obj对象,再通过obj对象调用foo函数;
- 那么foo调用的位置上其实还是obj被绑定了this;
var obj2 = {
name: "why1",
obj: obj
}
obj2.obj.foo() //obj
- 结果最为什么是window呢?
- 因为foo最终被调用的位置是bar,而bar在进行调用时没有绑定任何的对象,也就没有形成隐式绑定
- 相当于是一种默认绑定;
// 隐式丢失
var bar = obj.foo
bar() //window
2.3显示绑定
2.3.1 call、apply
- 通过call或者apply绑定this对象
- 显示绑定后,this就会明确的指向绑定的对象
- 第一个参数是相同的,后面的参数,apply为数组,call为参数列表;
// 通过call或者apply绑定this对象
// 显示绑定后,this就会明确的指向绑定的对象
function foo(num1, num2) {
console.log(this)
console.log(num1 + num2)
}
foo.call(window)
foo.call(123)
foo.call({
name: "why"
}, 1, 5)
foo.apply({
name: "why"
}, [1, 3])
2.3.2 bind函数
-
bind函数与apply和call的区别
- bind函数 返回的是一个新函数,需要去调用
- apply和call是立即去调用
使用Function.prototype.bind
function foo1() {
console.log(this)
}
var obj = {
name: 'za'
}
//返回的是一个新函数,要调用
var bar = foo1.bind(obj)
bar()
手动写了一个bind的辅助函数(以下foo1,obj传参直接使用上面)
function bind(func, obj) {
return function () {
return func.apply(obj, arguments)
}
}
var bind1 = bind(foo1, obj)
bind1()
2.3.3 内置函数
案例一:setTimeout函数
- setTimeout中会传入一个函数,这个函数中的this通常是window
setTimeout(function () {
console.log(this) //window
}, 1000)
- 这个和setTimeout源码的内部调用有关;
- setTimeout内部是通过apply进行绑定的this对象,并且绑定的是全局对象;
案例二:forEach函数 - 在forEach中传入的函数打印的也是Window对象;
- 这是因为默认情况下传入的函数是自动调用函数(默认绑定)
var names = ["a", "b", "c"]
names.forEach(function (item) {
console.log(this) //window
})
如何改变这里的this指向
- 这时候可以用forEach的第二个参数
/* 那么我们可以改变this的指向吗
这时候可以用forEach的第二个参数
*/
var obj = {
name: "why"
}
names.forEach(function (item) {
console.log(this) //obj
}, obj)
案例三:div点击事件
<style>
#box {
width: 200px;
height: 200px;
background-color: red;
}
</style>
<div id="box"></div>
- 在点击事件的回调中,this指向谁呢?box对象;
- 这是因为在发生点击时,执行传入的回调函数被调用时,会将box对象绑定到该函数中;
var btn = document.getElementById("box")
box.onclick = function () {
console.log(this) //box对象
}
2.4new关键字
使用new关键字创建一个构造函数,这里的this指向的是Person对象
function Person(name) {
console.log(this)//Person
this.name = name //Person{name:"张三"}
}
var person = new Person("张三")
console.log(person)
2.5规则优先级
new关键字>显示绑定(bind)>隐式绑定>默认
1.默认规则的优先级最低
2.显示绑定的优先级高于隐式绑定的优先级
- 隐式绑定,指向的是obj
- 先经过隐式绑定再经过显示绑定,this的指向是obj1
function foo() {
console.log(this)
}
var obj = {
name: "张三",
foo: foo
}
var obj1 = {
name: "李四",
foo: foo
}
// 隐式绑定
obj.foo() //obj
// 先隐式绑定 再显示绑定
obj.foo.call(obj1) //obj1
3.new绑定优先级高于隐式绑定
- 这里的this经过new后绑定的是foo对象
function foo() {
console.log(this)
}
var obj = {
name: "张三",
foo: foo
}
new obj.foo()//指向的是foo对象
3.new绑定优先级高于显示绑定
- new不可以和call、apply共同使用
function foo() {
console.log(this)
}
var obj = {
name: "张三",
foo: foo
}
new foo.call(obj)
- new的优先级高于bind函数
function foo() {
console.log(this)
}
var obj = {
name: "张三",
foo: foo
}
var obj = foo.bind(obj)
new obj//foo
3、this规则之外
3.1忽略显示绑定
- 如果显示绑定,我们传入的是一个null或者undefined,那么这个显示绑定会被忽略
function foo() {
console.log(this)
}
var obj = {
name: "zs",
foo: foo
}
foo.call(obj) //obj
foo.call(null) //window
foo.call(undefined) //window
var bar = foo.bind(null)
bar()//window
3.2函数间接调用
- obj2.foo=obj1.foo(此时这个this是指向obj1对象的)
- 但是它又自己再次调用,那么这时候就是默认绑定,this指向的是window
function foo() {
console.log(this);
}
var obj1 = {
name: "obj1",
foo: foo
};
var obj2 = {
name: "obj2"
}
obj1.foo(); // obj1对象
(obj2.foo = obj1.foo)(); // win
3.3ES6箭头函数
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 = ["2", "2", "3"]
_this.data.push(...res)
}, 100)
}
}
obj.getData()
2.使用箭头函数
- 为什么在setTimeout的回调函数中可以直接使用this呢?
- 因为箭头函数并不绑定this对象,那么this引用就会从上层作用域中找到对应的this
var obj1 = {
data: [],
getData: function () {
setTimeout(() => {
console.log(this) //指向obj
var res = ["2", "2", "3"]
this.data.push(...res)
}, 100)
console.log(this.data)
}
}
obj1.getData()
3.如果getData也是一个箭头函数,那么setTimeout中的回调函数中的this指向谁呢?
- 答案是window;
- 依然是不断的从上层作用域找,那么找到了全局作用域;
- 在全局作用域内,this代表的就是window;
var obj2 = {
data: [],
getData: () => {
setTimeout(() => {
console.log(this) //指向的是window
}, 100)
}
}
obj2.getData()
4、面试题this
4.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
}
4.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
4.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.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
本文章参考内容:https://mp.weixin.qq.com/s/hYm0JgBI25grNG_2sCRlTA