65.9K
CodeProject 正在变化。 阅读更多。
Home

JavaScript - 语言基础

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (24投票s)

2011年7月24日

CPL

10分钟阅读

viewsIcon

37749

深入探讨 JavaScript 基本/对象类型、函数、执行上下文、作用域链、闭包的本质和匿名函数(lambda)

引言

JavaScript "诞生于 Java 的阴影之下" - Brenden Eich

它是 "世界上最被误解的编程语言" - Douglas Crockford

JavaScript 如今被广泛使用,几乎无处不在,只要你使用网页浏览器上网,大多数网站都内置 JS,甚至在服务器端也是如此 - nodejs

根据 http://langpop.com/,JavaScript 是世界上第四受欢迎的编程语言

在这篇文章中,我将尝试使用精炼的语言来涵盖 JavaScript 的一些基础知识(其中许多曾经困扰过相当多的开发人员),包括基本数据类型、比较机制、函数、执行上下文、变量作用域链、闭包的本质和匿名函数(lambda)。希望它能多多少少帮助人们在未来的 JS 编码中获得更多乐趣,减少挫败感。

基本数据类型/对象类型

在 JavaScript 中,有 5 种基本类型:UndefinedNullBooleanNumberString。整数、布尔值和字符串按值访问的,这与许多现代语言(如 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 值)。下面的代码片段展示了 stringString 之间的区别

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 包装器:NumberBooleanString,它们继承自 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

execution-context.png

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 被调用,它会返回一个指向该匿名函数的指针,整个过程可以详细描述如下

  1. foo 准备执行上下文。
  2. 将形成 foo 的作用域链,链上的成员有:argumentsy、匿名函数。
  3. 匿名函数已定义但未执行,当它将来执行时,它自己的作用域链也将在 foo 的作用域链的较低层形成,链上的成员有:argumentsz,最重要的是,foo 的作用域链将为此匿名函数保留。
  4. foo 返回匿名函数,将创建一个闭包,闭包中的作用域链将被保留,除非程序明确将其设置为 null。请注意,当 foo 返回时,它内部的匿名函数并未执行!
  5. 当执行 bar 并传入参数 3 时,JavaScript 解释器会首先搜索 bar 的作用域链,并尝试查找 xyzz3 但找不到 xy,然后它会向上攀升一级,找到保留的 Scopex 的值是 1y 的值是 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,无论它执行多少次,上面的代码实际上只是打印出值为 1i,结果将始终是 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。:)

延伸阅读 (强烈推荐)

© . All rights reserved.