这是我所知道最完整最简洁的JavaScript基础教程。 本文将带你尽快走进JavaScript的世界——前提是你有一些编程经验的话。本文试图描述这门语言的最小子集。我给这个子集起名叫做“JavaScript简易教程”,并推荐那些准备深入阅读细节和高级技巧之前的新手阅读。合抱之木生于毫末,九层之台起于垒土,欲速则不达。本文的最后会提出如何进一步学习。 警告:下面是我所描述的规则集和最佳实践。我喜欢整洁清晰(例如,你可以随时通过下面的目录快速导航)。横看成岭侧成峰,远近高低各不同,虽然规则是无懈可击的,但不可避免——每个人的理解会各不相同。 目录本文约定(Conventions used in this blog post)命令行交互(Command line interaction)每当我介绍一个新概念,我都会尝试通过JavaScript命令行进行演示。像下面这样:
大于号后面的文本是用户输入内容。其他的都是JavaScript引擎的输出内容。此外,也可以使用console.log()来向控制台打印数据(这种方法可以在大部分JavaScript引擎中工作,包括Node.js). 查找文档(Finding documentation)有时你会看到一些函数或方法有超链接,你应该清楚他们的工作原理。如果没有,可以在Mozilla Developer Network (MDN)上查看细节,你也可以使用Google在MDN上查找文档。例如,下面是通过Google搜索数组的push()方法的例子: 语言的性质(The nature of the language)本节对JavaScript的性质做简要介绍,以帮你理解一些疑问。 JavaScript 和 ECMAScript(JavaScript versus ECMAScript)编程语言称为JavaScript,语言标准被称为ECMAScript。他们有不同名字的原因是因为“Java”已经被注册为商标(属于Oracle)。目前,只有Mozilla被正式允许使用“JavaScript”名称,因为很久以前他们得到一份许可。因此,开放的语言标准拥有不同的名字。当前的JavaScript版本是ECMAScript 5,ECMAScript 6当前是开发版。 影响(Influences)JavaScript之父,Brendan Eich 别无选择必须迅速创建一门语言。(否则,会更糟糕,Netscape将使用其他技术)。他借鉴了几门其他语言:
JavaScript直到ECMAScript 3才加入异常处理,这解释了为什么这门语言经常自动转换类型和经常静默失败:最初没有抛出异常的功能。 一方面,JavaScript有很多怪癖,并且缺失很多功能(块级变量作用域(block-sciped variables),模块(modules)支持子类型(subtyping)等)。另一方面,它有几个非常强大的特性,允许你弥补上面的问题。在其他语言中,你要学习语言特性。在JavaScript中,你需要经常学习模式代替。 深入阅读(Further reading)
语法(Syntax)这节介绍一些JavaScript的基本语法规则。 语句和表达式(Statements versus expressions)了解JavaScript的语法,先来了解两个主要的语法类型:语句和表达式。
语句和表达式之间的区别最好通过实例说明,JavaScript(像Java)有两种不同的方式实现if-then-else。一种是用语句:
另一种是表达式:
你可以将后者作为函数参数(但前者不行):
最后,每当JavaScript期待一个语句,你也可以用一个表达式代替。例如:
流程控制语句和语句块(Control flow statements and blocks)流程控制语句,其语句体可以是单条语句。举两个例子:
然而,任何语句总能被语句块代替,花括号包含零或多条语句。因此,你也可以这样写:
在本文中,我们只使用后一种方式。 分号(Semicolons)JavaScript中的分号是可选的。但省略(分号)可能会带来意想不到的结果,所以我建议你不要那样做。 正如上面所看到的,分号作为语句的结尾,但语句块不需要。仅有一种情况下你能看到语句块后面有分号——函数表达式后面的函数体块。表达式作为语句的结尾,后面是分号:
注释(Comments)JavaScript的注释有两种形式:单行注释和多行注释。单行注释以//开头,以换行符结尾:
多行注释用/**/包裹
深入阅读变量和赋值(Variables and assignment)JavaScript中的变量在使用前必须先声明,否则会报错引用错误(Reference Error):
赋值(Assignment)你可以在声明变量的同时为其赋值:
你也可以给已经存在的变量重新赋值:
复合赋值操作符(Compount assignment operators)有很多复合赋值操作符,例如 =。下面的两个赋值操作等价:
标识符和变量名(Identifiers and variable names)标识符就是事物的名字,在JavaScript中他们扮演不同的语法角色。例如,变量的名称是一个标识符。 大体上,标识符的第一个字符可以是任何Unicode字符、美元标志符($)或下划线(_)。后面可以是任意字符和数字。因此,下面全是合法的标识符:
注意:首字符不能是数字,如果是数字的话,该如何区分是数字还是变量呢? 一些标识符是“保留关键字”——他们是语法的一部分,不能用作变量名:
从技术上讲,下面三个标识符不是保留字,但也不应该作为变量名:
深入阅读
值(Values)JavaScript有所有我们期待的编程语言值类型:布尔,数字,字符串,数组等。JavaScript中的所有值都有属性。每个属性有一个键(或名字)和一个值。参考记录的域(fields of record)。你可以使用点(.)操作符读取属性:
举个例子:字符串“abc”有属性lenght(长度)。
上面的代码也可以写成下面这样:
点操作符也可以用来给属性赋值:
你也可以通过它(.)调用方法:
上面,我们在值“hello”上面调用方法 toUpperCase()。 原始类型值和对象(Primitive values versus objects)JavaScript定义了不同值之间的区别:
两者之间的主要区别在于他们是如何被比较的:每一个对象有一个独一无二的标志,并且仅和自己相等:
相反,所有原始值只要编码值相同就被认为是相同的:
接下来的两节会介绍原始值和对象的更多细节。 原始类型值(Primitive values)下面全是原始类型值(简称:原始值):
原始值的特征:
对象(Objects)所有非原始值(non-primitive)的值都是对象。最常见的几种对象类型是:
对象的特征:
-** 用户可扩展(user-extensible):**你可以通过构造函数定义新的对象类型。 所有的数据结构(如数组)都是对象,但并不是所有的对象都是数据结构。例如:正则表达式是对象,但不是数据结构。 undefined 和 null(undefined and null)多少有些不必要,JavaScript有两个“无值(non-values)”:undefined 和 null。
通常情况下你应该把undefined和null看成是等价的,如果他们代表相同意义的无值的话。检查他们的一种方式是通过严格比较:
另一种在实际中使用的方法是认为undefined 和 null 都是false:
警告:false,0,NaN 和 “” 都被当作false。 包装类型(Wrapper types)对象类型的实例Foo(包括内建类型,例如Array和其他自定义类型)从对象Foo.prototype上获取方法。你可以通过读取这个方法的方式(不是调用)验证这点:
相反,原始类型是没有类型的,所以每个原始类型有一个关联类型,称之为包装类型:
包装类型也有实例(他们的实例是对象),但不常用。相反,包装类型有其他用处:如果你将他们作为函数调用,他们可以将值转换为原始类型。
通过typeof 和 instanceof 将值分类(Categorizing values via typeof and instanceof)有两个操作符可以用来将值分类:typeof 主要用于原始值,instanceof 主要用于对象。 typeof 使用方法如下:
下面列出了typeof操作的所有结果:
有两个结果和我们上面说的的原始值与对象是矛盾的:
instanceof使用方法如下:
如果 value 是一个对象,并且value 是由构造函数Constr创建的(参考:类)。例如:
深入阅读
布尔(Booleans)布尔类型原始值包括true和false。下面的操作符产生布尔值:
真值和假值(Truthy and falsy)每当JavaScript希望一个布尔值时(例如:if语句的条件),可以使用任何值。它将被理解(转换)为true或false。下面的值被理解为false:
所有其他值被认为true。被理解为false的值称为假值(falsy),被理解为true的值称为真值(truthy)。可以使用Boolean作为函数,测试值被理解为什么。
二元逻辑运算符(Binary logical operators)JavaScript中的二元逻辑运算符是短路运算——如果第一个操作数可以确定结果,第二个操作数将不被验证(运算)。例如,在下面的代码中,函数foo()永远不会被调用。
此外,二元逻辑运算符会返回操作数中的一个——可能是一个布尔值,也可能不是。一张真值表用来决定返回哪个值:
等值运算符(Equality operators)在JavaScript中检测相等,你可以使用严格相等(===)和严格不等(!==)。或者你也可以使用非严格相等(==)和非严格不等(!=)。经验规则:总是用严格运算符,假装非严格运算符不存在。严格相等更安全。 深入阅读数字(Numbers)JavaScript中的所有数字都是浮点型(虽然大部分的JavaScript引擎内部也使用整数)。至于为什么这样设计,查看这里(每一个JavaScript开发者应该了解的浮点知识)。
特殊数字:
运算符(Operators)JavaScript中有下列算数运算符:
全局对象Math通过函数提供更多算数运算操作。 JavaScript中也有位运算符(例如:位与 &)。 深入阅读在2ality有一系列博文介绍这些内容,例如:
字符串(Strings)字符串可以直接通过字符串字面量创建。这些字面量被单引号或双引号包裹。反斜线(\)转义字符并且产生一些控制字符。例如:
可以通过方括号访问单个字符:
length属性是字符串的字符数量。
提醒:字符串是不可变的,如果你想改变现有字符串,你需要创建一个新的字符串。 字符串运算符(String operators)字符串可以通过加号操作符( )拼接,如果其中一个操作数为字符串,会将另一个操作数也转换为字符串。
连续执行拼接操作可以使用 = 操作符:
字符串方法(String methods)字符串有许多有用的方法。例如:
深入阅读语句(Statements)条件(Conditionals)if语句通过布尔条件决定执行那个分支:
下面的switch语句,furit的值决定那个分支被执行。
循环(Loops)for 循环的格式如下:
例子:
当条件成立时while循环继续循环它的循环体。
当条件成立时,do-while循环继续循环。由于条件位于循环体之后,所以循环体总是被至少至少执行一次。
在所有的循环中:
函数(Functions)定义函数的一种方法是通过函数声明:
上面的代码定义一个名称叫做add的函数,有两个参数param1和param2,并且返回参数的和。下面是如何调用这个函数:
另一种定义add()函数的方法是通过函数表达式:
函数表达式产生一个值,因此可以直接将函数作为参数传递给其他函数:
函数声明提升(Function declarations are hoisted)函数声明会被提升,他们全被移动到当前作用域开始之处。这允许你在函数声明之前调用它们:
注意:虽然变量声明也会被提升,但赋值的过程不会被提升:
特殊变量arguments(The special variable arguments)在JavaScript中你可以调用任意函数并传递任意数量的参数——语言绝不会抱怨(参数检测)。都可以正常工作,然而,使所有参数可访问需要通过特殊变量 arguments。arguments 看起来像数组,但它没有数组的方法(称为类数组 array-like)。
太多或太少参数(Too many or too few arguments)让我们通过下面的函数探索JavaScript中传递太多或太少参数时如何处理(函数 toArray在后面提到)
多出的参数将被忽略(可以通过arguments访问):
缺少的参数将会是undefined:
可选参数(Optional parameters)下面是一个常见模式,给参数设置默认值:
强制数量(Enforcing an arity)如果你想强制参数的数量,你可以检测arguments.length:
将arguments 转换为数组(Converting arguments to an array)arguments 不是一个数组,它仅仅是类数组(array-like):它有一个length属性,并且你可以通过方括号索引方式访问它的元素。然而,你不能移除元素,或在它上面调用任何数组方法。因此,有时你需要将其转换为数组。这就是下面函数的作用。
深入阅读异常处理(Exception handling)异常处理最常见的方式像下面这样:
try分支包裹易出错的代码,如果try分支内部抛出异常,catch分支将会执行。 深入阅读严格模式(Strict mode)严格模式开启检测和一些其他措施,使JavaScript变成更整洁的语言。推荐使用严格模式。为了开启严格模式,只需在JavaScript文件或script标签第一行添加如下语句:
你也可以在每个函数上选择性开启严格模式,只需将上面的代码放在函数的开头:
下面的两小节看下严格模式的三大好处。 明确错误(Explicit errors)让我们看一个例子,严格模式给我们明确的错误,否则JavaScript总是静默失败:下面的函数 f() 执行一些非法操作,它试图更改所有字符串都有的只读属性——length:
当你调用上面的函数,它静默失败,赋值操作被简单忽略。让我们将 f() 在严格模式下运行:
现在浏览器报给我们一些错误:
不是方法的函数中的this(this in non-method functions)在严格模式下,不作为方法的函数中的this值是undefined:
在非严格模式下,this的值是被称作全局对象(global object)(在浏览器里是window):
不再自动创建全局变量(No auto-created global variables)在非严格模式下,如果你给不存在的变量赋值,JavaScript会自动创建一个全局变量:
在严格模式下,这会产生一个错误:
深入阅读变量作用域和闭包(Variable scoping and closures)在JavaScript中,你必须使用变量之前,通过var声明变量:
你可以用一条var语句声明和初始化多个变量:
但我建议每个变量使用一条语句。因此,我将上面的语句重写为:
由于提升(见下文),最好在函数顶部声明变量。 变量和函数作用域(Variables are function-scoped)变量的作用域总是整个函数(没有块级作用域)。例如:
我们可以看到tmp变量不仅在(*)所在行的语句块存在,它在整个函数内都存在。 变量提升(Variables are hoisted)变量声明会被提升:声明会被移到函数的顶部,但赋值过程不会。举个例子,在下面的函数中(*)行位置声明了一个变量。
在内部,上面的函数被执行像下面这样:
闭包(Closures)每个函数保持和函数体内部变量的连接,甚至离开创建它的作用域之后。例如:
在(*)行开始的函数在它创建时保留上下文,并在内部保存一个start活动值:
闭包是一个函数加上和其作用域链的链接。因此,createIncrementor() 返回的是一个闭包。 IIFE:模拟块级作用域(IIFE: Simulating block scoping)有时你想模拟一个块,例如你想将变量从全局作用域隔离。完成这个工作的模式叫做 IIFE(立即执行函数表达式(Immediately Invoked Function Expression)):
上面你会看到函数表达式被立即执行。外面的括号用来阻止它被解析成函数声明;只有函数表达式能被立即调用。函数体产生一个新的作用域并使 tmp 变为局部变量。 闭包实现变量共享(Inadvertent sharing via closures)下面是个经典问题,如果你不知道,会让你费尽思量。因此,先浏览下,对问题有个大概的了解。 闭包保持和外部变量的连接,有时可能和你想像的行为不一致:
(*)行的返回值总是当前的i值,而不是当函数被创建时的i值。当循环结束后,i的值是5,这是为什么数组中的所有函数的返回值总是一样的。如果你想捕获当前变量的快照,你可以使用 IIFE:
深入阅读
对象和继承(Objects and inheritance)和所有的值类型一样,对象有属性。事实上,你可以将对象当作一组属性的集合,每个属性都是一对(键和值)。键是字符串,值可以是任意JavaScript值。到目前为止,我们仅仅见过键是标识符的属性,因为点操作符处理的键必须为标识符。在这节,你讲见到另一种访问属性的方法,能将任意字符串作为键。 单个对象(Single objects)在JavaScript中,你可以直接创建对象,通过对象字面量:
上面的对象有两个属性:name 和 describe。你能读(“get”)和 写(“set”)属性:
属性是函数如 describe 可以被当作方法调用。当调用他们时可以在它们内部通过this引用对象。
in 操作符用来检测一个属性是否存在:
若读取一个不存在的属性,将会得到undefined值。因此上面的两个检查也可以像下面这样:
delete操作符用来删除一个属性:
任意键属性(Arbitrary property keys)属性的键可以是任意字符串。到目前为止,我们看到的对象字面量中的和点操作符后的属性关键字。按这种方法你只能使用标识符。如果你想用其他任意字符串作为键名,你必须在对象字面量里加上引号,并使用方括号获取和设置属性。
方括号允许你动态计算属性关键字:
引用方法(Extracting methods)如果你引用一个方法,它将失去和对象的连接。就其本身而言,函数不是方法,其中的this值为undefined(严格模式下)。
解决办法是使用函数内置的bind()方法。它创建一个新函数,其this值固定为给定的值。
方法内部的函数(Functions inside a method)每个函数都有一个特殊变量this。如果你在方法内部嵌入函数是很不方便的,因为你不能从函数中访问方法的this。下面是一个例子,我们调用forEach循环一个数组:
调用 logHiToFriends 会产生错误:
有两种方法修复这问题。 #1:将this存储在不同的变量。
#2:forEach的第二个参数允许提供this值。
在JavaScript中函数表达式经常被用作函数参数。时刻小心函数表达式中的this。 构造函数:对象工厂(Constructors: factories for objects)目前为止,你可能认为JavaScript的对象仅是键值的映射,通过JavaScript对象字面量可以得出这个观点,看起来很像其他语言中的地图/字典(map/dictionary)。然而,JavaScript对象也支持真正意义上的面向对象特性:继承(inheritance)。本节不会完全讲解JavaScript中继承的工作原理,但会给你以此为开始的简单模式。如果你想得到更多知识,请查阅这篇文章“JavaScript inheritance by example”。 除了作为“真正”的函数和方法,函数还在JavaScript中扮演第三种角色:如果通过new操作符调用,他们会变为构造函数,对象的工厂。构造函数是对其他语言中的类的粗略模拟。约定俗成,构造函数的第一个字母大写。例如:
我们看到构造函数分为两部分:首先,Point函数设置实例数据。其次,Point.prototype属性包含对象的方法。前者的数据是每个实例私有的,后面的数据是所有实例共享的。 我们通过new操作符调用Point:
p是Point的一个实例:
深入阅读
数组(Arrays)数组是数组元素的序列,能通过整数索引方法数组元素,数组索引从0开始。 数组字面量(Array literals)数组字面量创建数组很方便:
上面的数组有三个元素:分别是字符串“a”,“b”, “c”。你可以通过整数索引访问它们:
length属性总表示一个数组有多少项元素。
除此之外它也可以用来从数组上移除尾部元素:
in操作符也可以在数组上工作。
值得注意的是数组是对象,因此可以有对象属性:
数组方法(Array methods)数组有许多方法。举些例子:
遍历数组(Iterating over arrays)有几种方法可以遍历数组元素。其中两个最重要的是 forEach 和 map。 forEach遍历整个数组,并将当前元素和它的索引传递给一个函数:
上面代码的输出
注意(*)行的函数参数是可省略的。例如:它可以只有一个参数 elem。 map创建一个新数组,通过给每个存在数组元素应用一个函数:
深入阅读正则表达式(Regular expressions)JavaScript内建支持正则表达式。他们被双斜线分隔:
方法 test():测试是否匹配(Method test(): is there a match?)
方法 exec():匹配和捕获组(Method exec(): match and capture groups)
返回的数组第一项(索引为0)是完整匹配,捕获的第一个分组在第二项(索引为1),等。有一种方法可以反复调用获取所有匹配。 方法 replace():搜索并替换(Method replace(): search and replace)
replace的第一个参数必须是正则表达式,并且开启全局搜索(/g 标记),否则仅第一个匹配项会被替换。有一种方法使用一个函数来计算替换项。 深入阅读
|
|