Open creeperyang opened 8 years ago
this
this
关键词是JavaScript中最令人疑惑的机制之一。this
是非常特殊的关键词标识符,在每个函数的作用域中被自动创建,但它到底指向什么(对象),很多人弄不清。
当函数被调用,一个activation record(即 execution context)被创建。这个record包涵信息:函数在哪调用(call-stack),函数怎么调用的,参数等等。record的一个属性就是this
,指向函数执行期间的this
对象。
this
不是author-time binding,而是 runtime binding。this
的上下文基于函数调用的情况。和函数在哪定义无关,而和函数怎么调用有关。this
在具体情况下的分析在全局上下文(任何函数以外),this
指向全局对象。
console.log(this === window); // true
在函数内部时,this
由函数怎么调用来确定。
简单调用,即独立函数调用。由于this
没有通过call
来指定,且this
必须指向对象,那么默认就指向全局对象。
function f1(){
return this;
}
f1() === window; // global object
在严格模式下,this
保持进入execution context时被设置的值。如果没有设置,那么默认是undefined
。它可以被设置为任意值(包括null/undefined/1
等等基础值,不会被转换成对象)。
function f2(){
"use strict"; // see strict mode
return this;
}
f2() === undefined;
在箭头函数中,this
由词法/静态作用域设置(set lexically)。它被设置为包含它的execution context的this
,并且不再被调用方式影响(call/apply/bind)。
var globalObject = this;
var foo = (() => this);
console.log(foo() === globalObject); // true
// Call as a method of a object
var obj = {foo: foo};
console.log(obj.foo() === globalObject); // true
// Attempt to set this using call
console.log(foo.call(obj) === globalObject); // true
// Attempt to set this using bind
foo = foo.bind(obj);
console.log(foo() === globalObject); // true
当函数作为对象方法调用时,this
指向该对象。
var o = {
prop: 37,
f: function() {
return this.prop;
}
};
console.log(o.f()); // logs 37
this on the object's prototype chain
原型链上的方法根对象方法一样,作为对象方法调用时this
指向该对象。
在构造函数(函数用new
调用)中,this
指向要被constructed的新对象。
Function.prototype
上的call
和apply
可以指定函数运行时的this
。
function add(c, d){
return this.a + this.b + c + d;
}
var o = {a:1, b:3};
add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16
add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34
注意,当用call
和apply
而传进去作为this
的不是对象时,将会调用内置的ToObject
操作转换成对象。所以4
将会装换成new Number(4)
,而null/undefined
由于无法转换成对象,全局对象将作为this
。
ES5引进了Function.prototype.bind
。f.bind(someObject)
会创建新的函数(函数体和作用域与原函数一致),但this
被永久绑定到someObject
,不论你怎么调用。
this
自动设置为触发事件的dom元素。
在第2部分对this
的探讨中,我们已经部分涉及到了作用域,只是没有展开说,或者从作用域角度来说。
先尝试从几个方面描述下:
综合一下,Scope即上下文,包含当前所有可见的变量。
Scope分为Lexical Scope和Dynamic Scope。Lexical Scope正如字面意思,即词法阶段定义的Scope。换种说法,作用域是根据源代码中变量和块的位置,在词法分析器(lexer)处理源代码时设置。
让我们考虑下面的代码来分析Lexical Scope:
function foo(a) {
// inner scope 'foo'
// defined argument a, and look-up b upwards
console.log( a + b );
}
// outmost/global scope
// defined b
var b = 2;
foo( 2 ); // 4
Scope是分层的,内层Scope可以访问外层Scope的变量,反之则不行。上面的代码中即有嵌套Scope。用泡泡来比喻Scope可能好理解一点:
foo
;foo
,有标识符a
,bar
,b
;bar
,仅有标识符c
。Scope在我们写代码的时候就被定义好了,比如谁嵌套在谁里面。
JavaScript采用Lexical Scope。
于是,我们仅仅通过查看代码(因为JavaScript采用Lexical Scope),就可以确定各个变量到底指代哪个值。
另外,变量的查找是从里往外的,直到最顶层(全局作用域),并且一旦找到,即停止向上查找。所以内层的变量可以shadow外层的同名变量。
如果Scope仅仅由函数在哪定义的决定(在写代码时决定),那么还有方式更改Scope吗?JS有eval
和with
两种机制,但两者都会导致代码性能差。
eval
eval
接受字符串为参数,把这些字符串当做真的在程序的这个点写下的代码——意味着可以编码方式来在某个点生成代码,就像真的在程序运行前在这里写了代码。
function foo(str, a) {
eval( str ); // cheating!
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3
默认情况下,eval
会动态执行代码,并改变当前Scope。但非直接(indirectly)调用eval
可以让代码执行在全局作用域,即修改全局Scope。
function bar(str) {
(0, eval)( str ); // cheating in global!
}
bar('var hello = "hi";')
window.hello // "hi"
另外,严格模式下,eval
运行在它自己的Scope下,即不会修改包含它的Scope。
function foo(str) {
"use strict";
eval( str );
console.log( a ); // ReferenceError: a is not defined
}
foo( "var a = 2" );
with
function foo(obj) {
with (obj) {
a = 2;
}
}
var o1 = {
a: 3
};
var o2 = {
b: 3
};
foo( o1 );
console.log( o1.a ); // 2
foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2 -- Oops, leaked global!
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
。
上一节讲到,JS采用Lexical Scope,这里再明确一下:
JavaScript没有Dynamic Scope。
那么为什么又单开一节讲一下?
一是强调,二是,JS中的this
机制跟Dynamic Scope很像,都是运行时绑定。
上面的内容有意无意似乎应该表明了,JS没有Block Scope。
除了Global Scope,只有function可以创建新作用域(Function Scope)。 不过这已经是老黄历了,ES6引入了Block Scope。
{
let x = 0;
}
console.log(x); // Uncaught ReferenceError: x is not defined
另外,with
和try catch
都可以创建Block Scope。
try {
undefined(); // illegal operation to force an exception!
}
catch (err) {
console.log( err ); // works!
}
console.log( err ); // ReferenceError: `err` not found
长文必火!前排挤挤~
男神起飞
_围观男神装逼!!!!_吓得我打字都歪了
不明觉厉
写的好,感觉理解又深了一些
文章很好啊谢谢
为何要发到issue里面,直接写成文档多好~
@CharlesOy 写issue里可以互动啊 😃
好棒~,很详细。复习了一遍,查缺补漏理解更深入了,谢谢 ~
那如果函数的形参和函数声明的名字一样,该如何理解呢?
function test(arg){ console.log(arg); // [function : arg] var arg = 'hello'; function arg(){ console.log('hello world') } console.log(arg); // hello } test('hi');
@liu3042 可以看 变量的解析顺序(优先级),与变量进入作用域的4种方式的顺序一致。 相关部分。
function test(arg){
// 1. 形参 arg 是 "hi"
// 2. function arg声明提升,arg 是 function
console.log(arg); // [function : arg]
var arg = 'hello';
// 3. arg 此时是 "hello"
function arg(){
console.log('hello world')
}
console.log(arg); // hello
}
test('hi');
@liu3042 ,你的代码等价于:
function test(arg){
var arg;
arg = function(){
console.log('hello world');
}
console.log(arg);
arg = 'hello';
console.log(arg);
}
test('hi');
好文,然而上下文和作用域并不是同一个概念。
@JackZhouMine
上面是MDN对Scope
的定义,第一句就是 The current context of execution
。
The scope of a variable declared with var is its current execution context, which is either the enclosing function or, for variables declared outside any function, global. If you re-declare a JavaScript variable, it will not lose its value.
上面是 var
相关部分截取的,可以看到MDN这样定义:一个声明的变量的作用域,是它当前的执行上下文。
所以我觉得把 execution context 和 scope 等同起来理解没有问题(从实际使用来说,或者说至少从理解作用域这些来说没有问题)。
比如 http://davidshariff.com/blog/what-is-the-execution-context-in-javascript/ 这篇文章也把两者等同。
但上下文和作用域是不是同一个概念?我觉得这要看定义。有些规范上这两个概念应该是有区别的,所以你的话没有错。
补充:
稍微看了下ES6规范 Executable Code and Execution Contexts,可以看到,scope和规范中的 Lexical Environment 比较接近,而Execution Context要超出scope的概念,但scope是其重要的组成部分。
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 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 的绑定。
Global Environment Records
An execution context is a specification device that is used to track the runtime evaluation of code by an ECMAScript implementation.
Execution Context的组成:
Component | Purpose |
---|---|
code evaluation state | Any state needed to perform, suspend, and resume evaluation of the code associated with this execution context. |
Function | ... |
Realm | ... |
LexicalEnvironment | Identifies the Lexical Environment used to resolve identifier references made by code within this execution context. |
VariableEnvironment | Identifies the Lexical Environment whose EnvironmentRecord holds bindings created by VariableStatements within this execution context. |
Generator | The GeneratorObject that this execution context is evaluating. |
发现第 3 点中闭包被漏掉了,这里补上。
A closure is the combination of a function and the lexical environment within which that function was declared.
在 JavaScript 中,函数形成闭包。闭包就是函数和函数声明时的词法作用域的组合。
下面用例子说明:
function outer () {
var name = 'Jerry'
function inner () {
console.log(name)
}
return inner
}
var fn = outer()
fn() // Jerry
在部分编程语言中,函数内部的局部变量仅仅存在于函数执行期间,一旦函数执行完毕,变量就销毁(不再能访问)。
但在 JavaScript 中,由于闭包的原因,fn
仍可以访问 name
变量。
闭包就是函数和函数声明的词法作用域的组合,这句话有点难懂...
函数和函数声明时的词法作用域形成闭包 ——这样可能好理解一点。
大神您好,构造函数的call调用比较复杂 能请教下吗?
function add(c, d){
this.d=d; // set调用对象o
return this.a + this.b + c + d; // get调用对象o
}
var o = {a:1, b:3};
add.call(o, 5, 7);
console.log(o);
@Hibop 不是很明白你的问题。没有所谓的构造函数和非构造函数,在用new
调用函数时,函数就是构造函数。
这里 add.call(o, 5, 7);
时,this
指向 o
,就是这样的。
写得很好,赞一下作者。
大佬好! 关于第一点,声明提升中的函数声明提升,有一个这个问题
console.log(fn)
fn()
if (true) {
function fn() {
console.log('fn inner')
}
}
// undefined
// TypeError:fn is not a function
把函数fn的调用移到if块里面,就正常控制台输出fn inner;
严格模式下 console.log(fn)这一行代码也要报错,提示fn is not defined,也就是说函数名的提升只到了if块里面。
这个情况我理解的是函数提升的时候是分为两步的,先是把函数名定义提升到广义的作用域顶部,然后再把函数体的定义提升到狭义的作用域顶部。
这里解释一下我自己定义的广义的作用域和狭义的作用域,广义的指的是ES6之前的作用域,即全局作用域和函数作用域,狭义的除了广义的作用域之外加上了这个if,for等块级作用域。
大佬看我这个理解是否正确呢?
@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 代码](https://babeljs.io/repl/#?experimental=false&evaluate=true&loose=false&spec=false&code=%7B%0A%20%20function%20nested%20()%20%7B%0A%20%20%20%20return%20'nest'%0A%20%20%7D%0A%7D%0A%0Aconsole.log(nested())%3B)的,内外层已经把函数名都变不一样了:
此外,我们从不推荐在block 内定义函数,不论是 ES6 还是 ES5。可以参考 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode/Transitioning_to_strict_mode https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode http://whereswalden.com/2011/01/24/new-es5-strict-mode-requirement-function-statements-not-at-top-level-of-a-program-or-function-are-prohibited/ 了解更多相关知识。
你是哪里知道我的邮箱的
---原始邮件---
发件人: "西门吹喵"<notifications@github.com>
发送时间: 2020年4月4日(周六) 晚上10:33
收件人: "creeperyang/blog"<blog@noreply.github.com>;
抄送: "Subscribed"<subscribed@noreply.github.com>;
主题: Re: [creeperyang/blog] 深入理解JS中声明提升、作用域(链)和this
关键字 (#16)
有个疑问,请求大佬解惑!:关于作用域链的确认的
function a(){ let num = 0; return function(){ console.log(++num) } } const b =a() b() 当我们调用这个函数a的时候,会生成函数a的执行上下文,然后先预编译(我不知道这个“预编译”用的准确不?),然后创建了这个函数执行上下文的变量对象(变量num,不知道这个里面有没有匿名函数🤔️,return的值在这个阶段咋个处理?🤔️)和作用域链(a函数的作用域和=》全局作用域),创建完毕。再执行里面的代码。
当我们执行函数b()的时候,上面同理,然后创建作用域链(函数b =》 函数a =》 全局)。 提问:这里并不是因为函数b里面引用了a的变量count才将函数a放在这条作用域链上的,而是因为函数a嵌套了函数a,所以对于函数b的执行上下文里面的作用域链来说,他们在一条作用域链上,我这样理解对吗?🤔️
— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub, or unsubscribe.
有个疑问,请求大佬解惑!:关于作用域链的确认的
function a(){
let num = 0;
return function(){
console.log(++num)
}
}
const b =a()
b()
当我们调用这个函数a的时候,会生成函数a的执行上下文,然后先预编译(我不知道这个“预编译”用的准确不?),然后创建了这个函数执行上下文的变量对象(变量num,不知道这个里面有没有匿名函数🤔️,return的值在这个阶段咋个处理?🤔️)和作用域链(a函数的作用域和=》全局作用域),创建完毕。再执行里面的代码。
当我们执行函数b()的时候,上面同理,然后创建作用域链(函数b =》 函数a =》 全局)。 提问:这里并不是因为函数b里面引用了a的变量count才将函数a放在这条作用域链上的,而是因为函数a嵌套了函数a,所以对于函数b的执行上下文里面的作用域链来说,他们在一条作用域链上,我这样理解对吗?🤔️
有个疑问,请求大佬解惑!:关于作用域链的确认的
function a(){ let num = 0; return function(){ console.log(++num) } } const b =a() b()
当我们调用这个函数a的时候,会生成函数a的执行上下文,然后先预编译(我不知道这个“预编译”用的准确不?),然后创建了这个函数执行上下文的变量对象(变量num,不知道这个里面有没有匿名函数🤔️,return的值在这个阶段咋个处理?🤔️)和作用域链(a函数的作用域和=》全局作用域),创建完毕。再执行里面的代码。
当我们执行函数b()的时候,上面同理,然后创建作用域链(函数b =》 函数a =》 全局)。 提问:这里并不是因为函数b里面引用了a的变量count才将函数a放在这条作用域链上的,而是因为函数a嵌套了函数a,所以对于函数b的执行上下文里面的作用域链来说,他们在一条作用域链上,我这样理解对吗?🤔️
没什么问题
这个issue试图阐述JavaScript这门语言的3个难点:声明提升、作用域(链)和
this
。首先推荐https://github.com/getify/You-Dont-Know-JS,这是一本非常棒的JavaScript书籍,几乎所有的JS知识点都包括并且详细解释了。看一遍相信必有大收获。
1. 声明提升
大部分编程语言都是先声明变量再使用,但在JS中,事情有些不一样:
上面是合法的JS代码,正常输出
undefined
而不是报错Uncaught ReferenceError: a is not defined
。为什么?就是因为声明提升(hoisting)。1.1 变量声明
参考:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var
语法:
变量名可以是任意合法标识符;值可以是任意合法表达式。
重点:
var
声明的变量的作用域就是当前执行上下文(execution context),即某个函数,或者全局作用域(声明在函数外)。声明变量和未声明变量的区别:
在
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 种:
this
和arguments
两个变量名(global没有arguments
);function foo() {}
;var foo
,包括函数表达式。函数声明和变量声明总是会被移动(即hoist)到它们所在的作用域的顶部(这对你是透明的)。
而变量的解析顺序(优先级),与变量进入作用域的4种方式的顺序一致。
一个详细的例子: