-
Notifications
You must be signed in to change notification settings - Fork 207
Description
这个issue试图阐述JavaScript这门语言的3个难点:声明提升、作用域(链)和this
。
首先推荐https://github.com/getify/You-Dont-Know-JS,这是一本非常棒的JavaScript书籍,几乎所有的JS知识点都包括并且详细解释了。看一遍相信必有大收获。
1. 声明提升
大部分编程语言都是先声明变量再使用,但在JS中,事情有些不一样:
console.log(a); // undefined
var a = 1;
上面是合法的JS代码,正常输出undefined
而不是报错Uncaught ReferenceError: a is not defined
。为什么?就是因为声明提升(hoisting)。
1.1 变量声明
参考: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
,赋值给未声明的变量将报错。
1.2 定义函数(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
*/
Activity
creeperyang commentedon Feb 19, 2016
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元素。creeperyang commentedon Feb 23, 2016
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。cdll commentedon Feb 25, 2016
长文必火!前排挤挤~
yernsun commentedon Feb 25, 2016
男神起飞
For-me commentedon Feb 25, 2016
_围观男神装逼!!!!_吓得我打字都歪了
Power-kxLee commentedon Feb 25, 2016
不明觉厉
keifergu commentedon Apr 17, 2016
写的好,感觉理解又深了一些
moahmn commentedon May 7, 2016
文章很好啊谢谢
oychao commentedon Nov 10, 2016
为何要发到issue里面,直接写成文档多好~
creeperyang commentedon Nov 10, 2016
@CharlesOy 写issue里可以互动啊 😃
9 remaining items
creeperyang commentedon Apr 10, 2017
稍微看了下ES6规范 Executable Code and Execution Contexts,可以看到,scope和规范中的 Lexical Environment 比较接近,而Execution Context要超出scope的概念,但scope是其重要的组成部分。
Lexical Environment
Lexical Environment 是规范里的一种类型,用于定义 Identifiers 和 指定variables/functions 的关联(基于代码的词法嵌套结构lexical nesting structure of ECMAScript code)。
Lexical Environment由 Environment Record 和 一个可能为null的指向外层Lexical Environment的引用 组成。
通常,Lexical Environment和ECMAScript代码的特定词法结构由关,比如
FunctionDeclaration, BlockStatement, Catch clause of a TryStatement
,一旦这种代码执行,新的Lexical Environment被创建。一个 Environment Record 记录了它所属 Lexical Environment 的 scope 内的 identifier bindings。
Environment Record 有两种基本的类别:declarative Environment Records 和 object Environment Records。
一个 Declarative Environment Record 对应一个 ECMAScript program scope (scope包含
variable, constant, let, class, module, import, and/or function declarations
)。Declarative Environment Record绑定了这个scope里所有声明定义的identifiers。Object Environment Records
Function Environment Records
一个 function Environment Record 是用于表示一个函数的顶层scope的 Declarative Environment Record,并且,如果这个函数不是箭头函数,还提供 this 的绑定。
Execution Context
Execution Context的组成:
creeperyang commentedon Aug 9, 2017
发现第 3 点中闭包被漏掉了,这里补上。
闭包(closure)
在 JavaScript 中,函数形成闭包。闭包就是函数和函数声明时的词法作用域的组合。
下面用例子说明:
在部分编程语言中,函数内部的局部变量仅仅存在于函数执行期间,一旦函数执行完毕,变量就销毁(不再能访问)。
但在 JavaScript 中,由于闭包的原因,
fn
仍可以访问name
变量。leirt97 commentedon Sep 12, 2017
闭包就是函数和函数声明的词法作用域的组合,这句话有点难懂...
creeperyang commentedon Sep 12, 2017
函数和函数声明时的词法作用域形成闭包 ——这样可能好理解一点。
Hibop commentedon Jan 24, 2018
大神您好,构造函数的call调用比较复杂 能请教下吗?
creeperyang commentedon Jan 24, 2018
@Hibop 不是很明白你的问题。没有所谓的构造函数和非构造函数,在用
new
调用函数时,函数就是构造函数。这里
add.call(o, 5, 7);
时,this
指向o
,就是这样的。HarryLit commentedon Dec 23, 2018
写得很好,赞一下作者。
hrpc commentedon Feb 13, 2019
大佬好!
关于第一点,声明提升中的函数声明提升,有一个这个问题
把函数fn的调用移到if块里面,就正常控制台输出fn inner;
严格模式下
console.log(fn)这一行代码也要报错,提示fn is not defined,也就是说函数名的提升只到了if块里面。
这个情况我理解的是函数提升的时候是分为两步的,先是把函数名定义提升到广义的作用域顶部,然后再把函数体的定义提升到狭义的作用域顶部。
这里解释一下我自己定义的广义的作用域和狭义的作用域,广义的指的是ES6之前的作用域,即全局作用域和函数作用域,狭义的除了广义的作用域之外加上了这个if,for等块级作用域。
大佬看我这个理解是否正确呢?
creeperyang commentedon Feb 13, 2019
@hrpc 你注意到ES6开始作用域有所不同,这是对的。
ES6 引入了一个新的概念,叫
“Temporal Dead Zone” (TDZ)
,建议可以阅读 https://ponyfoo.com/articles/es6-let-const-and-temporal-dead-zone-in-depth,写的非常清晰。ES6 开始支持了 block scope(局部作用域),大括号内定义的局部变量(非 var 定义)外部不可访问。可以参考下 babel 是怎么翻译一段 ES6 代码的,内外层已经把函数名都变不一样了:
853210384 commentedon Apr 4, 2020
qyang-pp commentedon Apr 4, 2020
有个疑问,请求大佬解惑!:关于作用域链的确认的
当我们调用这个函数a的时候,会生成函数a的执行上下文,然后先预编译(我不知道这个“预编译”用的准确不?),然后创建了这个函数执行上下文的变量对象(变量num,**不知道这个里面有没有匿名函数🤔️,return的值在这个阶段咋个处理?**🤔️)和作用域链(a函数的作用域和=》全局作用域),创建完毕。再执行里面的代码。
当我们执行函数b()的时候,上面同理,然后创建作用域链(函数b =》 函数a =》 全局)。
提问:这里并不是因为函数b里面引用了a的变量count才将函数a放在这条作用域链上的,而是因为函数a嵌套了函数a,所以对于函数b的执行上下文里面的作用域链来说,他们在一条作用域链上,我这样理解对吗?🤔️
creeperyang commentedon Apr 7, 2020
没什么问题