聲明提升
大部分程式設計語言都是先聲明變量再使用,但在JavaScript中,事情有些不一樣:
console.log(a);//undefined
var a = 0;
上面是合法的JavaScript代碼,正常輸出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]]]]
變量名可以是任意合法辨別符;值可以是任意合法表達式。
重點:
- 變量聲明,不管在哪裡發生,都會在任意代碼執行前處理;
- 聲明的變量的作用域就是目前執行上下文,即某個函數或者全局作用域;
- 指派給未聲明的變量,當執行時會隐式建立全局變量;
聲明的變量和未聲明的變量的差別:
- 聲明的變量通常是局部的,未聲明的變量通常是全局的;
- 聲明變量在任意代碼執行前建立,未聲明的變量直到指派時才存在;
- 聲明的變量是執行上下文(function/global)的non-configurable屬性,未聲明的變量則是configurable;
- 在es5 strict mode,指派給未聲明的變量将報錯;
1.2 定義函數
定義一個函數有兩種方式:函數聲明和函數表達式
函數聲明:
文法:function name(arguments){}
函數表達式:
文法:var fun = function(arguments){}
函數表達式中函數可以不需要名字,即匿名函數。
其它
還可以用Function構造函數來建立函數。
文法:var function_name = new Function(arg1, arg2, ..., argN, function_body);
在函數内部引用函數本身有3中方式。比如var foo = function bar(){};
- 函數名字,即bar();
- arguments.callee();
- foo();
1.3 聲明提升
聲明的變量會在任意代碼執行前處理,這意味着在任意地方聲明變量都等同于在頂部聲明-即聲明提升。生命提升中,需要綜合考慮一般變量和函數。
在JavaScript中,一個變量名進入作用域的方式有四種:
- 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
變量進入函數作用域的順序為this,arguments --> 形參 --> 函數聲明 --> 變量聲明。如果重新定義與形參名字相同的變量,則變量聲明部分會被忽略,形參會被重新指派。代碼執行前,聲明提升部分已完成,函數的聲明會比變量的聲明先進入作用域。是以,在變量指派前列印變量,對于函數則輸出函數聲明(包括函數體),對于變量則輸出undefined。
this關鍵字
this關鍵字是JavaScript中最令人疑惑的機制之一。this是非常特殊的關鍵詞辨別符,在每個函數的作用域中被自動建立,但它到底指向什麼(對象),很多人弄不清。
當函數被調用,一個activation record(即execution context)被建立。這個record包含資訊:函數在哪調用(call-stack),函數怎麼調用的,參數等等。record的一個屬性就是this,指向函數執行期間的this對象。
- this不是author-time binding,而是runtime binding(即this的值不是在代碼解釋階段确定的,而是在函數被調用時确定的);
- this的上下文基于函數調用的情況。和函數在哪定義無關,而和函數怎麼調用有關。
this在具體情況下的分析
2.1 Global context
在全局上下文,this指向全局對象。
console.log(this === window); //true
2.2 Function context
在函數内部時,this由函數怎麼調用來确定。
2.2.1 Simple call
簡單調用,即獨立函數調用。由于this沒有通過call來指定,且this必須指向對象,那麼預設就指向全局對象。
function f1(){
return this;
}
f1() === window; // global object
在嚴格模式下,this保持進入execution context時被設定的值。如果沒有設定,那麼預設是undefined。它可以被設定為任意值(包括null/undefined/1等等基礎值,不會被轉換為對象)。
function f2(){
"use strict";
return this;
}
console.log(f2() === undefined); //true
2.2.2 Arrow functions
在箭頭函數中,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
2.2.3 As an object method
當函數作為對象方法調用時,this指向該對象。
var o = {
prop: 37,
f: function() {
return this.prop;
}
};
console.log(o.f()); //logs 37
this on the object's prototype chain
原型鍊上的方法跟對象方法一樣,作為對象方法調用時this指向該對象。
2.2.4 構造函數
在構造函數(函數用new調用)中,this指向要被constructed的新對象。
2.2.5 call和apply
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。
2.2.6 bind
ES5引進了Function.prototype.bind。f.bind(someObject)會建立新的函數(函數體和作用域與原函數一緻),但this被永久綁定到someObject,不論你怎麼調用。
2.2.7 As a DOM event handler
this自動設定為觸發事件的dom元素。
作用域(Scope)和閉包(closure)
3.1 Scope是什麼?
先嘗試從幾個方面描述下:
- Scope這個術語被用來描述在某個代碼塊可見的所有實體(或有效的所有辨別符),更精準一點,叫做上下文(context)或環境(environment)。
- 目前執行的上下文(The current context of execution)。
綜合一下,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在我們寫代碼的時候就被定義好了,比如誰嵌套在誰裡面。
3.2 JavaScript Scope
JavaScript采用Lexical Scope。
于是,我們僅僅通過檢視代碼(因為JavaScript采用Lexical Scope),就可以确定各個變量到底指代哪個值。
另外,變量的查找是從裡往外的,直到最頂層(全局作用域),并且一旦找到,即停止向上查找。是以内層的變量可以shadow外層的同名變量。
3.2.1 Cheating Lexical
如果Scope僅僅由函數在哪定義的決定(在寫代碼時決定),那麼還有方式更改Scope嗎?JavaScript有eval和with兩種機制,但兩者都會導緻代碼性能差。
3.2.1.1 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");
3.2.1.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。
3.3 Dynamic Scope?
JavaScript沒有Dynamic Scope。JavaScript中的this機制跟Dynamic Scope很像,都是運作時綁定。
3.4 Function vs. Block Scope
JavaScript沒有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' is not defined