什么是作用域?
几乎所有编程语言最基本的功能之一,就是能够储存变量当中的值,并且能在之后对这个 值进行访问或修改。事实上,正是这种储存和访问变量的值的能力将状态带给了程序。
若没有了状态这个概念,程序虽然也能够执行一些简单的任务,但它会受到高度限制,做 不到非常有趣。
但是将变量引入程序会引起几个很有意思的问题,这些变量住在 哪里?换句话说,它们储存在哪里?最重要的是,程序需要时如何找到它们?
这些问题说明需要一套设计良好的规则来存储变量,并且之后可以方便地找到这些变量。 这套规则被称为作用域。
简而言之,作用域是根据名称查找变量的一套规则。
(以上摘录自《你不知道的JS》)
什么是this?
JavaScript有两种作用域:词法(静态)作用域和动态作用域(即this)。
词法作用域即函数和变量的作用域在定义的时候就确定了(即声明的位置)。如:
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar();
// 结果是 ???
答案是1. foo定义在全局对象,所以作用域即window, foo内部没有找到value,就往作用域链上面找,即window,window对象有声明value,则打印1.
注:浏览器的全局对象默认是window
在看一个例子(摘取《你不知道的JS》):
function identify() {
return this.name.toUpperCase();
}
function speak() {
var greeting = "Hello, I'm " + identify.call( this );
console.log( greeting );
}
var me = {
name: "Kyle"
};
var you = {
name: "Reader"
};
identify.call( me ); // KYLE
identify.call( you ); // READER
speak.call( me ); // Hello, 我是 KYLE
speak.call( you ); // Hello, 我是 READER
这段代码可以在不同的上下文对象(me 和 you)中重复使用函数 identify() 和 speak(), 不用针对每个对象编写不同版本的函数。
如果不使用 this,那就需要给 identify() 和 speak() 显式传入一个上下文对象(context)。
function identify(context) {
return context.name.toUpperCase();
}
function speak(context) {
var greeting = "Hello, I'm " + identify( context );
console.log( greeting );
}
var me = {
name: "Kyle"
};
var you = {
name: "Reader"
};
identify(you); // READER
speak(me); //Hello, I'm KYLE
随着你的使用模式越来越复杂,显式传递上下文对象会让代码变得越来越混乱,使用 this 提供了一种更优雅的方式来隐式“传递”一个对象引用。也就是动态作用域。this
指的是函数运行时所在的环境(上下文对象)。
了解了作用域和this,接下来进入本文的主题:
如何确定函数的运行环境?
var obj = {
foo: function () { console.log(this.bar) },
bar: 1
};
var foo = obj.foo;
var bar = 2;
obj.foo() // 1
foo() // 2
同样都是foo函数,为什么结果不一样呢?这种差异的原因,就在于函数体内部使用了this
关键字。很多教科书会告诉你,this
指的是函数运行时所在的环境。对于obj.foo()
来说,foo
运行在obj
环境,所以this
指向obj
;对于foo()
来说,foo
运行在全局环境,所以this
指向全局环境。所以,两者的运行结果不一样。这种解释没错,但是教科书往往不告诉你,为什么会这样?也就是说,函数的运行环境到底是怎么决定的?举例来说,为什么
obj.foo()
就是在obj
环境执行,而一旦var foo = obj.foo
,foo()
就变成在全局环境执行?
这篇文章从内存的数据结构以及函数如何存储方法进行了解释。
下面简要总结下:
JavaScript 中函数也是对象。上面代码中foo函数有一块独立的内存来保存函数,obj.foo保存的是这块内存的地址。由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行(即被任意对象调用)。
比如obj.foo() foo是在obj内部的指向函数的属性,并通过 obj.foo 间接引用了函数,所以函数调用位置上下文对象是obj,所以,this 指代的是 foo函数的调用者obj,打印1
foo() 是直接使用不带任何修饰的函数引用进行调用, 相当于独立调用,而非由其他对象所调用,这种情况在非严格环境下,this会默认绑定到全局对象window, 所以打印2
注:严格环境下会是undefined
下面再来看几个例子:
例一:
通过上述解释,funs通过obj.funs()调用,所以funs内this是obj,虽然fun2定义在funs内部,但是上面说过:this
指的是函数运行时
所在的环境(上下文对象)。 和定义在哪里无关。funs() 通过函数引用直接调用,所以会使用默认绑定到window
上述例子我们知道了this绑定的两种情况:
- 隐式绑定:由指定对象间接调用函数,绑定到指定对象
- 默认绑定:非严格模式下window,严格模式下undefined
那么如何让例一中fun2()的this也绑定到obj呢?
改造下:
let obj = {
funs() {
let fun2 = function() { console.log(this) } //obj
fun2.call(this)
}
}
obj.funs()
通过call, apply, bind等方法可以显示指定this绑定到哪个对象。这便是this的显示绑定。
new
当我们使用new来调用函数的时候,此时函数被当作构造函数来处理,具体new的实现可以参考这篇文章,构造函数内的this绑定到新生成的对象。
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2
this绑定的四种规则
至此,上述内容可以总结出四种规则来确定this的值:
- 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。 var bar = new foo()
- 函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是 指定的对象。 var bar = foo.call(obj2)
- 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上 下文对象。 var bar = obj1.foo()
- 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到 全局对象。 var bar = foo()
以上出自《你不知道的JS》
this词法
我们之前介绍的四条规则已经可以包含所有正常的函数。但是 ES6 中介绍了一种无法使用 这些规则的特殊函数类型:箭头函数。
箭头函数并不是使用 function 关键字定义的,而是使用被称为“胖箭头”的操作符 => 定 义的。箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决 定 this。
将上面的例一改造下:
let obj = {
funs() {
let fun2 = () => { console.log(this) } //obj
fun2()
}
}
obj.funs()
箭头函数使用词法作用域(在定义时确定),fun2定义在函数funs内,所以函数funs环境决定了fun2的环境。
obj.funs()调用后funs的环境是obj, 所以fun2的环境也是obj, 后面调用时打印出obj。
注:箭头函数this值无法被修改
绑定例外
在某些场景下 this 的绑定行为会出乎意料,你认为应当应用其他绑定规则时,实际上应用 的可能是默认绑定规则。
function foo() {
console.log( this.a );
}
var a = 2;
foo.call( null ); // 2
如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值 在调用时会被忽略,实际应用的是默认绑定规则。
var value = 1;
var foo = {
value: 2,
bar: function () {
return this.value;
}
}
//示例1
console.log(foo.bar()); // 2
//示例2
console.log((foo.bar)()); // 2
//示例3
console.log((foo.bar = foo.bar)()); // 1
//示例4
console.log((false || foo.bar)()); // 1
//示例5
console.log((foo.bar, foo.bar)()); // 1
另一个需要注意的是,你有可能(有意或者无意地)创建一个函数的“间接引用”,在这 种情况下,调用这个函数会应用默认绑定规则。
如示例3 赋值表达式 foo.bar = foo.bar 的返回值是目标函数的引用,函数实际是直接使用不带任何修饰的(匿名)函数引用进行调用, 类似与通过 foo() 调用(注:这里若调用foo()会报not function错误哦,因为全局未声明) 而不是 foo.bar()调用 。根据我们之前说过的,这里会应用默认绑定。示例4,5 分别使用 逻辑运算和逗号运算符返回了目标函数的引用,因此结果同示例3。
注意:对于默认绑定来说,决定 this 绑定对象的并不是调用位置是否处于严格模式,而是 函数体是否处于严格模式。如果函数体处于严格模式,this 会被绑定到 undefined,否则 this 会被绑定到全局对象。
参考来源
书籍《你不知道的JavaScript》
JavaScript 的 this 原理 -- 阮一峰
JavaScript深入之从ECMAScript规范解读this -- 冴羽
JavaScript深入之new的模拟实现 -- 冴羽
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。