JavaScript - 语言基础
深入探讨 JavaScript 基本/对象类型、函数、执行上下文、作用域链、闭包的本质和匿名函数(lambda)
引言
JavaScript "诞生于 Java 的阴影之下" - Brenden Eich
它是 "世界上最被误解的编程语言" - Douglas Crockford
JavaScript 如今被广泛使用,几乎无处不在,只要你使用网页浏览器上网,大多数网站都内置 JS,甚至在服务器端也是如此 - nodejs
根据 http://langpop.com/,JavaScript 是世界上第四受欢迎的编程语言。
在这篇文章中,我将尝试使用精炼的语言来涵盖 JavaScript 的一些基础知识(其中许多曾经困扰过相当多的开发人员),包括基本数据类型、比较机制、函数、执行上下文、变量作用域链、闭包的本质和匿名函数(lambda)。希望它能多多少少帮助人们在未来的 JS 编码中获得更多乐趣,减少挫败感。
基本数据类型/对象类型
在 JavaScript 中,有 5 种基本类型:Undefined
、Null
、Boolean
、Number
和 String
。整数、布尔值和字符串是按值访问的,这与许多现代语言(如 C# (System.String
) 或 Python (PyStringObject
))不同,在这些语言中,string
是对象并按引用传递。下面的 JS 代码片段证明了 string
存储在栈中并按值传递。
var str = "a";
function strValTest(s) {
s = "b"; // "s" is a String value: "a".
alert(s); // Will alert "b".
}
strValTest(str);
alert(str); // Will alert "a", because when called strValTest,
// String variable's value is passed as argument.
JavaScript 中的 String
也是不可变的,就像许多其他语言一样,即对现有 string
所做的任何更改都会在内存中创建一个新的 string
并销毁旧的 string
(这仍然与 C# 不同,在 C# 中有一个 字符串内部池 来存储托管堆中的所有 String
值)。下面的代码片段展示了 string
和 String
之间的区别
var str1 = "A new String";
console.log("str1");
console.log(str1 instanceof String); // false
console.log(str1 instanceof Object); // false
console.log(typeof (str1)); // string
var str2 = new String("A new String"); // Create a new Object stored
// on the heap with value "A new String"
console.log("str2");
console.log(str2 instanceof String); // true
console.log(str2 instanceof Object); // true
console.log(typeof (str2)); // object
那么你可能会有疑问:既然 string
是值类型,为什么 string
实例会有方法呢?答案是,在 JavaScript 中,基本类型有相应的 Object
包装器:Number
、Boolean
和 String
,它们继承自 Object
并拥有自己的属性和方法,例如 Number.toFixed()
和 String.indexOf()
。下面给出了一个简单的代码片段
string str = "I am a JSer"; // Create a new string variable on the stack
// with value "I am a JSer".
alert(str.indexOf("JSer"));
本质上,在后端,JS 解释器会临时创建一个新的 String
对象并调用其实例方法 "indexOf
",方法调用完成后,临时 String
对象将被回收,该过程可以演示如下
string str = "I am a JSer";
var tmp = new String(str);
alert(tmp.indexOf("JSer"));
tmp = null;
比较
比较在 JavaScript 中可能是一个非常令人困惑的部分,为什么?请看下面的代码
console.log(null == undefined); // true Per ECMA-262, null and undefined are
// superficially equal, essentially "the value
// undefined is a derivative of
// null"<professional>.
console.log(null === undefined); // false
console.log(NaN == NaN); // false. A specific NaN is not considered
// equal to another NaN because they may be
// different values. Also refer:
// http://en.wikipedia.org/wiki/NaN
console.log('5' == 5); // true.
console.log('5' === 5); // false. typeof('5') is string and
// typeof(5) is number
console.log(false == 0); // true
console.log(true == 1); // true
console.log(null == 0); // false
console.log(typeof (null)); // object
console.log(typeof (undefined)); // undefined
Foo.prototype = {
constructor: Foo,
valueOf: function () {
return "Object Foo";
},
toString: function () {
return "Foo";
}
};
var foo1 = new Foo();
console.log("foo1 == foo2: " + (foo1 == "Object Foo")); // true will call
// foo1.valueOf()
var foo2 = new Foo();
console.log("foo1 == foo2: " + (foo1 == foo2)); // false foo1, foo2 points to
// different instance of Foo
foo2 = foo1;
console.log("foo1 == foo2: " + (foo1 == foo2)); // true no doubt
</professional>
你流汗了吗?我流汗了……所以我读了一些书,并从《Web 开发人员专业 JavaScript》中复制了下面一段话。
- 如果操作数是布尔值,则在检查相等性之前将其转换为数值。
- false 值转换为 0,true 值转换为 1。
- 如果一个操作数是字符串,另一个是数字,则在检查相等性之前尝试将字符串转换为数字。
- 如果任一操作数是对象,则调用 valueOf() 方法以检索原始值,然后根据之前的规则进行比较。如果 valueOf() 不可用,则调用 toString()。
- 操作符在进行比较时也遵循这些规则
- null 和 undefined 的值是相等的。
- null 和 undefined 的值不能转换为任何其他值以进行相等性检查。
- 如果任一操作数是 NaN,则相等运算符返回 false,不相等运算符返回 true。重要提示:即使两个操作数都是 NaN,相等运算符也返回 false,因为根据规则,NaN 不等于 NaN。
- 如果两个操作数都是对象,则比较它们是否是同一个对象。如果两个操作数都指向同一个对象,则相等运算符返回 true。否则,两者不相等。
函数
在 JavaScript 中,函数不仅是传统的函数,而且还是一个对象,定义一个函数实际上是定义一个指向该函数的指针,函数不仅是传统的函数,而且还是一个 Object
。我编写了下面的代码片段以便更好地理解
function dummyFunc() { // Define a function and a pointer to it,
// the pointer's name is "dummyFunc"
this.DummyProperty = "Dummy Property";
console.log("Dummy func");
}
var tempFunc = dummyFunc; // Define a variable tempFunc,
// let it equal to dummyFunc which is a function pointer
// pointing to function defined above
dummyFunc = null; // null the dummyFunc
tempFunc(); // tempFunc still points to the function defined above
// so still can be executed.
var dummy = new tempFunc(); // Will invoke tempFunc's constructor to form a new Object
console.log(dummy.DummyProperty);
函数的另一个非常重要的点是参数,在 JavaScript 中,函数的参数全部按值传递,而不是引用,即使参数是 Object
,要证明这一点,请看下面的代码片段
var person = new Object();
function setName(obj) {
obj.Name = "Wayne"; // obj is actually newly created and given the pointer's
// value, so obj and the reference type outside this
// function will both point to the Object on the heap,
// thus operation on obj will affect the Object passed
// in the function.
obj = new Object(); // By executing this line,
// temporary variable obj will point to a new Object,
// has no relationship with the passed-in Object any more.
obj.Name = "Wendy";
}
setName(person); // Executing this line will pass person's pointer stored
// in stack to the function setName,
alert(person.Name); // Will alert "Wayne"
执行上下文和变量作用域链
执行上下文是所有 JavaScript 运行的环境,如果未指定,上下文通常是全局(window),或者可以通过调用 call/apply 来指定。在较低级别,当 JavaScript 解释器开始执行一个函数时,该函数的执行上下文将被推入栈中,然后函数本身将被推入栈中。
代码片段选自:http://www.nczonline.net/blog/2010/02/16/my-javascript-quiz/。
var x = 5,
o = {
x: 10,
doIt: function doIt() {
var x = 20;
setTimeout(function () {
alert(this.x);
}, 10);
}
};
o.doIt(); // Will alert 5 because the execution context is window, window.x = 5;
o.doIt = function () {
var x = 20;
// Change the function's execution context by call()/apply
setTimeout((function () { alert(this.x); }).apply(o), 20);
}
o.doIt(); // Will alert 10 because execution context is object o, o.x = 10;
作用域链是用于搜索上下文中代码中出现的标识符的对象列表。当一段代码在其执行上下文中执行时,在该上下文中会形成一个作用域链,本地变量位于开头,全局变量位于结尾,JavaScript 通过向上遍历作用域链(从本地到全局)来解析特定上下文中的标识符,如果在遍历整个作用域链后仍找不到变量,则会发生错误。在函数内部,其作用域链中的第一个变量是 arguments
。
var name = "solarSystem"; // Assuming the global execution context is
// The Universe here:)
function earth() {
var name = 'earth';
(function () {
var name = 'country'; // name belongs to local Scope Chain now
alert(name); // country
})();
alert(name); // earth
}
earth(); // In the earth execution context,
// "The Universe"'s Scope Chain contains solarSystem can be accessed.
alert(name); // solarSystem
alert(blah); // Throw error, because cannot find variable definition
// for "blah" after traversing the entire Scope Chain.
闭包
Douglas Crockford:“JavaScript 拥有闭包。这意味着内部函数总是可以访问其外部函数的 var
和参数,即使在外部函数已经返回之后也是如此。这是该语言的一个极其强大的特性。”
在 JavaScript 中,当你嵌套函数时会形成闭包,内部函数可以引用其外部封装函数中存在的变量,即使它们的父函数已经执行完毕。
让我们首先看看一个基本的闭包
function foo(x) {
var y = 2;
return function (z) {
console.log("x + y + z: " + (x + y + z));// Result will be 1 + 2 + 3 = 6
}
}
var bar = foo(1); // Bar is now a closure
bar(3);
要深入理解闭包,我们必须首先理解我上面描述的函数、执行上下文和作用域链,因此,上面代码片段的描述可以是:foo
被定义为一个函数指针,该函数接受一个参数,因此该参数在未来被调用时属于其作用域链,在 foo
内部,定义了一个局部变量 y
,其整数值为 2
,因此它也在作用域链中,最后它返回一个接受一个参数 z
的匿名函数,一旦 foo
被调用,它会返回一个指向该匿名函数的指针,整个过程可以详细描述如下
- 为
foo
准备执行上下文。 - 将形成
foo
的作用域链,链上的成员有:arguments
、y
、匿名函数。 - 匿名函数已定义但未执行,当它将来执行时,它自己的作用域链也将在
foo
的作用域链的较低层形成,链上的成员有:arguments
、z
,最重要的是,foo
的作用域链将为此匿名函数保留。 foo
返回匿名函数,将创建一个闭包,闭包中的作用域链将被保留,除非程序明确将其设置为null
。请注意,当foo
返回时,它内部的匿名函数并未执行!- 当执行 bar 并传入参数
3
时,JavaScript 解释器会首先搜索 bar 的作用域链,并尝试查找x
、y
、z
,z
是3
但找不到x
和y
,然后它会向上攀升一级,找到保留的Scope
,x
的值是1
,y
的值是2
(如果找不到会再次向上攀升,在这种情况下,它将是全局的),啊哈,我们找到了它们,结果是6
。
清楚了吗?没有困惑?我希望是 :) 简单来说,闭包是一个可以访问父作用域链并且作用域链被保留的函数!
下面的代码片段应该能帮助我们完全理解“保留的作用域链”
function wayneClosore() {
var i = 0;
i++;
return function () {
console.log(i);
};
}
var closure = wayneClosore();
closure(); // 1
closure(); // 1
closure(); // 1
closure(); // 1
一旦我们创建了新的闭包——"closure
",它的外部函数的作用域链就会为它保留(即变量 "i
" 以值 1
存储在作用域链中),因此当这个闭包稍后执行时,它会获取存储的作用域链,其中变量 "i
" 的值是 1
,无论它执行多少次,上面的代码实际上只是打印出值为 1
的 i
,结果将始终是 1
。
既然作用域链被保留,其中的变量就可以被改变,如果我像下面这样修改上面的代码
function wayneClosore() {
var i = 0;
return function () {
console.log(++i);
};
}
var closure = wayneClosore();
closure(); // 1
closure(); // 2
closure(); // 3
closure(); // 4
每次我执行 "closure
" 时,它都会在作用域链中获取变量 "i
" 并每次将其值增加 i
。
此外,如果一个函数体内有多个内部函数,那么保留的作用域链将在它们之间共享,请参考下面的另一个示例
function shareScope() {
var n = 0;
return {
"innerFuncA": function () { console.log(++n); },
"innerFuncB": function () { console.log(++n); }
};
}
var shareScopeInstance = shareScope();
shareScopeInstance.innerFuncA(); // 1
shareScopeInstance.innerFuncB(); // 2
深入探讨,本质上在 ECMAScript 中,函数有一个“内部属性”——[[Scope]]
,ECMA-262 将其定义为:定义函数对象执行环境的词法环境。例如,在上面的例子中,当 foo 执行并返回值给 bar 时,foo 的作用域链被保存到 bar 的 [[Scope]] 属性中。
最后,让我们看一个可能会让很多人困惑的例子,然后结束闭包部分。
function buildList(list) {
var result = [];
for (var i = 0; i < list.length; i++) {
result.push(function () {
console.log(++i);
});
}
return result;
}
var fnlist = buildList([1, 2, 3]);
for (var idx in fnlist)
fnlist[idx]();
在上面的例子中,答案不是 "1,2,3
",在 buildList
执行后,result
是一个包含 n (n = 3) 个闭包的数组,所有这些闭包都共享 buildList
创建的相同作用域链,当它们中的每一个执行时,JS 解释器会获取作用域链并寻找 i
,在作用域链中 i
的值是多少?在 for
循环之后 i
变成了 3,因为 JS 没有块级作用域,i
仍然存在于 for
循环之外,在控制台中你会看到打印出 "4, 5, 6
"。
匿名函数 (Lambda)
只要我们完全理解作用域链和闭包,就不会对匿名函数感到困惑,它本质上是一个没有函数指针指向它的已声明函数,它总是用于设置一个“块级作用域”,例如,许多 JavaScript 库都在一个大的匿名函数内部执行
(function (window, undefined) {
var VirtualCompany = function () {
};
})(window);
匿名函数一旦完全下载就立即执行,传入 window 对象,并且只暴露一个全局对象:VirtualCompany
,这样库就封装了其内部实现,并且不会与其他 JS 库冲突。
通过使用匿名函数,我们可以修改我上面演示的闭包示例,以使其达到我们最初的目标
function buildList(list) {
var result = [];
for (var i = 0; i < list.length; i++) {
result.push((function (i) {
return function () { console.log(++i); };
})(i));
}
return result;
}
var fnlist = buildList([1, 2, 3]);
for (var idx in fnlist)
fnlist[idx]();
这次,结果将是 "1,2,3
",因为每次调用 "result.push
" 时,推入数组的是什么?答案是:一个存储了匿名函数作用域链的闭包,匿名函数作用域链中有什么?for
循环中的 i
。在 for
循环的每次迭代中,通过将 i
传入来执行一个匿名函数,所以 i
存在于它的作用域链中,并且由于匿名函数返回另一个匿名函数,因此形成了一个闭包,并且作用域被保留了下来。
摘要
在我看来,JavaScript 是一门伟大的语言,它有着非常光明的未来,考虑到它在即将到来的 Web 标准 - HTML5 中的重要作用,基于事件 I/O 的高性能 Web 服务器 - nodejs,以及 JavaScript 在即将到来的云计算时代也将扮演重要角色,是时候让那些以前没有认真学习它的人重新学习了。事实上,我已经编写了 6 年的糟糕 JavaScript 代码,然而,说实话,我很惭愧过去从未认真学习过它,它的一些基本理论、有用的技能以及最佳实践我从未了解过,所以我写了这篇文章来总结我重新学习的东西。希望它能帮助像我这样的程序员。
快乐地编写 JavaScript 代码!做一个快乐的 JSer。:)