Javascript 的 this 理解
Javascript 的this
是让很多码农(包括自己)觉得复杂的机制,要弄清楚它,就需要弄清楚几个问题:
- 为什么要用
this
? this
到底是什么?this
的绑定规则是什么?
一、为什么要用this
?
有如下代码:
function showName(){
return this.name.toUpperCase();
}
function sayHello(){
var greeting = "Hello,I'm " + showName.call(this);
console.log(greeting);
}
var xm ={
name:"xiaoming"
}
var hh={
name:"huahua"
}
console.log(showName.call(xm)); // XIAOMING
console.log(showName.call(hh)); // HUAHUA
sayHello.call(xm); // Hello,I'm XIAOMING
sayHello.call(hh); // Hello,I'm HUAHUA
这段代码可以在不同的上下文对象xm
和hh
(还可以有无数个)中复用函数showName()
和sayHello()
,如果不用this
,那就需要给showName()
和sayHello()
显示传入上下文对象:
function showName(context){
return context.name.toUpperCase();
}
function sayHello(context){
var greeting = "Hello,I'm " + showName.call(context);
console.log(greeting);
}
console.log(showName.call(xm));
sayHello.call(hh);
随着调用关系越来越复杂,显示传递上下文对象会让代码变得越来越混乱和难以维护,使用this
则能有效避免这个问题。
二、this
到底是什么?
首先说结论,不是指向函数自身,也不是函数作用域,而是运行时绑定的。它的上下文取决于函数调用的各种条件,所以this
的绑定只取决于函数的调用方式;当一个函数被调用时,会创建一个运行时上下文:包含函数在那里被调用(调用栈)、函数的调用方式、传入的参数等信息,而this
就是这个上下文的一个属性,会在函数执行的过程中用到。
function foo(num){
console.log('foo被调用了:',num);
this.count++;
}
foo.count=0;
for(var i=0;i<10;i++){
if(i>5){
foo(i)
}
}
// foo被调用了: 6
// foo被调用了: 7
// foo被调用了: 8
// foo被调用了: 9
// 输出foo总共被调用了多少次
console.log(foo.count); // 0 呐尼?
上面的代码输出证明foo
被调用了4次,但是foo.count
仍然为0,执行foo.count=0
的确向函数对象foo
添加了一个属性count
,但是函数内部的this并不是指向那个函数对象,所以this.count++
不会导致foo.count
自增,换个写法(代码只列出了变化的部分):
function foo(num){
console.log('foo被调用了:',num);
foo.count++;
}
...
运行一下试试,console.log(foo.count); // 4
,再换个写法:
...
for(var i=0;i<10;i++){
if(i>5){
foo.call(foo,i)
}
}
...
运行结果console.log(foo.count); // 4
,依然为4,是我们期望的结果,以上证明this
不是指向函数自身,再看下面的代码:
function foo(){
var a=2;
this.bar();
}
function bar(){
console.log(this.a);
}
foo(); // a is not defined
这段错误的代码试图通过this
联通foo()
和bar()
的词法作用域,从而让bar
可以访问foo
作用域的变量a
,所以this
指向作用域是错误说法。
三、this
绑定规则
函数的调用位置决定了this的绑定规则,而调用位置则需要从调用栈中查找,它就在当前正在执行的函数的前一个调用中,如下代码:
function baz(){
// 当前调用栈是:baz
// 因此,当前的调用位置是全局作用域
console.log('baz');
bar(); // <-- bar的调用位置
}
function bar(){
// 当前调用栈是:baz -> bar
// 因此,当前的调用位置在baz中
console.log('bar');
foo(); // <-- foo的调用位置
}
function foo(){
// 当前调用栈是:baz -> bar -> foo
// 因此,当前的调用位置在bar中
console.log('foo');
}
baz() // <-- baz的调用位置
- 默认绑定
这条规则是其他绑定规则无法匹配时的默认规则。最常见的独立函数调用,代码如下:
function foo(){
console.log(this.a);
}
var a = 2;
foo(); // 2
声明在全局作用域中的变量(比如var a=2
),就是全局对象的一个同名属性,所以调用foo()
时,this.a
被解析成了全局变量a
,因为本例应用了默认绑定,this
指向全局变量,当然如果是严格模式下,则不能将全局对象用于默认绑定,如下:
function foo(){
"use strict"
console.log(this.a);
}
var a = 2;
foo(); // TypeError: this is undefined
- 隐式绑定
当调用位置有上下文对象或者被某个对象拥有(包含)时,代码如下:
function foo(){
console.log(this.a);
}
var obj = {
a:2,
foo
}
obj.foo(); // 2
严格来说foo()
函数不管是在obj
中定义还是先定义再添加为引用属性,都是不属于obj对象。然而,调用位置会使用obj
上下文来引用函数,因此函数调用时obj
对象“拥有”或者“包含”函数引用,隐式绑定规则会把函数调用中的this
绑定到这个上下文对象,因为调用foo()
时this
被绑定到obj
,因此obj.a
和this.a
是一样的。
对象属性引用链中只有最后一层在调用位置中起作用,代码如下:
function foo(){
console.log(this.a);
}
var obj1 = {
a:99,
foo
}
var obj2 = {
a:100,
obj1
}
obj2.obj1.foo(); // 99 this 绑定到最后一层obj1
- 隐式丢失
隐式绑定一个常见的问题就是隐式绑定的函数会丢失绑定对象,从而绑定到全局对象或者undefined上:
function foo(){
console.log(this.a);
}
var obj = {
a:2,
foo
}
var bar=obj.foo; // 函数别名
var a="oops , global" // a是全局对象的属性
bar(); // oops , global
虽然bar
是obj.foo
的一个引用,但实际上引用的是foo()
函数本身,因此bar()
调用等同于foo()
调用,应用默认绑定规则,更常见的是传入回调函数中:
function foo(){
console.log(this.a);
}
function doFoo(fn){
// fn 引用的是foo
fn(); // <-- 调用位置
}
var obj = {
a:2,
foo
}
var a="oops , global"
doFoo(obj.foo); // oops , global
参数传递其实是一种隐式赋值,因此传入函数时也会被隐式赋值,索引结果和上一个例子一样,更直观的例子:
function foo(){
console.log(this.a);
}
function doFoo(fn){
// fn 引用的是foo
fn(); // <-- 调用位置
}
var obj = {
a:2,
foo
}
var a="oops , global"
setTimeout(obj.foo,100); // oops , global
此处是否想到了箭头函数??
- 显示绑定
使用call(...)
和apply(...)
方法,可以在某个对象上强制调用函数,它们的第一个参数是一个对象,是给this准备的,称之为显示绑定:
function foo(){
console.log(this.a);
}
var obj = {
a:2
}
var a="oops , global"
foo.call(obj); // 2
显示绑定也无法解决绑定丢失的问题,不过一个变种可以解决这个问题:
function foo(){
console.log(this.a);
}
var obj = {
a:2
}
var bar=function(){
foo.call(obj);
}
bar(); // 2
setTimeout(bar,100); // 2
bar.call(window); // 2
通过一个包裹函数手动调用foo.call(obj)
,后面无论如何调用函数bar,都会在obj
上调用foo
,将包裹函数改写为可复用的辅助函数:
function foo(something){
console.log(this.a,something);
return this.a + something;
}
// 简单的辅助绑定函数
function bind(fn,obj){
return function(){
return fn.apply(obj,arguments);
}
}
var obj = {
a:2
}
var bar = bind(foo,obj);
var b = bar(3); // 2 3
console.log(b); // 5
由于这个辅助函数非常常用,所以ES5提供了内置方法Function.prototype.bind
,用法如下:
function foo(something){
console.log(this.a,something);
return this.a + something;
}
var obj = {
a:2
}
var bar = foo.bind(obj);
var b = bar(3); // 2 3
console.log(b); // 5
第三方库的许多函数以及Javascript语言和宿主环境中许多的内置函数,都提供一个可选参数,称为上下文(context),其作用和bind
一样,确保回调函数使用指定的this
:
function foo(el){
console.log(el,':',this.id);
}
var obj = {
id:"awesom"
};
// 调用foo(...)时把this绑定到obj
[2,3,5].forEach(foo,obj); // 2:awesom 3:awesom 5:awesom
这些函数实际上就是通过call(...)
或者apply(...)
实现了显示绑定。
- new绑定
首先要明确一点,js
的new
操作符和面向对象语言(java/C#
)调用构造函数完全不一样,js
的构造函数只是一些使用new操作符时被调用的函数,它们并不属于某个类,也不会实例化一个类,只是被new
操作符调用的普通函数,使用new
来调用函数,会自动执行下面的操作:
1. 创建一个全新的对象;
2. 这个新对象会被执行`[[Prototype]]`连接;
3. 这个新对象会绑定到函数调用的`this`;
4. 如果函数没有返回其他对象,那么`new`表达式中的函数调用会自动返回这个新对象;
function foo(a){
this.a=a;
}
var bar = new foo(2);
console.log(bar.a); // 2
使用new
来调用foo(...)
时,会构造一个新对象并把它绑定到foo(...)
调用中的this
上,称之为new
绑定。
- 优先级
四条this
绑定规则:默认绑定、隐式绑定、显示绑定、new
绑定,如果某个调用位置可以应用多条规则该应用哪条规则?首先,默认绑定优先级是最低的,看看隐式绑定和显示绑定:
function foo(){
console.log(this.a);
}
var obj1={
a:2,
foo
}
var obj2={
a:3,
foo
}
obj1.foo(); // 2 隐式绑定
obj2.foo(); // 3 隐式绑定
obj1.foo.call(obj2); // 3 显示绑定
obj2.foo.call(obj1); // 2 显示绑定
可以看到显示绑定优先级更高,那new
绑定和隐式绑定呢?
function foo(params){
this.a = params;
}
var obj1={
foo
}
var obj2={}
obj1.foo(2);
console.log(obj1.a); // 2
obj1.foo.call(obj2, 3);
console.log(obj2.a); // 3
var bar = new obj1.foo(4);
console.log(obj1.a); // 2
console.log(bar.a); // 4 bar 是new返回的新对象
可以看到new
绑定比隐式绑定优先级更高,但new
绑定和显示绑定谁的优先级更高呢?因为无法通过new foo.call(obj1)
来测试,而Function.prototype.bind(...)
创建的包装函数是硬绑定,如下:
function foo(params){
this.a = params;
}
var obj1={};
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a); // 2
var baz = new bar(3);
console.log(obj1.a); // 2
console.log(baz.a); // 3
bar
被应绑定到obj1
上,但是new bar(3)
并没有把obj1.a
修改为3,相反new
修改了绑定到bar(...)
中的this
,所以new
绑定优先级高于硬绑定。
- 绑定例外
如果把null
或者undefined
作为this
的绑定对象传入call
,apply
或者bind
,这些值在调用时会被忽略,实际应用默认绑定:
function foo(){
console.log(this.a);
}
var a = 2;
foo.call(null); // 2
间接应用也会导致调用这个函数引用默认绑定:
function foo(){
console.log(this.a);
}
var a = 2;
var o = { a:3,foo };
var p = { a:4 };
o.foo(); // 3
(p.foo = o.foo)(); // 2
赋值表达式p.foo = o.foo
的返回值是目标函数的引用,因此调用位置是foo()
而不是p.foo
或者o.foo
,从而应用默认规则;
- 箭头函数
ES6中有一种无法应用四种绑定规则的特殊函数:箭头函数。它使用外层(函数或者全局)作用域来决定this
。
function foo(){
return (a)=>{
// this 继承自foo()
console.log(this.a)
}
}
var obj1={a:2};
var obj2={a:3};
var bar = foo.call(obj1);
bar.call(obj2); // 2,不是3
foo()
内部创建的箭头函数会捕获调用时foo()
的this
,由于foo(
)的this
绑定到obj1
,bar
(引用箭头函数)的this
也会绑定到obj1
,箭头函数的绑定无法被修改。
结束…~ _ ~