为什么需要 this ?
在人类语言中,几乎都有代词。代词用来指代什么东西,并且需要结合语境才能知道代词具体指代的是什么。没有代词可以吗?当然可以。没有代词只是会让语言更加繁琐罢了。
this 就是编程语言中的代词,没有 this 可以,而有了它会使代码编写更加便捷高效。
常见的编程语言几乎都有 this 这个关键字(Objective-C 中使用的是 self),但是 JavaScript 中的 this 和常见的面向对象语言中的 this 不太一样
常见面向对象的编程语言中,比如 Java、C++、Swift、Dart 等等一系列语言中,this 通常只会出现在类的方法中,也就是你需要有一个类,类中的方法(特别是实例方法)中,this 代表的是当前调用对象。
this 指向什么?
全局作用域
- 浏览器环境:
Window
- node 环境:
{}
一个空对象
很少在全局作用域中使用 this,通常是在函数中使用。函数执行会在调用栈中生成函数执行上下文,其中就有一条 this 的记录。
函数中 this 绑定的特点:
- 函数在调用时,JavaScript 会默认给 this 绑定一个值;
- this 的绑定和定义的位置(编写的位置)没有关系;
- this 的绑定和调用方式以及调用的位置有关系;
- this 是在运行时被绑定的;
this 的具体绑定规则:
- 默认绑定;
- 隐式绑定;
- 显示绑定;
- new 绑定;
默认绑定
什么情况下使用默认绑定呢?独立函数调用。
独立的函数调用我们可以理解成函数没有被绑定到某个对象上进行调用。
常见的独立调用案例:
function foo() { console.log(this); };
foo(); // Window
function foo() {
console.log(this);
};
function foo1() {
console.log(this);
foo(); // 独立调用
};
function foo2() {
console.log(this);
foo1(); // 独立调用
};
foo2(); // 调用
// 打印三个 Window
下面三种案例都是一样的,虽然定义在了对象里,但是改了堆内存中函数对象的引用,执行的时候还是作独立函数执行的。
let obj = {
name: 'zs',
foo: function() {
console.log(this);
}
};
let fn = obj.foo; // 没有()调用符,只改了堆内存函数对象的引用
// 虽然函数定义在对象里,但是this指向和定义位置无关,只和调用位置有关,所以调用还是独立调用
fn(); // Window
function foo() {
console.log(this);
};
let obj = {
name: 'zs',
foo: foo,
};
let fn = obj.foo; // 没有()调用符,只改了堆内存函数对象的引用
// 一样的,换汤不换药,只和调用位置有关
fn(); // Window
function foo() {
return function() {
console.log(this);
};
};
let fn = foo();
fn(); // Window
隐式绑定
隐式调用也就是它的调用位置中,是通过某个对象发起的函数调用。
为什么叫隐式?因为这个绑定过程是 js 引擎自己完成的,我们看不见具体绑了谁。
隐式绑定有一个前提条件:
- 必须在调用的对象内部有一个对函数的引用(比如一个属性);
- 如果没有这样的引用,在进行调用时,会报找不到该函数的错误;
- 正是通过这个引用,间接的将 this 绑定到了这个对象上;
隐式绑定的案例:
function fn() {
console.log(this);
};
let obj = {
name: 'zs',
foo: fn,
};
obj.foo(); // {name: 'zs', foo: ƒ}
let obj = {
name: 'zs',
foo: function() {
console.log(this.name + '123');
},
};
let fn = obj.foo;
fn(); // 123,还是独立调用,this 指向的 Window 没有 name 属性,所以打印为空
let obj1 = {
name: 'obj1',
foo: function() {
console.log(this);
},
};
let obj2 = {
name: 'obj2',
foo: obj1.foo,
};
obj2.foo(); // {name: 'obj2', foo: ƒ},虽然函数定义在obj1,但是调用还是obj2
显示绑定
如果我们不希望在对象内部包含这个函数的引用,同时又希望在这个对象上进行强制调用,该怎么做呢?
JavaScript 所有的函数都可以使用 call 和 apply 方法(这个和Prototype有关)来调用函数。
call() / apply()
call()
和apply()
都可以调用函数,那和函数直接调用有什么区别?
最大的区别就是**call()**
和**apply()**
能明确地指定 this 绑定的对象。因此这也能让不相干的对象和函数绑定到一起产生联系,而不用在对象内部包含一个函数的引用属性。
function foo() {
console.log(this);
};
// 都可以调用函数
foo(); // Window
foo.call(); // Window
foo.apply(); // Window
// call(),apply() 指定 this
foo.call('aaa'); // String {'aaa'}
foo.apply('bbb'); // String {'bbb'}
function foo() {
console.log(this);
};
let obj = {
name: 'zs',
};
foo(); // Window
foo.call(obj); // {name: 'zs'}
foo.apply(obj); // {name: 'zs'}
**call()**
和**apply()**
有啥区别?
当调用的函数有参数时,call()
参数直接写,apply()
需要将参数用数组包裹后传入。
function sum(num1, num2) {
console.log(num1 + num2 + this.name);
};
let obj = { name: 'zs' };
sum.call(obj, 1, 2); //3zs
sum.apply(obj, [2, 3]); //5zs
bind()
还有一个函数**bind()**
也能显示绑定 this 指向,但是它不调用函数,而是返回一个修改好 this 指向的新函数。call()\apply()
修改 this 后调用都是一次性的,所以当需要函数修改 this 指向后并需要多次执行,就可以使用bind()
返回的新函数。
function foo() {
console.log(this);
};
let newFoo = foo.bind('aaa');
newFoo(); // String {'aaa'}
new 绑定
new 一个构造函数的时候,会创建一个空对象,构造函数中的 this 就会指向这个空对象。
function Person(name, age) {
console.log(this);
this.name = name; // this 会指向一个空对象,这就是在给空对象添加属性
this.age = age;
console.log(this);
};
let person1 = new Person('zs', 18); // Person {name: 'zs', age: 18}
let person2 = new Person('ls', 19); // Person {name: 'ls', age: 19}
内置函数的 this 指向问题
有些时候,我们会调用一些 JavaScript 的内置函数,或者一些第三方库中的内置函数。这些内置函数一般是高阶函数,会要求我们传入另外一个函数。
我们自己并不会显示的调用这些函数,而是 JavaScript 内部或者第三方库内部会帮助我们执行传入的参数函数;这些参数函数中的 this 又是如何绑定的呢?
setTimeout() \ setInterval()
中参数函数为独立调用,所以指向 WindowsetTimeout(function() {
console.log(this); // Window
}, 1000);
数组中的
forEach() \ map() \ filter() \ find()
也是独立调用,this 指向 Window,但是也可以接收参数指定 this 的指向。 ```javascript let arr = [1, 4, 2, 3];
// 默认独立调用 arr.forEach(function(item) { console.log(item + this); // Window }); // 参数中指定 this 的指向 arr.forEach(function(item) { console.log(item + this); // String {‘aaa’} }, ‘aaa’);
- dom 操作操作,如:监听点击事件,都是指向了绑定事件的 dom 元素对象。
`dom.onclick = f()`绑定相当于把回调函数添加为 dom 元素对象的一个属性,所以点击执行回调函数这相当于一个隐式调用,this 指向该 dom 元素对象。
`dom.addEventListener('click', f())`通过事件监听器添加点击事件,这种方式能添加多个点击事件给元素,而不发生同类事件回调函数覆盖。这些回调函数会被放入一个数组中,触发事件时,就会遍历出这些回调函数进行调用。调用是通过`call()`函数调用,同时在`call()`函数中指定了 this 指向该 dom 元素。
```html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<style>
div {
height: 100px;
width: 100px;
background-color: aqua;
}
</style>
<body>
<div class="box"></div>
</body>
<script>
const boxDiv = document.querySelector('.box')
boxDiv.onclick = function () {
console.log(this) // <div class="box"></div>
};
boxDiv.addEventListener('click', function () {
// fn.call('boxDiv')
console.log(this) // <div class="box"></div>
});
boxDiv.addEventListener('click', function () {
console.log(this) // <div class="box"></div>
});
// mouseover 事件
boxDiv.addEventListener('hover', function () {
console.log(this) // <div class="box"></div>
});
</script>
</html>
规则的优先级
如果一个函数调用位置应用了多条规则,this 的绑定规则就有了优先级。
优先级这个东西我觉得体现了一种设计哲学。定制高于通用,手动高于自动,小范围高于大范围。代码里也一样。
默认绑定优先级最低,因为是默认的。
显示绑定也高于隐式绑定,因为显示绑定需要手动明确的指出 this 的指向,而隐式绑定只是因为是在对象中,自动绑定了调用对象。这两条规则都很符合优先级的设计哲学。
// 显示绑定高于隐式绑定
let obj = {
name: 'obj',
foo: function foo() {
console.log(this);
}
};
obj.foo(); // 隐式调用 {name: 'obj', foo: ƒ}
obj.foo.call('aaa'); // 隐式调用了再显示调用 String {'aaa'}
obj.foo.apply('aaa'); // String {'aaa'}
// bind() 有点特殊,不能这样比,因为它是返回一个函数,压根没有进行隐式调用,所以比不出来
obj.foo.bind('aaa');
function foo() {
console.log(this);
};
// 可以在对象中就进行显示绑定
let obj1 = {
name: 'obj1',
foo: foo.bind('aaa')
};
obj1.foo(); // String {'aaa'}
new 绑定规则优先级是最高的。注意:new绑定和call、apply是不允许同时使用的,所以不存在谁的优先级更高。说 new 比显示绑定优先级高,是因为new绑定和bind一起使用,new绑定优先级更高。
// new 高于隐式绑定
let obj = {
name: 'obj',
foo: function() {
console.log(this);
},
};
// 既有隐式调用又有new 调用
// 如果隐式调用优先级高,则打印 obj 对象
// 如果new 绑定优先级更高,则会打印名为 foo 的空对象
new obj.foo(); // foo {}
// new 优先级高于 bind
function foo() {
console.log(this);
};
// 显示绑定
let fn = foo.bind('aaa');
// new 绑定
new fn(); // foo {}
总结一下:
- 默认规则的优先级最低
- 显示绑定优先级高于隐式绑定
- new绑定优先级高于隐式绑定
- new绑定优先级高于bind
new绑定 > 显示绑定(apply/call/bind) > 隐式绑定(obj.foo()) > 默认绑定(独立函数调用)
特殊的绑定
忽略显示绑定
如果在显示绑定中,我们传入一个null
或者undefined
,那么这个显示绑定会被忽略,使用默认规则:
function foo() {
console.log(this);
};
foo(); // 独立函数默认绑定 Window
foo.call('aaa'); // 显示绑定 aaa 字符串对象
foo.call(null); // 忽略显示绑定,this 指向 Window
foo.call(undefined); // Window
let fn = foo.bind(null);
fn(); // Window
间接函数引用
另外一种情况,创建一个函数的 间接引用,这种情况使用默认绑定规则。例如:赋值(obj2.foo = obj1.foo)
的结果是foo函数;foo函数被直接调用,那么是默认绑定;
function foo() {
console.log(this);
};
let obj = {
name: 'obj',
foo: foo,
};
obj.foo(); // 隐式调用:{name: 'obj', foo: ƒ}
let obj1 = {
name: 'obj1'
};
// 将 foo 的引用复制给 obj1.foo,并直接调用
(obj1.foo = obj.foo)(); // this 指向 Window
// 这种不算间接引用,因为都赋值完成了,属于正常调用
obj1.foo(); // {name: 'obj1', foo: ƒ}
插播:js 没有分号结尾的危害
function foo() {
console.log('炖鸡爪');
};
let obj = {} // 没有分号结尾
(obj.foo = foo)(); // 报错,无法执行
let obj = {}
[1,2].forEach(); // 报错,无法执行
类似上面这种情况,前面代码没有以分号结尾,但后面的代码有符号包裹的时候。引擎词法分析就无法识别出前面的代码已经结束,会认为它们是一行代码。实际上执行的代码是:
let obj = {}(obj.foo = foo)();
let obj = {}[1,2].forEach();
这样的代码不符合规范肯定执行不了,解决这种问题很简单,只要人为加上分号,明确告知引擎这行代码已经结束就可以了。
箭头函数中的 this 指向问题
箭头函数不使用 this 的四种标准规则(也就是不绑定this),而是根据外层作用域来决定this。
首先得明白作用域范围?
- 由调用栈入栈的执行上下文可知,作用域范围只有全局和函数内部。(es6 有了作用域块的概念)
作用域有一个很容易弄错的点,那就是误以为字面量对象花括号包起来的范围也是一个新作用域,其实并没有,对象的作用域就是全局。
var obj = {
name: 'a',
foo: function() {
// 上层作用域是全局
}
};
function Foo() {
this.foo = function() {
// 上层作用域是 Foo
};
};
这个误区经常导致我们对箭头函数中 this 指向判断错误:
var name = 'GO'
var obj = {
name: 'obj',
foo: () => {
console.log(this.name)
},
fo: function() {
return () => console.log(this.name)
},
fn: function() {
console.log(this.name)
}
}
// 很容易误以为箭头函数打印的是foo函数的上层,obj 对象,然而其实是 Window
// 因为 obj 对象就是全局的,本身没有obj对象作用域的概念
obj.foo(); // GO
// 这个箭头函数上包了一层函数,函数作用域是真正存在的作用域,所以它拿到了fo 函数中的 this
obj.fo()(); // obj
obj.fn(); // obj,普通的隐式调用
// 这里箭头函数的外层作用域是 Window
var foo = () => { console.log(this); };
var obj = { foo: foo };
// 独立调用
foo(); // Window
// 隐式调用
obj.foo(); // Window
// 显示调用
foo.call('aaa'); // Window
// new 调用
new foo(); // 执行错误:foo is not a constructor
箭头函数中的 this 指向外层作用域这一点提供了便捷,尤其在对象中的函数使用外部 API 函数时,API 函数需要访问对象中其他参数的时候。
本来对象中的函数访问对象中其他属性,函数中的 this 指向对象,正常访问属性没问题。但是函数中使用的外部 API 函数想要访问对象中其他属性,此时外部API的 this 并不指向该对象,所以拿不到对象中的其他属性。
网络请求交互很好的体现了这个场景。
let result = {
data: null,
getData: function() {
// 将指向对象的this赋值给一个变量,以变量的方式传入第三方函数,从而保证获取到该对象的引用
let _this = this;
console.log(_this);
setTimeout(function() { // 以定时器代替 axios 这些第三方函数(反正只要不指向该对象就行)
_this.data = 1;
},1000);
},
};
result.getData();
let obj = {
data: null,
getData: function() {
setTimeout(() => {
this.data = 1; // 箭头函数中 this 本来就指向上一层作用域,所以不用手动保存引用了
}, 1000);
}
}
obj.getData();
自测题
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: 赋值表达式(独立函数调用)
// }
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.foo1(); // person1(隐式绑定)
// person1.foo1.call(person2); // person2(显示绑定优先级大于隐式绑定)
// person1.foo2(); // window(不绑定作用域,上层作用域是全局)
// person1.foo2.call(person2); // window
// person1.foo3()(); // window(独立函数调用)
// person1.foo3.call(person2)(); // window(独立函数调用)
// person1.foo3().call(person2); // person2(最终调用返回函数式, 使用的是显示绑定)
// person1.foo4()(); // person1(箭头函数不绑定this, 上层作用域this是person1)
// person1.foo4.call(person2)(); // person2(上层作用域被显示的绑定了一个person2)
// person1.foo4().call(person2); // person1(上层找到person1)
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() // person1
// person1.foo1.call(person2) // person2(显示高于隐式绑定)
// person1.foo2() // person1 (上层作用域中的this是person1)
// person1.foo2.call(person2) // person1 (上层作用域中的this是person1)
// person1.foo3()() // window(独立函数调用)
// person1.foo3.call(person2)() // window
// person1.foo3().call(person2) // person2
// person1.foo4()() // person1
// person1.foo4.call(person2)() // person2
// person1.foo4().call(person2) // person1
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)
// person1.obj.foo1()() // window
// person1.obj.foo1.call(person2)() // window
// person1.obj.foo1().call(person2) // person2
// person1.obj.foo2()() // obj
// person1.obj.foo2.call(person2)() // person2
// person1.obj.foo2().call(person2) // obj