深入理解 JS 中声明提升、作用域(链)和 this 关键字
这个 issue 试图阐述 JavaScript 这门语言的3个难点:声明提升、作用域(链)和 this
。
声明提升
大部分编程语言都是先声明变量再使用,但在 JS 中,事情有些不一样:
console.log(a); // undefined var a = 1;
上面是合法的 JS 代码,正常输出 undefined
而不是报错 Uncaught ReferenceError: a is not defined
。为什么?就是因为声明提升(hoisting)。
变量声明
参考:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var
语法:
var varname1 [= value1 [, varname2 [, varname3 ... [, varnameN]]]];
变量名可以是任意合法标识符;值可以是任意合法表达式。
重点:
- 变量声明,不管在哪里发生(声明),都会在任意代码执行前处理。(Variable declarations, wherever they occur, are processed before any code is executed. )。
- 以
var
声明的变量的作用域就是当前执行上下文(execution context),即某个函数,或者全局作用域(声明在函数外)。 - 赋值给未声明的变量,当执行时会隐式创建全局变量(成为 global 的属性)。
声明变量和未声明变量的区别:
- 声明变量通常是局部的,未声明变量通常全局的。
- 声明变量在任意代码执行前创建,未声明变量直到赋值时才存在。
- 声明变量是execution context(function/global)的non-configurable 属性,未声明变量则是configurable。
在es5 strict mode
,赋值给未声明的变量将报错。
定义函数(Defining functions)
定义一个函数有两种方式:函数声明(function definition/declaration/statement)和函数表达式( function expression)。
1.2.1 function definition
语法:function name(arguments) {}
对参数而言,primitive parameter 是传值,对象是传引用。
1.2.2 function expression
语法:var fun = function (arguments) {}
函数表达式中函数可以不需要名字,即匿名函数。
1.2.3 其它
还可以用 Function
构造函数来创建函数。
在函数内部引用函数本身有3种方式。比如var foo = function bar(){};
- 函数名字,即
bar()
arguments.callee()
foo()
1.3 声明提升
1.1
提到,var
声明的变量会在任意代码执行前处理,这意味着在任意地方声明变量都等同于在顶部声明——即声明提升。1.2
特意强调了函数定义,因为声明提升中,需要综合考虑一般变量和函数。
在JavaScript中,一个变量名进入作用域的方式有 4 种:
- Language-defined:所有的作用域默认都会给出
this
和arguments
两个变量名(global 没有arguments
); - Formal parameters(形参):函数有形参,形参会添加到函数的作用域中;
- Function declarations(函数声明):如
function foo() {}
; - Variable declarations(变量声明):如
var foo
,包括_函数表达式_。
函数声明和变量声明总是会被移动(即 hoist)到它们所在的作用域的顶部(这对你是透明的)。
而变量的解析顺序(优先级),与变量进入作用域的4种方式的顺序一致。
一个详细的例子:
function testOrder(arg) { console.log(arg); // arg是形参,不会被重新定义 console.log(a); // 因为函数声明比变量声明优先级高,所以这里a是函数 var arg = 'hello'; // var arg;变量声明被忽略, arg = 'hello'被执行 var a = 10; // var a;被忽视; a = 10被执行,a变成number function a() { console.log('fun'); } // 被提升到作用域顶部 console.log(a); // 输出10 console.log(arg); // 输出hello }; testOrder('hi'); /* 输出: hi function a() { console.log('fun'); } 10 hello */
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: AMD 加载器分析与实现
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(10)
@CharlesOy 写issue里可以互动啊
为何要发到issue里面,直接写成文档多好~
文章很好啊谢谢
写的好,感觉理解又深了一些
不明觉厉
_围观男神装逼!!!!_吓得我打字都歪了
男神起飞
长文必火!前排挤挤~
3. 作用域(Scope)和闭包(closure)
在第2部分对
this
的探讨中,我们已经部分涉及到了作用域,只是没有展开说,或者从作用域角度来说。3.1 Scope是什么?
先尝试从几个方面描述下:
综合一下,Scope即上下文,包含当前所有可见的变量。
Scope分为Lexical Scope和Dynamic Scope。Lexical Scope正如字面意思,即词法阶段定义的Scope。换种说法,作用域是根据源代码中变量和块的位置,在词法分析器(lexer)处理源代码时设置。
让我们考虑下面的代码来分析Lexical Scope:
Scope是分层的,内层Scope可以访问外层Scope的变量,反之则不行。上面的代码中即有嵌套Scope。用泡泡来比喻Scope可能好理解一点:
foo
;foo
,有标识符a
,bar
,b
;bar
,仅有标识符c
。Scope在我们写代码的时候就被定义好了,比如谁嵌套在谁里面。
3.2 JavaScript Scope
JavaScript采用Lexical Scope。
于是,我们仅仅通过查看代码(因为JavaScript采用Lexical Scope),就可以确定各个变量到底指代哪个值。
另外,变量的查找是从里往外的,直到最顶层(全局作用域),并且一旦找到,即停止向上查找。所以内层的变量可以shadow外层的同名变量。
3.2.1 Cheating Lexical
如果Scope仅仅由函数在哪定义的决定(在写代码时决定),那么还有方式更改Scope吗?JS有
eval
和with
两种机制,但两者都会导致代码性能差。3.2.1.1
eval
eval
接受字符串为参数,把这些字符串当做真的在程序的这个点写下的代码——意味着可以编码方式来在某个点生成代码,就像真的在程序运行前在这里写了代码。默认情况下,
eval
会动态执行代码,并改变当前Scope。但非直接(indirectly)调用eval
可以让代码执行在全局作用域,即修改全局Scope。另外,严格模式下,
eval
运行在它自己的Scope下,即不会修改包含它的Scope。3.2.1.1
with
with
以对象为参数,并把这个对象当做完全独立的Lexical Scope(treats that object as if it is a wholly separate lexical scope),然后这个对象的属性就被当做定义的变量了。**注意:**尽管把对象当做Scope,
var
定义的变量仍然scoped到包含with
的函数中。不像
eval
可以改变当前Scope,with
凭空创建了全新的Scope,并把对象传进去。所以o1
传进去时可以正确更改o1.a
,而o2
传进去时,创建了全局变量a
。3.3 Dynamic Scope?
上一节讲到,JS采用Lexical Scope,这里再明确一下:
JavaScript没有Dynamic Scope。
那么为什么又单开一节讲一下?
一是强调,二是,JS中的
this
机制跟Dynamic Scope很像,都是运行时绑定。3.4 Function vs. Block Scope
上面的内容有意无意似乎应该表明了,JS没有Block Scope。
除了Global Scope,只有function可以创建新作用域(Function Scope)。 不过这已经是老黄历了,ES6引入了Block Scope。
另外,
with
和try catch
都可以创建Block Scope。2.
this
this
关键词是JavaScript中最令人疑惑的机制之一。this
是非常特殊的关键词标识符,在每个函数的作用域中被自动创建,但它到底指向什么(对象),很多人弄不清。当函数被调用,一个activation record(即 execution context)被创建。这个record包涵信息:函数在哪调用(call-stack),函数怎么调用的,参数等等。record的一个属性就是
this
,指向函数执行期间的this
对象。this
不是author-time binding,而是 runtime binding。this
的上下文基于函数调用的情况。和函数在哪定义无关,而和函数怎么调用有关。2.1
this
在具体情况下的分析2.1.1 Global context
在全局上下文(任何函数以外),
this
指向全局对象。2.1.2 Function context
在函数内部时,
this
由函数怎么调用来确定。2.1.2.1 Simple call
简单调用,即独立函数调用。由于
this
没有通过call
来指定,且this
必须指向对象,那么默认就指向全局对象。在严格模式下,
this
保持进入execution context时被设置的值。如果没有设置,那么默认是undefined
。它可以被设置为任意值**(包括null/undefined/1
等等基础值,不会被转换成对象)**。2.1.2.2 Arrow functions
在箭头函数中,
this
由词法/静态作用域设置(set lexically)。它被设置为包含它的execution context的this
,并且不再被调用方式影响(call/apply/bind)。2.1.2.3 As an object method
当函数作为对象方法调用时,
this
指向该对象。this on the object's prototype chain
原型链上的方法根对象方法一样,作为对象方法调用时
this
指向该对象。2.1.2.4 构造函数
在构造函数(函数用
new
调用)中,this
指向要被constructed的新对象。2.1.2.5 call和apply
Function.prototype
上的call
和apply
可以指定函数运行时的this
。注意,当用
call
和apply
而传进去作为this
的不是对象时,将会调用内置的ToObject
操作转换成对象。所以4
将会装换成new Number(4)
,而null/undefined
由于无法转换成对象,全局对象将作为this
。2.1.2.6 bind
ES5引进了
Function.prototype.bind
。f.bind(someObject)
会创建新的函数(函数体和作用域与原函数一致),但this
被永久绑定到someObject
,不论你怎么调用。2.1.2.7 As a DOM event handler
this
自动设置为触发事件的dom元素。