JavaScript
一、基本认识
JavaScript是一门弱类型脚本语言,其源代码在发往客户端之前不需要经过编译,而是将文本格式的字符代码发送给浏览器由浏览器解释运行。
原生JS开发,即指按照标准的开发方式ES,ES5全浏览器支持,常用ES6。
TypeScript是JavaScript的超集,纳入了许多新特性,需要编译成JS才能被浏览器执行。
JavaScript框架:
1.JQuery:简化了dom操作,但影响前端性能,使用较少
2.Angular:Google收购的前端框架,将后台的MVC模式搬入了前端并增加了模块化开发的理念,采用TypeScript语法。
3.React:Facebook出品,高性能JS框架,提出来新概念:虚拟dom用于减少真实dom操作,提高了前端渲染效率,需额外学习一门语言JSX。
4.Vue:渐进式JavaScript框架,所谓渐进式就是逐步实现新特性的意思,如实现模块化开发、路由、状态管理等新特性。其特点综合了Angular和React。
5.Axios:前端通信框架,Vue不具备通信能力,需要一个额外的通信框架与服务器交互。当然也可以直接使用JQuery提供的AJAX通信功能。
UI框架:
1.Ant-Design:阿里巴巴出品,基于React的UI框架。
2.ElementUI,iview,ice:饿了么出品,基于VUE的UI框架。
3.Bootstrap:Twitter推出的一个用于前端开发的开源工具包。
4.AmazeUI:HTML5跨屏前端框架。
JS构建工具
1.Babel:JS编译工具,常用语编译TypeScript
2.WebPack:模块打包器
三端统一
1.混合开发(Hybrid App)主要目的实现一套代码,三端统一,云打包:Hbuild->HbuildX,Dcloud出品,API Cloud。本地打包:Cordova。
2.微信小程序 : WeUI。
后端技术
NodeJS:方便前端开发后台应用。
Express:NodeJS框架
Koa:Express简化版
NPM:项目综合管理工具,类似于Maven
YARN:NPM的替代方案。
主流前端框架
Vue.js
二、快速入门
2.1引入JS
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!-- script标签,写javascript-->
</head>
<body>
<!--内部标签-->
<script>
alert('hello,world');
</script>
<!--外部引入-->
<script src="D1.js"></script>
<script src="js/D1.js"></script>
<!--不写也行,默认为js-->
<script type="text/javascript"></script>
</body>
</html>
控制台
Web 控制台 不仅可以展示当前已加载页面的信息,还包含一个可以在当前页面执行Javascript表达式的 命令行。
在火狐浏览器菜单栏的 “工具" => “Web开发者” => “Web控制台” 可以打开控制台( 在Windows和Linux上Ctrl+Shift+K ,Mac上Cmd+Option+K) ,它会如期出现在浏览器窗口的底部。如图,最下一行就是可以执行输入的命令行,面板上可以显示执行结果:
控制台的工作方式与eval完全相同:返回最后输入的表达式。为了简单起见,可以想象每次输入到控制台的东西实际上都被 console.log 所包围。
代码草稿纸
Web控制台 对于执行单行 JS 命令十分便捷,虽然你也可以执行多行命令,但是在控制台操作并不方便。除此之外,使用控制台你无法保存你的代码片段。因此对于更为复杂的代码片段,Scratchpad(代码草稿纸) 是一个更好的工具。
从 “Web开发者” 菜单(在火狐浏览器的主菜单下)中选择 “代码草稿纸” 来打开(Shift+F4)。它是一个拥有独立窗口的编辑器,你可以用来编辑和在浏览器中执行 JavaScript。你也可以将代码保存到本地磁盘,或者从本地载入。
如果你选择显示,草稿纸中的代码会在浏览器中执行,并在内容后面以注释的形式插入返回的结果:
比如
function greetMe(user) {
alert('Hi ' + user);
}
greetMe('Hou'); // 'Hi Hou'
在 Scratchpad(代码草稿纸)中选择要执行的代码,然后点击“运行(Ctrl+R)”就可以在浏览器中看到选中代码的执行结果。
常见验证方式
alert() 弹窗
console.log() 控制台输出
document.write() 页面打印
2.2语法和数据类型
基础
JS是区分大小写的,并使用Unicode字符集。
指令被称为语句(Statement),并使用分号分隔;
Javascript 源码从左往右被扫描并转换成一系列由 token 、控制字符、行终止符、注释和空白字符组成的输入元素。空白字符指的是空格、制表符和换行符等。
注释
// 单行注释
/* 这是一个更长的,
多行注释
*/
/* 然而, 你不能, /* 嵌套注释 */ 语法错误 */
声明
var:
声明一个变量,可选初始化一个值
let:
声明一个块作用域的局部变量,可选初始化一个值。
const:
声明一个块作用域的只读常量。
在应用程序中,使用变量来作为值的符号名。变量的名字又叫做标识符,其需要遵守一定的规则。
一个 JavaScript 标识符必须以字母、下划线(_)或者美元符号($)开头;后续的字符也可以是数字(0-9)。因为 JavaScript 语言是区分大小写的,所以字母可以是从“A”到“Z”的大写字母和从“a”到“z”的小写字母。
声明变量
你可以用以下三种方式声明变量:
1.使用关键词 var 。例如 var x = 42。这个语法可以用来声明局部变量和全局变量。
2. 直接赋值。例如x = 42。在函数外使用这种形式赋值,会产生一个全局变量。在严格模式下会产生错误。因此你不应该使用这种方式来声明变量。
3. 使用关键词 let 。例如 let y = 13。这个语法可以用来声明块作用域的局部变量
变量求值
用 var 或 let 语句声明的变量,如果没有赋初始值,则其值为 undefined
如果访问一个未声明的变量会导致抛出一个引用错误(ReferenceError)异常:
var a;
console.log("The value of a is " + a); // a 的值是 undefined
console.log("The value of b is " + b);// b 的值是 undefined
var b;
// 在你阅读下面的‘变量声明提升’前你可能会疑惑,因为var创建的变量是挂载在window顶级对象上
console.log("The value of c is " + c); // 未捕获的引用错误: c 未被定义
let x;
console.log("The value of x is " + x); // x 的值是 undefined
console.log("The value of y is " + y);// 未捕获的引用错误: y 未被定义
let y;
你可以使用 undefined 来判断一个变量是否已赋值。在以下的代码中,变量input未被赋值,因此 if 条件语句的求值结果是 true 。
var input;
if(input === undefined){
doThis();
} else {
doThat();
}
undefined 值在布尔类型环境中会被当作 false 。例如,下面的代码将会执行函数 myFunction,因为数组 myArray 中的元素未被赋值:
var myArray = [];
if (!myArray[0]) myFunction();
数值类型环境中 undefined 值会被转换为 NaN
var a;
a + 2; // 计算为 NaN
当你对一个 null 变量求值时,空值 null 在数值类型环境中会被当作0来对待,而布尔类型环境中会被当作 false。例如:
var n = null;
console.log(n * 32); // 在控制台中会显示 0
变量的作用域
在函数之外声明的变量,叫做全局变量,因为它可被当前文档中的任何其他代码所访问。在函数内部声明的变量,叫做局部变量,因为它只能在当前函数的内部访问。
ECMAScript 6 之前的 JavaScript 没有 语句块 作用域;相反,语句块中声明的变量将成为语句块所在函数(或全局作用域)的局部变量。例如,如下的代码将在控制台输出 5,因为 x 的作用域是声明了 x 的那个函数(或全局范围),而不是 if 语句块。
if (true) {
var x = 5;
}
console.log(x); // 5
如果使用 ECMAScript 6 中的 let 声明,上述行为将发生变化。
if (true) {
let y = 5;
}
console.log(y); // ReferenceError: y 没有被声明
变量提升
JavaScript 变量的另一个不同寻常的地方是,你可以先使用变量稍后再声明变量而不会引发异常。这一概念称为变量提升;JavaScript 变量感觉上是被“提升”或移到了函数或语句的最前面。但是,提升后的变量将返回 undefined 值。因此在使用或引用某个变量之后进行声明和初始化操作,这个被提升的变量仍将返回 undefined 值。
/**
* 例子1
*/
console.log(x === undefined); // true
var x = 3;
/**
* 例子2
*/
// will return a value of undefined
var myvar = "my value";
(function() {
console.log(myvar); // undefined
var myvar = "local value";
})();
由于存在变量提升,一个函数中所有的var语句应尽可能地放在接近函数顶部的地方。这个习惯将大大提升代码的清晰度。
在 ECMAScript 6 中,let(const)同样会被提升变量到代码块的顶部但是不会被赋予初始值。在变量声明之前引用这个变量,将抛出引用错误(ReferenceError)。这个变量将从代码块一开始的时候就处在一个“暂时性死区”,直到这个变量被声明为止。
console.log(x); // ReferenceError
let x = 3;
函数提升
对于函数来说,只有函数声明会被提升到顶部,而函数表达式不会被提升。
foo(); // "bar"
function foo() {
console.log("bar");
}
/* 函数表达式 */
baz(); // 类型错误:baz 不是一个函数
var baz = function() {
console.log("bar2");
};
全局变量
need links to pages discussing scope chains and the global object 实际上,全局变量是全局对象的属性。在网页中,(译注:缺省的)全局对象是 window ,所以你可以用形如 window.variable 的语法来设置和访问全局变量。
因此,你可以通过指定 window 或 frame 的名字,在当前 window 或 frame 访问另一个 window 或 frame 中声明的变量。例如,在文档里声明一个叫 phoneNumber 的变量,那么你就可以在子框架里使用 parent.phoneNumber 的方式来引用它
常量
用关键字 const 创建一个只读的常量。常量标识符的命名规则和变量相同:必须以字母、下划线(_)或美元符号($)开头并可以包含有字母、数字或下划线。
常量不可以通过重新赋值改变其值,也不可以在代码运行时重新声明。它必须被初始化为某个值。
常量的作用域规则与 let 块级作用域变量相同。若省略const关键字,则该标识符将被视为变量。
在同一作用域中,不能使用与变量名或函数名相同的名字来命名常量。例如:
// 这会造成错误
function f() {};
const f = 5;
// 这也会造成错误
function f() {
const g = 5;
var g;
//语句
}
然而,对象属性被赋值为常量是不受保护的,所以下面的语句执行时不会产生错误。
const MY_OBJECT = {"key": "value"};
MY_OBJECT.key = "otherValue";
同样的,数组的被定义为常量也是不受保护的,所以下面的语句执行时也不会产生错误。
const MY_ARRAY = ['HTML','CSS'];
MY_ARRAY.push('JAVASCRIPT');
console.log(MY_ARRAY); //logs ['HTML','CSS','JAVASCRIPT'];
数据结构和类型
8种数据类型
七种基本数据类型:
1.布尔值
2.null
3.undefined
4.数字(Number),
5.任意精度的整数
6.字符串
7.代表
以及对象
对象(Objects)和函数(functions)是这门语言的另外两个基本元素。你可以把对象当作存放值的一个命名容器,然后将函数当作你的程序能够执行的步骤。
数据类型的转换
JavaScript是一种动态类型语言(dynamically typed language)。这意味着你在声明变量时可以不必指定数据类型,而数据类型会在代码执行时会根据需要自动转换。因此,你可以按照如下方式来定义变量:
var answer = 42;
然后,你还可以给同一个变量赋予一个字符串值,例如:
answer = "Thanks for all the fish...";
因为 JavaScript 是动态类型的,这种赋值方式并不会提示出错。
在包含的数字和字符串的表达式中使用加法运算符(+),JavaScript 会把数字转换成字符串。例如,观察以下语句:
x = "The answer is " + 42 // "The answer is 42"
y = 42 + " is the answer" // "42 is the answer"
在涉及其它运算符(译注:如下面的减号’-')时,JavaScript语言不会把数字变为字符串。例如(译注:第一例是数学运算,第二例是字符串运算):
"37" - 7 // 30
"37" + 7 // "377"
字符串转化为数字
有一些方法可以将内存中表示一个数字的字符串转换为对应的数字。
parseInt()和parseFloat()
parseInt 方法只能返回整数,所以使用它会丢失小数部分。另外,调用 parseInt 时最好总是带上进制(radix) 参数,这个参数用于指定使用哪一种进制。
将字符串转换为数字的另一种方法是使用一元加法运算符。
"1.1" + "1.1" = "1.11.1"
(+"1.1") + (+"1.1") = 2.2
// 注意:加入括号为清楚起见,不是必需的。
字面量
字面量是由语法表达式定义的常量;或,通过由一定字词组成的语词表达式定义的常量
在JavaScript中,你可以使用各种字面量。这些字面量是脚本中按字面意思给出的固定的值,而不是变量。(译注:字面量是常量,其值是固定的,而且在程序脚本运行中不可更改,比如false,3.1415,thisIsStringOfHelloworld ,invokedFunction: myFunction(“myArgument”)。
数组字面量
数组字面值是一个封闭在方括号对([])中的包含有零个或多个表达式的列表,其中每个表达式代表数组的一个元素。当你使用数组字面值创建一个数组时,该数组将会以指定的值作为其元素进行初始化,而其长度被设定为元素的个数。
var coffees = ["French Roast", "Colombian", "Kona"];
var a=[3];
console.log(a.length); // 1
console.log(a[0]); // 3
若在顶层(全局)脚本里用字面值创建数组,JavaScript语言将会在每次对包含该数组字面值的表达式求值时解释该数组。另一方面,在函数中使用的数组,将在每次调用函数时都会被创建一次。
数组字面值同时也是数组对象
当数组字面值中有多余逗号时。若你在同一行中连写两个逗号(,),数组中就会产生一个没有被指定的元素,其初始值是undefined。以下示例创建了一个名为fish的数组:
var fish = ["Lion", , "Angel"];
在这个数组中,有两个已被赋值的元素,和一个空元素(fish[0]是"Lion",fish[1]是undefined,而fish[2]是"Angel";译注:此时数组的长度属性fish.length是3)。
如果你在元素列表的尾部添加了一个逗号,它将会被忽略。在下面的例子中,数组的长度是3,并不存在myList[3]这个元素。
布尔字面量
布尔类型有两种字面量:true和false。
不要混淆作为布尔对象的真和假与布尔类型的原始值true和false。布尔对象是原始布尔数据类型的一个包装器。
Boolean对象是一个布尔值的对象包装器。
如果需要,作为第一个参数传递的值将转换为布尔值。如果省略或值0,-0,null,false,NaN,undefined,或空字符串(“”),该对象具有的初始值false。所有其他值,包括任何对象,空数组([])或字符串"false",都会创建一个初始值为true的对象。
注意不要将基本类型中的布尔值 true 和 false 与值为 true 和 false 的 Boolean 对象弄混了。
其值不是undefined或null的任何对象(包括其值为false的布尔对象)在传递给条件语句时都将计算为true。 例如,以下if语句中的条件评估为true:
var x = new Boolean(false);
if (x) {
// 这里的代码会被执行
}
var x = false;
if (x) {
// 这里的代码不会执行
}
var x = Boolean(expression); // 推荐
var x = !!(expression); // 推荐
var x = new Boolean(expression); // 不太好
对于任何对象,即使是值为 false 的 Boolean 对象,当将其传给 Boolean 函数时,生成的 Boolean 对象的值都是 true。
var myFalse = new Boolean(false); // true
var g = new Boolean(myFalse); // true
var myString = new String("Hello");
var s = new Boolean(myString); // true
**最后,不要在应该使用基本类型布尔值的地方使用 Boolean 对象。**
整数字面量
整数可以用十进制(基数为10)、十六进制(基数为16)、八进制(基数为8)以及二进制(基数为2)表示。
十进制整数字面量由一串数字序列组成,且没有前缀0。
八进制的整数以 0(或0O、0o)开头,只能包括数字0-7。
十六进制整数以0x(或0X)开头,可以包含数字(0-9)和字母 a~f 或 A~F。
二进制整数以0b(或0B)开头,只能包含数字0和1。
严格模式下,八进制整数字面量必须以0o或0O开头,而不能以0开头。
整数字面量举例:
0, 117 and -345 (十进制, 基数为10)
015, 0001 and -0o77 (八进制, 基数为8)
0x1123, 0x00111 and -0xF1A7 (十六进制, 基数为16或"hex")
0b11, 0b0011 and -0b11 (二进制, 基数为2)
浮点数字面量
浮点数字面值可以有以下的组成部分:
一个十进制整数,可以带正负号(即前缀“+”或“ - ”),
小数点(“.”),
小数部分(由一串十进制数表示),
指数部分。
指数部分以“e”或“E”开头,后面跟着一个整数,可以有正负号(即前缀“+”或“-”)。浮点数字面量至少有一位数字,而且必须带小数点或者“e”(大写“E”也可)。
[(+|-)][digits][.digits][(E|e)[(+|-)]digits]
3.14
-.2345789 // -0.23456789
-3.12e+12 // -3.12*10^12
.1e-23 // 0.1*10^(-23)=10^(-24)=1e-24
对象字面量
对象字面值是封闭在花括号对({})中的一个对象的零个或多个"属性名-值"对的(元素)列表。你不能在一条语句的开头就使用对象字面值,这将导致错误或产生超出预料的行为, 因为此时左花括号({)会被认为是一个语句块的起始符号。(译者:这 里需要对语句statement、块block等基本名词的解释)
以下是一个对象字面值的例子。对象car的第一个元素(译注:即一个属性/值对)定义了属性myCar;第二个元素,属性getCar,引用了一个函数(即CarTypes(“Honda”));第三个元素,属性special,使用了一个已有的变量(即Sales)。
var Sales = "Toyota";
function CarTypes(name) {
return (name === "Honda") ?
name :
"Sorry, we don't sell " + name + "." ;
}
var car = { myCar: "Saturn", getCar: CarTypes("Honda"), special: Sales };
console.log(car.myCar); // Saturn
console.log(car.getCar); // Honda
console.log(car.special); // Toyota
更进一步的,你可以使用数字或字符串字面值作为属性的名字,或者在另一个字面值内嵌套上一个字面值。如下的示例中使用了这些可选项。
var car = { manyCars: {a: "Saab", "b": "Jeep"}, 7: "Mazda" };
console.log(car.manyCars.b); // Jeep
console.log(car[7]); // Mazda
对象属性名字可以是任意字符串,包括空串。如果对象属性名字不是合法的javascript标识符,它必须用"“包裹。属性的名字不合法,那么便不能用.访问属性值,而是通过类数组标记(”[]")访问和赋值。
var unusualPropertyNames = {
"": "An empty string",
"!": "Bang!"
}
console.log(unusualPropertyNames.""); // 语法错误: Unexpected string
console.log(unusualPropertyNames[""]); // An empty string
console.log(unusualPropertyNames.!); // 语法错误: Unexpected token !
console.log(unusualPropertyNames["!"]); // Bang!
增强的对象字面量
在ES2015,对象字面值扩展支持在创建时设置原型,简写了 foo: foo 形式的属性赋值,方法定义,支持父方法调用,以及使用表达式动态计算属性名。总之,这些也使对象字面值和类声明更加紧密地联系起来,让基于对象的设计从这些便利中更加受益。
var obj = {
// __proto__
__proto__: theProtoObj,
// Shorthand for ‘handler: handler’
handler,
// Methods
toString() {
// Super calls
return "d " + super.toString();
},
// Computed (dynamic) property names
[ 'prop_' + (() => 42)() ]: 42
};
请注意:
var foo = {a: "alpha", 2: "two"};
console.log(foo.a); // alpha
console.log(foo[2]); // two
//console.log(foo.2); // SyntaxError: missing ) after argument list
//console.log(foo[a]); // ReferenceError: a is not defined
console.log(foo["a"]); // alpha
console.log(foo["2"]); // two
PegExp字面值
一个正则表达式是字符被斜线(译注:正斜杠“/”)围成的表达式。下面是一个正则表达式文字的一个例子。
var re = /ab+c/;
字符串字面量(String literals)
字符串字面量是由双引号(")对或单引号(')括起来的零个或多个字符。字符串被限定在同种引号之间;也即,必须是成对单引号或成对双引号。下面的例子都是字符串字面值:
"foo"
'bar'
"1234"
"one line \n another line"
"John's cat"
你可以在字符串字面值上使用字符串对象的所有方法——JavaScript会自动将字符串字面值转换为一个临时字符串对象,调用该方法,然后废弃掉那个临时的字符串对象。你也能用对字符串字面值使用类似String.length的属性:
console.log("John's cat".length)
// 将打印字符串中的字符个数(包括空格)
// 结果为:10
在ES2015中,还提供了一种模板字面量(template literals),模板字符串提供了一些语法糖来帮你构造字符串。这与Perl、Python还有其他语言中的字符串插值(string interpolation)的特性非常相似。除此之外,你可以在通过模板字符串前添加一个tag来自定义模板字符串的解析过程,这可以用来防止注入攻击,或者用来建立基于字符串的高级数据抽象。
// Basic literal string creation
`In JavaScript '\n' is a line-feed.`
// Multiline strings
`In JavaScript this is
not legal.`
// String interpolation
var name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`
// Construct an HTTP request prefix is used to interpret the replacements and construction
POST`http://foo.org/bar?a=${a}&b=${b}
Content-Type: application/json
X-Credentials: ${credentials}
{ "foo": ${foo},
"bar": ${bar}}`(myOnReadyStateChangeHandler);
在字符串中使用的特殊字符
JavaScript 特殊字符
字符 意思
\0 Null字节
\b 退格符
\f 换页符
\n 换行符
\r 回车符
\t Tab (制表符)
\v 垂直制表符
’ 单引号
" 双引号
\ 反斜杠字符(\)
\XXX 由从0到377最多三位八进制数XXX表示的 Latin-1 字符。例如,\251是版权符号的八进制序列。
\xXX 由从00和FF的两位十六进制数字XX表示的Latin-1字符。例如,\ xA9是版权符号的十六进制序列。
\uXXXX 由四位十六进制数字XXXX表示的Unicode字符。例如,\ u00A9是版权符号的Unicode序列。见Unicode escape sequences (Unicode 转义字符).
\u{XXXXX} Unicode代码点 (code point) 转义字符。例如,\u{2F804} 相当于Unicode转义字符 \uD87E\uDC04的简写。
2.3流程控制与错误处理
1.语句块
分号(;)字符被用来分割语句。在JavaScript中,任何表达式(expression)都可以看作一条语句(statement)。
重要:在ECMAScript 6标准之前,Javascript没有块作用域。在一个块中引入的变量的作用域是包含函数或脚本,并且设置它们的效果会延续到块之外。换句话说,块语句不定义范围。JavaScript中的“独立”块会产生与C或Java中完全不同的结果。示例:
var x = 1;
{
var x = 2;
}
alert(x); // 输出的结果为 2
这段代码的输出是2,这是因为块级作用域中的 var x变量声明与之前的声明在同一个作用域内。在C语言或是Java语言中,同样的代码输出的结果是1。
从 ECMAScript 2015 开始,使用 let 和const变量是块作用域的。 更多信息请参考 let 和 const。
2.条件判断语句
支持两种
if…else
当一个逻辑条件为真,用if语句执行一个语句。当这个条件为假,使用可选择的 else 从句来执行这个语句。if 语句如下所示:
if (condition) {
statement_1;
}else {
statement_2;
} //推荐使用严格的语句块模式,语句else可选
你也可以组合语句通过使用 else if 来测试连续多种条件判断,就像下面一样:
if (condition_1) {
statement_1;
}else if (condition_2) {
statement_2;
}else if (condition_n_1) {
statement_n;
}else {
statement_last;
}
不建议在条件表达式中使用赋值语句,因为在快速查阅代码时容易把它看成等值比较。例如,不要使用下面的代码:
if(x = y){
/* 语句 */
}
如果你需要在条件表达式中使用赋值,通常在赋值语句前后额外添加一对括号。例如:
if ((x = y)) {
/* statements here */
}
错误的值 即被认为是false
false
undefined
null
0
NaN
空字符串(" ")
当传递给条件语句所有其他的值,包括所有对象会被计算为真 。
注意:请不要混淆原始的布尔值true和false 与 Boolean对象的真和假。例如:
var b = new Boolean(false);
if (b) //结果视为真
if (b == true) // 结果视为假
示例
function checkData() {
if (document.form1.threeChar.value.length == 3) {
return true;
} else {
alert("Enter exactly three characters. " +
document.form1.threeChar.value + " is not valid.");
return false;
}
}
switch
switch 语句允许一个程序求一个表达式的值并且尝试去匹配表达式的值到一个 case 标签。如果匹配成功,这个程序执行相关的语句。switch 语句如下所示:
switch (expression) {
case label_1:
statements_1
[break;]
case label_2:
statements_2
[break;]
...
default:
statements_def
[break;]
}
程序首先查找一个与 expression 匹配的 case 语句,然后将控制权转移到该子句,执行相关的语句。如果没有匹配值, 程序会去找 default 语句,如果找到了,控制权转移到该子句,执行相关的语句。如果没有找到 default,程序会继续执行 switch 语句后面的语句。default 语句通常出现在switch语句里的最后面,当然这不是必须的。
可选的 break 语句与每个 case 语句相关联, 保证在匹配的语句被执行后程序可以跳出 switch 并且继续执行 switch 后面的语句。如果break被忽略,则程序将继续执行switch语句中的下一条语句。
示例:
switch (fruittype) {
case "Oranges":
document.write("Oranges are $0.59 a pound.<br>");
break;
case "Apples":
document.write("Apples are $0.32 a pound.<br>");
break;
case "Bananas":
document.write("Bananas are $0.48 a pound.<br>");
break;
case "Cherries":
document.write("Cherries are $3.00 a pound.<br>");
break;
case "Mangoes":
case "Papayas":
document.write("Mangoes and papayas are $2.79 a pound.<br>");
break;
default:
document.write("Sorry, we are out of " + fruittype + ".<br>");
}
document.write("Is there anything else you'd like?<br>");
3.异常处理语句
你可以用 throw 语句抛出一个异常并且用 try…catch 语句捕获处理它。
异常类型:
JavaScript 可以抛出任意对象。然而,不是所有对象能产生相同的结果。尽管抛出数值或者字母串作为错误信息十分常见,但是通常用下列其中一种异常类型来创建目标更为高效:
ECMAScript exceptions
DOMException and DOMError
throw语句
使用throw语句抛出一个异常。当你抛出异常,你规定一个含有值的表达式要被抛出。
throw expression;
你可以抛出任意表达式而不是特定一种类型的表达式。下面的代码抛出了几个不同类型的表达式:
throw "Error2"; // String type
throw 42; // Number type
throw true; // Boolean type
throw {toString: function() { return "I'm an object!"; } };
你可以在抛出异常时声明一个对象。那你就可以在catch块中查询到对象的属性。
// Create an object type UserException
function UserException (message){
this.message=message;
this.name="UserException";
}
// Make the exception convert to a pretty string when used as
// a string (e.g. by the error console)
UserException.prototype.toString = function (){
return this.name + ': "' + this.message + '"';
}
// Create an instance of the object type and throw it
throw new UserException("Value too high");
try…catch 语句
try…catch 语句标记一块待尝试的语句,并规定一个以上的响应应该有一个异常被抛出。如果我们抛出一个异常,try…catch语句就捕获它。
try…catch 语句有一个包含一条或者多条语句的try代码块,0个或1个的catch代码块,catch代码块中的语句会在try代码块中抛出异常时执行。 换句话说,如果你在try代码块中的代码如果没有执行成功,那么你希望将执行流程转入catch代码块。如果try代码块中的语句(或者try 代码块中调用的方法)一旦抛出了异常,那么执行流程会立即进入catch 代码块。如果try代码块没有抛出异常,catch代码块就会被跳过。finally 代码块总会紧跟在try和catch代码块之后执行,但会在try和catch代码块之后的其他代码之前执行。
下面的例子使用了try…catch语句。示例调用了一个函数用于从一个数组中根据传递值来获取一个月份名称。如果该值与月份数值不相符,会抛出一个带有"InvalidMonthNo"值的异常,然后在捕捉块语句中设monthName变量为unknown。
function getMonthName(mo) {
mo = mo - 1; // Adjust month number for array index (1 = Jan, 12 = Dec)
var months = ["Jan","Feb","Mar","Apr","May","Jun","Jul",
"Aug","Sep","Oct","Nov","Dec"];
if (months[mo]) {
return months[mo];
} else {
throw "InvalidMonthNo"; //throw keyword is used here
}
}
try { // statements to try
monthName = getMonthName(myMonth); // function could throw exception
}
catch (e) {
monthName = "unknown";
logMyErrors(e); // pass exception object to error handler -> your own function
}
catch 块
你可以使用catch块来处理所有可能在try块中产生的异常
捕捉块指定了一个标识符(上述语句中的catchID)来存放抛出语句指定的值;你可以用这个标识符来获取抛出的异常信息。在插入throw块时JavaScript创建这个标识符;标识符只存在于catch块的存续期间里;当catch块执行完成时,标识符不再可用。
举个例子,下面代码抛出了一个异常。当异常出现时跳到catch块。
try {
throw "myException" // generates an exception
}
catch (e) {
// statements to handle any exceptions
logMyErrors(e) // pass exception object to error handler
}
finally块
finally块包含了在try和catch块完成后、下面接着try…catch的语句之前执行的语句。finally块无论是否抛出异常都会执行。如果抛出了一个异常,就算没有异常处理,finally块里的语句也会执行。
你可以用finally块来令你的脚本在异常发生时优雅地退出;举个例子,你可能需要在绑定的脚本中释放资源。接下来的例子用文件处理语句打开了一个文件(服务端的JavaScript允许你进入文件)。如果在文件打开时一个异常抛出,finally块会在脚本错误之前关闭文件。
openMyFile();
try {
writeMyFile(theData); //This may throw a error
}catch(e){
handleError(e); // If we got a error we handle it
}finally {
closeMyFile(); // always close the resource
}
如果finally块返回一个值,该值会是整个try-catch-finally流程的返回值,不管在try和catch块中语句返回了什么:
function f() {
try {
console.log(0);
throw "bogus";
} catch(e) {
console.log(1);
return true; // this return statement is suspended
// until finally block has completed
console.log(2); // not reachable
} finally {
console.log(3);
return false; // overwrites the previous "return"
console.log(4); // not reachable
}
// "return false" is executed now
console.log(5); // not reachable
}
f(); // console 0, 1, 3; returns false
用finally块覆盖返回值也适用于在catch块内抛出或重新抛出的异常:
function f() {
try {
throw 'bogus';
} catch(e) {
console.log('caught inner "bogus"');
throw e; // this throw statement is suspended until
// finally block has completed
} finally {
return false; // overwrites the previous "throw"
}
// "return false" is executed now
}
try {
f();
} catch(e) {
// this is never reached because the throw inside
// the catch is overwritten
// by the return in finally
console.log('caught outer "bogus"');
}
// OUTPUT
// caught inner "bogus"
嵌套 try…catch 语句
你可以嵌套一个或多个try … catch语句。如果一个内部try … catch语句没有catch块,它需要有一个finally块,并且封闭的try … catch语句的catch块被检查匹配
具体看官网
error对象
根据错误类型,你也许可以用’name’和’message’获取更精炼的信息。'name’提供了常规的错误类(如 ‘DOMException’ 或 ‘Error’),而’message’通常提供了一条从错误对象转换成字符串的简明信息。
在抛出你个人所为的异常时,为了充分利用那些属性(比如你的catch块不能分辨是你个人所为的异常还是系统的异常时),你可以使用 Error 构造函数。比如:
function doSomethingErrorProne () {
if (ourCodeMakesAMistake()) {
throw (new Error('The message'));
} else {
doSomethingToGetAJavascriptError();
}
}
....
try {
doSomethingErrorProne();
}
catch (e) {
console.log(e.name); // logs 'Error'
console.log(e.message); // logs 'The message' or a JavaScript error message)
}
2.4循环与迭代
for语句
形式
for ([initialExpression]; [condition]; [incrementExpression])
statement
当一个 for 循环执行的时候,会发生以下过程:
- 如果有初始化表达式 initialExpression,它将被执行。这个表达式通常会初始化一个或多个循环计数器,但语法上是允许一个任意复杂度的表达式的。这个表达式也可以声明变量。
- 计算 condition 表达式的值。如果 condition 的值是 true,循环中的语句会被执行。如果 condition 的值是 false,for 循环终止。如果 condition 表达式整个都被省略掉了,condition的值会被认为是true。
- 循环中的 statement 被执行。如果需要执行多条语句,可以使用块({ … })来包裹这些语句。
- .如果有更新表达式 incrementExpression,执行更新表达式。
- 回到步骤 2。
例子:下面的函数包含一个含有 for 循环去计算一个滑动列表中被选中项目的个数(一个 元素允许选择多项)。for 循环声明了变量i并将它的初始值设为 0。它检查 i 是否比 元素中的选项数量少,执行了随后的 if 语句,然后在每次完成循环后,i 的值增加 1。
<form name="selectForm">
<p>
<label for="musicTypes">Choose some music types, then click the button below:</label>
<select id="musicTypes" name="musicTypes" multiple="multiple">
<option selected="selected">R&B</option>
<option>爵士</option>
<option>布鲁斯</option>
<option>新纪元</option>
<option>古典</option>
<option>歌剧</option>
</select>
</p>
<p><input id="btn" type="button" value="选择了多少个选项?" /></p>
</form>
<script>
function howMany(selectObject) {
var numberSelected = 0;
for (var i = 0; i < selectObject.options.length; i++) {
if (selectObject.options[i].selected) {
numberSelected++;
}
}
return numberSelected;
}
var btn = document.getElementById("btn");
btn.addEventListener("click", function(){
alert('选择选项的数量是: ' + howMany(document.selectForm.musicTypes))
});
</script>
do…while
do…while 语句一直重复直到指定的条件求值得到假值(false)。 一个 do…while 语句看起来像这样:
do
statement
while (condition);
statement 在检查条件之前会执行一次。要执行多条语句(语句块),要使用块语句({ … })包括起来。 如果 condition 为真(true),statement 将再次执行。 在每个执行的结尾会进行条件的检查。当 condition 为假(false),执行会停止并且把控制权交回给 do…while 后面的语句。
实例
var i = 0;
do {
i += 1;
console.log(i);
} while (i < 5);
while
实例
var n = 0;
var x = 0;
while (n < 3) {
n++;
x += n;
}
label
一个 label 提供了一个让你在程序中其他位置引用它的标识符。例如,你可以用 label 标识一个循环, 然后使用 break 或者 continue 来指出程序是否该停止循环还是继续循环。
实例
在这个例子里,标记 markLoop 标识了一个 while 循环。
未添加 Label:
var num = 0;
for (var i = 0 ; i < 10 ; i++) { // i 循环
for (var j = 0 ; j < 10 ; j++) { // j 循环
if( i == 5 && j == 5 ) {
break; // i = 5,j = 5 时,会跳出 j 循环
} // 但 i 循环会继续执行,等于跳出之后又继续执行更多次 j 循环
num++;
}
}
alert(num); // 输出 95
添加 Label 后:
var num = 0;
outPoint:
for (var i = 0 ; i < 10 ; i++){
for (var j = 0 ; j < 10 ; j++){
if( i == 5 && j == 5 ){
break outPoint; // 在 i = 5,j = 5 时,跳出所有循环,
// 返回到整个 outPoint 下方,继续执行
}
num++;
}
}
alert(num); // 输出 55
使用 continue 语句,则可达到与未添加 label 相同的效果,但在这种有多层循环的情况下,循环的跳出进入流程更为明晰一些:
var num = 0;
outPoint:
for(var i = 0; i < 10; i++) {
for(var j = 0; j < 10; j++) {
if(i == 5 && j == 5) {
continue outPoint;
}
num++;
}
}
alert(num); // 95
从alert(num)的值可以看出,continue outPoint; 语句的作用是跳出当前循环,并跳转到outPoint(标签)下的 for 循环继续执行。
break
使用 break 语句来终止循环,switch, 或者是链接到 label 语句。
当你使用不带 label 的 break 时, 它会立即终止当前所在的 while,do-while,for,或者 switch 并把控制权交回这些结构后面的语句。
当你使用带 label 的 break 时,它会终止指定的带标记(label)的语句。
for (i = 0; i < a.length; i++) {
if (a[i] == theValue) {
break;
}
}
var x = 0;
var z = 0
labelCancelLoops: while (true) {
console.log("外部循环: " + x);
x += 1;
z = 1;
while (true) {
console.log("内部循环: " + z);
z += 1;
if (z === 10 && x === 10) {
break labelCancelLoops;
} else if (z === 10) {
break;
}
}
}
continue
continue 语句可以用来继续执行(跳过代码块的剩余部分并进入下一循环)一个 while、do-while、for,或者 label 语句。
当你使用不带 label 的 continue 时, 它终止当前 while,do-while,或者 for 语句到结尾的这次的循环并且继续执行下一次循环。
当你使用带 label 的 continue 时, 它会应用被 label 标识的循环语句。
var i = 0;
var n = 0;
while (i < 5) {
i++;
if (i == 3) {
continue;
}
n += i;
console.log(n);
}
//1,3,7,12
var i = 0;
var n = 0;
while (i < 5) {
i++;
if (i == 3) {
// continue;
}
n += i;
console.log(n);
}
// 1,3,6,10,15
一个被标签为 checkiandj 的语句包含了一个标签为 checkj 的语句。
如果遇到 continue 语句,程序会结束当前 chechj 的迭代并开始下一轮的迭代。
每次遇到 continue 语句时,checkj 语句会一直重复执行,直到 checkj 语句的条件为 false。
当返回 false 后,将会执行 checkiandj 的剩余语句,checkiandj 会一直执行,直到 checkiandj 的条件为 false。
当 checkiandj 的返回值为 false 时,将会执行 checkiandj 的下面的语句。
如果 continue 有一个标记 checkiandj, 程序将会从 checkiandj 语句块的顶部继续执行。
var i = 0;
var j = 10;
checkiandj:
while (i < 4) {
console.log(i);
i += 1;
checkj:
while (j > 4) {
console.log(j);
j -= 1;
if ((j % 2) == 0) {
continue checkj;
}
console.log(j + ' 是奇数。');
}
console.log('i = ' + i);
console.log('j = ' + j);
}
for…in
for…in 语句循环一个指定的变量来循环一个对象所有可枚举的属性。JavaScript 会为每一个不同的属性执行指定的语句。
下面的函数通过它的参数得到一个对象和这个对象的名字。然后循环这个对象的所有属性并且返回一个列出属性名和该属性值的字符串。
function dump_props(obj, obj_name) {
var result = "";
for (var i in obj) {
result += obj_name + "." + i + " = " + obj[i] + "<br>";
}
result += "<hr>";
return result;
}
对于一个拥有 make 和 model 属性的 car 对象来说,执行结果 result 是:
car.make = Ford
car.model = Mustang
数组
虽然使用 for…in 来迭代数组 Array 元素听起来很诱人,但是它返回的东西除了数字索引外,还有可能是你自定义的属性名字。因此还是用带有数字索引的传统的 for 循环来迭代一个数组比较好,因为,如果你想改变数组对象,比如添加属性或者方法,for…in 语句迭代的是自定义的属性,而不是数组的元素。(译者注:下面的 for…of 语句,和 forEach(),也是理想的选择。)
for…of
for…of 语句在可迭代对象(包括Array、Map、Set、arguments 等等)上创建了一个循环,对值的每一个独特属性调用一次迭代。
for (variable of object) {
statement
}
下面的这个例子展示了 for…of 和 for…in 两种循环语句之间的区别。 for…in 循环遍历的结果是数组元素的下标,而 for…of 遍历的结果是元素的值:
let arr = [3, 5, 7];
arr.foo = "hello";
for (let i in arr) {
console.log(i); // 输出 "0", "1", "2", "foo"
}
for (let i of arr) {
console.log(i); // 输出 "3", "5", "7"
}
// 注意 for...of 的输出没有出现 "hello"
// 官方文档不知为何在此使用三个空格来缩进…
2.5函数
函数是 JavaScript 中的基本组件之一。 一个函数是 JavaScript 过程 — 一组执行任务或计算值的语句。要使用一个函数,你必须将其定义在你希望调用它的作用域内。
一个JavaScript 函数用function关键字定义,后面跟着函数名和圆括号。
定义函数
函数声明
一个函数定义(也称为函数声明,或函数语句)由一系列的function关键字组成,依次为:
函数的名称。
函数参数列表,包围在括号中并由逗号分隔。
定义函数的 JavaScript 语句,用大括号{}括起来。
原始参数(比如一个具体的数字)被作为值传递给函数;值被传递给函数,如果被调用函数改变了这个参数的值,这样的改变不会影响到全局或调用函数。
如果你传递一个对象(即一个非原始值,例如Array或用户自定义的对象)作为参数,而函数改变了这个对象的属性,这样的改变对函数外部是可见的,如下面的例子所示:
function myFunc(theObject) {
theObject.make = "Toyota";
}
var mycar = {make: "Honda", model: "Accord", year: 1998};
var x, y;
x = mycar.make; // x获取的值为 "Honda"
myFunc(mycar);
y = mycar.make; // y获取的值为 "Toyota"
// (make属性被函数改变了)
函数表达式
虽然上面的函数声明在语法上是一个语句,但函数也可以由函数表达式创建。这样的函数可以是匿名的;它不必有一个名称。例如,函数square也可这样来定义:
const square = function(number) { return number * number; };
var x = square(4); // x gets the value 16
然而,函数表达式也可以提供函数名,并且可以用于在函数内部代指其本身,或者在调试器堆栈跟踪中识别该函数:
const factorial = function fac(n) {return n<2 ? 1 : n*fac(n-1)};
console.log(factorial(3));
当将函数作为参数传递给另一个函数时,函数表达式很方便。下面的例子演示了一个叫map的函数如何被定义,而后使用一个表达式函数作为其第一个参数进行调用:
function map(f, a) {
let result = []; // 创建一个数组
let i; // 声明一个值,用来循环
for (i = 0; i != a.length; i++)
result[i] = f(a[i]);
return result;
}
const f = function(x) {
return x * x * x;
}
let numbers = [0,1, 2, 5,10];
let cube = map(f,numbers);
console.log(cube);
返回 [0, 1, 8, 125, 1000]。
在 JavaScript 中,可以根据条件来定义一个函数。比如下面的代码,当num 等于 0 的时候才会定义 myFunc :
var myFunc;
if (num == 0){
myFunc = function(theObject) {
theObject.make = "Toyota"
}
}
当一个函数是一个对象的属性时,称之为方法。
调用函数
定义一个函数并不会自动的执行它。定义了函数仅仅是赋予函数以名称并明确函数被调用时该做些什么。调用函数才会以给定的参数真正执行这些动作。例如,一旦你定义了函数square,你可以如下这样调用它:
square(5);
函数一定要处于调用它们的域中,但是函数的声明可以被提升(出现在调用语句之后),如下例:
console.log(square(5));
/* ... */
function square(n) { return n*n }
函数域是指函数声明时的所在的地方,或者函数在顶层被声明时指整个程序。
备注:注意只有使用如上的语法形式(即 function funcName(){})才可以。而下面的代码是无效的。就是说,函数提升仅适用于函数声明,而不适用于函数表达式。
错误情况
console.log(square); // square is hoisted with an initial value undefined.
console.log(square(5)); // Uncaught TypeError: square is not a function
const square = function (n) {
return n * n;
}
函数的参数并不局限于字符串或数字。你也可以将整个对象传递给函数。函数 show_props(其定义参见 用对象编程)就是一个将对象作为参数的例子。
function factorial(n){
if ((n == 0) || (n == 1))
return 1;
else
return (n * factorial(n - 1));
}
var a, b, c, d, e;
a = factorial(1); // 1赋值给a
b = factorial(2); // 2赋值给b
c = factorial(3); // 6赋值给c
d = factorial(4); // 24赋值给d
e = factorial(5); // 120赋值给e
还有其它的方式来调用函数。常见的一些情形是某些地方需要动态调用函数,或者函数的实参数量是变化的,或者调用函数的上下文需要指定为在运行时确定的特定对象。显然,函数本身就是对象,因此这些对象也有方法(参考Function )。作为此中情形之一,apply()方法可以实现这些目的。
函数作用域
在函数内定义的变量不能在函数之外的任何地方访问,因为变量仅仅在该函数的域的内部有定义。相对应的,一个函数可以访问定义在其范围内的任何变量和函数。换言之,定义在全局域中的函数可以访问所有定义在全局域中的变量。在另一个函数中定义的函数也可以访问在其父函数中定义的所有变量和父函数有权访问的任何其他变量。
// 下面的变量定义在全局作用域(global scope)中
var num1 = 20,
num2 = 3,
name = "Chamahk";
// 本函数定义在全局作用域
function multiply() {
return num1 * num2;
}
multiply(); // 返回 60
// 嵌套函数的例子
function getScore() {
var num1 = 2,
num2 = 3;
function add() {
return name + " scored " + (num1 + num2);
}
return add();
}
getScore(); // 返回 "Chamahk scored 5"
作用域和函数堆栈
递归
一个函数可以指向并调用自身。有三种方法可以达到这个目的:
函数名
arguments.callee
作用域下的一个指向该函数的变量名
例如,思考一下如下的函数定义:
var foo = function bar() {
// statements go here
};
在这个函数体内,以下的语句是等价的:
bar()
arguments.callee() (注:ES5禁止在严格模式下使用此属性)
foo()
调用自身的函数我们称之为递归函数。在某种意义上说,递归近似于循环。两者都重复执行相同的代码,并且两者都需要一个终止条件(避免无限循环或者无限递归)。例如以下的循环:
var x = 0;
while (x < 10) { // "x < 10" 是循环条件
// do stuff
x++;
}
可以被转化成一个递归函数和对其的调用:
function loop(x) {
if (x >= 10) // "x >= 10" 是退出条件(等同于 "!(x < 10)")
return;
// 做些什么
loop(x + 1); // 递归调用
}
loop(0);
不过,有些算法并不能简单的用迭代来实现。例如,获取树结构中所有的节点时,使用递归实现要容易得多:
function walkTree(node) {
if (node == null) //
return;
// do something with node
for (var i = 0; i < node.childNodes.length; i++) {
walkTree(node.childNodes[i]);
}
}
跟loop函数相比,这里每个递归调用都产生了更多的递归。
将递归算法转换为非递归算法是可能的,不过逻辑上通常会更加复杂,而且需要使用堆栈。事实上,递归函数就使用了堆栈:函数堆栈。
这种类似堆栈的行为可以在下例中看到:
function foo(i) {
if (i < 0)
return;
console.log('begin:' + i);
foo(i - 1);
console.log('end:' + i);
}
foo(3);
// 输出:
// begin:3
// begin:2
// begin:1
// begin:0
// end:0
// end:1
// end:2
// end:3
嵌套函数和闭包
你可以在一个函数里面嵌套另外一个函数。嵌套(内部)函数对其容器(外部)函数是私有的。它自身也形成了一个闭包。一个闭包是一个可以自己拥有独立的环境与变量的表达式(通常是函数)。
既然嵌套函数是一个闭包,就意味着一个嵌套函数可以”继承“容器函数的参数和变量。换句话说,内部函数包含外部函数的作用域。
可以总结如下:
内部函数只可以在外部函数中访问。
内部函数形成了一个闭包:它可以访问外部函数的参数和变量,但是外部函数却不能使用它的参数和变量。
function addSquares(a, b) {
function square(x) {
return x * x;
}
return square(a) + square(b);
}
a = addSquares(2, 3); // returns 13
b = addSquares(3, 4); // returns 25
c = addSquares(4, 5); // returns 41
由于内部函数形成了闭包,因此你可以调用外部函数并为外部函数和内部函数指定参数:
function outside(x) {
function inside(y) {
return x + y;
}
return inside;
}
fn_inside = outside(3); // 可以这样想:给一个函数,使它的值加3
result = fn_inside(5); // returns 8
result1 = outside(3)(5); // returns 8
保存变量
注意到上例中 inside 被返回时 x 是怎么被保留下来的。一个闭包必须保存它可见作用域中所有参数和变量。因为每一次调用传入的参数都可能不同,每一次对外部函数的调用实际上重新创建了一遍这个闭包。只有当返回的 inside 没有再被引用时,内存才会被释放。
这与在其他对象中存储引用没什么不同,但是通常不太明显,因为并不能直接设置引用,也不能检查它们。
多层嵌套函数
函数可以被多层嵌套。例如,函数A可以包含函数B,函数B可以再包含函数C。B和C都形成了闭包,所以B可以访问A,C可以访问B和A。因此,闭包可以包含多个作用域;他们递归式的包含了所有包含它的函数作用域。这个称之为作用域链。(稍后会详细解释)
思考一下下面的例子:
function A(x) {
function B(y) {
function C(z) {
console.log(x + y + z);
}
C(3);
}
B(2);
}
A(1); // logs 6 (1 + 2 + 3)
在这个例子里面,C可以访问B的y和A的x。这是因为:
B形成了一个包含A的闭包,B可以访问A的参数和变量
C形成了一个包含B的闭包
B包含A,所以C也包含A,C可以访问B和A的参数和变量。换言之,C用这个顺序链接了B和A的作用域
反过来却不是这样。A不能访问C,因为A看不到B中的参数和变量,C是B中的一个变量,所以C是B私有的。
命名冲突
当同一个闭包作用域下两个参数或者变量同名时,就会产生命名冲突。更近的作用域有更高的优先权,所以最近的优先级最高,最远的优先级最低。这就是作用域链。链的第一个元素就是最里面的作用域,最后一个元素便是最外层的作用域。
看以下的例子:
function outside() {
var x = 5;
function inside(x) {
return x * 2;
}
return inside;
}
outside()(10); // returns 20 instead of 10
命名冲突发生在return x上,inside的参数x和outside变量x发生了冲突。这里的作用链域是{inside, outside, 全局对象}。因此inside的x具有最高优先权,返回了20(inside的x)而不是10(outside的x)。
闭包
闭包是 JavaScript 中最强大的特性之一。JavaScript 允许函数嵌套,并且内部函数可以访问定义在外部函数中的所有变量和函数,以及外部函数能访问的所有变量和函数。
但是,外部函数却不能够访问定义在内部函数中的变量和函数。这给内部函数的变量提供了一定的安全性。
此外,由于内部函数可以访问外部函数的作用域,因此当内部函数生存周期大于外部函数时,外部函数中定义的变量和函数的生存周期将比内部函数执行时间长。当内部函数以某一种方式被任何一个外部函数作用域访问时,一个闭包就产生了。
var pet = function(name) { //外部函数定义了一个变量"name"
var getName = function() {
//内部函数可以访问 外部函数定义的"name"
return name;
}
//返回这个内部函数,从而将其暴露在外部函数作用域
return getName;
};
myPet = pet("Vivie");
myPet(); // 返回结果 "Vi
实际上可能会比上面的代码复杂的多。在下面这种情形中,返回了一个包含可以操作外部函数的内部变量方法的对象
var createPet = function(name) {
var sex;
return {
setName: function(newName) {
name = newName;
},
getName: function() {
return name;
},
getSex: function() {
return sex;
},
setSex: function(newSex) {
if(typeof newSex == "string"
&& (newSex.toLowerCase() == "male" || newSex.toLowerCase() == "female")) {
sex = newSex;
}
}
}
}
var pet = createPet("Vivie");
pet.getName(); // Vivie
pet.setName("Oliver");
pet.setSex("male");
pet.getSex(); // male
pet.getName(); // Oliver
在上面的代码中,外部函数的name变量对内嵌函数来说是可取得的,而除了通过内嵌函数本身,没有其它任何方法可以取得内嵌的变量。内嵌函数的内嵌变量就像内嵌函数的保险柜。它们会为内嵌函数保留“稳定”——而又安全——的数据参与运行。而这些内嵌函数甚至不会被分配给一个变量,或者不必一定要有名字。
var getCode = (function(){
var secureCode = "0]Eal(eh&2"; // A code we do not want outsiders to be able to modify...
return function () {
return secureCode;
};
})();
getCode(); // Returns the secret code
备注:尽管有上述优点,使用闭包时仍然要小心避免一些陷阱。如果一个闭包的函数定义了一个和外部函数的某个变量名称相同的变量,那么这个闭包将无法引用外部函数的这个变量。
var createPet = function(name) { // Outer function defines a variable called "name"
return {
setName: function(name) { // Enclosed function also defines a variable called "name"
name = name; // ??? How do we access the "name" defined by the outer function ???
}
}
}
使用 arguments 对象
函数的实际参数会被保存在一个类似数组的arguments对象中。在函数内,你可以按如下方式找出传入的参数:
arguments[i]
其中i是参数的序数编号(译注:数组索引),以0开始。所以第一个传来的参数会是arguments[0]。参数的数量由arguments.length表示。
使用arguments对象,你可以处理比声明的更多的参数来调用函数。这在你事先不知道会需要将多少参数传递给函数时十分有用。你可以用arguments.length来获得实际传递给函数的参数的数量,然后用arguments对象来取得每个参数。
例如,设想有一个用来连接字符串的函数。唯一事先确定的参数是在连接后的字符串中用来分隔各个连接部分的字符(译注:比如例子里的分号“;”)。该函数定义如下:
function myConcat(separator) {
var result = ''; // 把值初始化成一个字符串,这样就可以用来保存字符串了!!
var i;
// iterate through arguments
for (i = 1; i < arguments.length; i++) {
result += arguments[i] + separator;
}
return result;
}
你可以给这个函数传递任意数量的参数,它会将各个参数连接成一个字符串“列表”:
// returns "red, orange, blue, "
myConcat(", ", "red", "orange", "blue");
// returns "elephant; giraffe; lion; cheetah; "
myConcat("; ", "elephant", "giraffe", "lion", "cheetah");
// returns "sage. basil. oregano. pepper. parsley. "
myConcat(". ", "sage", "basil", "oregano", "pepper", "parsley");
备注:arguments变量只是 ”类数组对象“,并不是一个数组。称其为类数组对象是说它有一个索引编号和length属性。尽管如此,它并不拥有全部的Array对象的操作方法。
函数参数
从ECMAScript 6开始,有两个新的类型的参数:默认参数,剩余参数。
默认参数
在JavaScript中,函数参数的默认值是undefined。然而,在某些情况下设置不同的默认值是有用的。这时默认参数可以提供帮助。
在过去,用于设定默认参数的一般策略是在函数的主体中测试参数值是否为undefined,如果是则赋予这个参数一个默认值。如果在下面的例子中,调用函数时没有实参传递给b,那么它的值就是undefined,于是计算a*b得到、函数返回的是 NaN。但是,在下面的例子中,这个已经被第二行获取处理:
function multiply(a, b) {
b = (typeof b !== 'undefined') ? b : 1;
return a*b;
}
multiply(5); // 5
使用默认参数,在函数体的检查就不再需要了。现在,你可以在函数头简单地把1设定为b的默认值:
function multiply(a, b = 1) {
return a*b;
}
multiply(5); // 5
剩余参数
剩余参数语法允许将不确定数量的参数表示为数组。在下面的例子中,使用剩余参数收集从第二个到最后参数。然后,我们将这个数组的每一个数与第一个参数相乘。这个例子是使用了一个箭头函数,这将在下一节介绍。
function multiply(multiplier, ...theArgs) {
return theArgs.map(x => multiplier * x);
}
var arr = multiply(2, 1, 2, 3);
console.log(arr); // [2, 4, 6]
箭头函数
箭头函数表达式(也称胖箭头函数)相比函数表达式具有较短的语法并以词法的方式绑定 this。箭头函数总是匿名的。另见 hacks.mozilla.org 的博文:“深度了解ES6:箭头函数”。
有两个因素会影响引入箭头函数:更简洁的函数和 this。
更简洁的函数
在一些函数模式中,更简洁的函数很受欢迎。对比一下:
var a = [
"Hydrogen",
"Helium",
"Lithium",
"Beryllium"
];
var a2 = a.map(function(s){ return s.length });
console.log(a2); // logs [ 8, 6, 7, 9 ]
var a3 = a.map( s => s.length );
console.log(a3); // logs [ 8, 6, 7, 9 ]
this 的词法
在箭头函数出现之前,每一个新函数都重新定义了自己的 this 值(在构造函数中是一个新的对象;在严格模式下是未定义的;在作为“对象方法”调用的函数中指向这个对象;等等)。以面向对象的编程风格,这样着实有点恼人。
function Person() {
// 构造函数Person()将`this`定义为自身
this.age = 0;
setInterval(function growUp() {
// 在非严格模式下,growUp()函数将`this`定义为“全局对象”,
// 这与Person()定义的`this`不同,
// 所以下面的语句不会起到预期的效果。
this.age++;
}, 1000);
}
var p = new Person();
在ECMAScript 3/5里,通过把this的值赋值给一个变量可以修复这个问题。
function Person() {
var self = this; // 有的人习惯用`that`而不是`self`,
// 无论你选择哪一种方式,请保持前后代码的一致性
self.age = 0;
setInterval(function growUp() {
// 以下语句可以实现预期的功能
self.age++;
}, 1000);
另外,创建一个约束函数可以使得 this值被正确传递给 growUp() 函数。
箭头函数捕捉闭包上下文的this值,所以下面的代码工作正常。
function Person(){
this.age = 0;
setInterval(() => {
this.age++; // 这里的`this`正确地指向person对象
}, 1000);
}
var p = new Person();
预定义函数
JavaScript语言有好些个顶级的内建函数:eval()
eval()方法会对一串字符串形式的JavaScript代码字符求值。
uneval() Non-Standard
uneval()方法创建的一个Object的源代码的字符串表示。
isFinite()
isFinite()函数判断传入的值是否是有限的数值。 如果需要的话,其参数首先被转换为一个数值。
isNaN()
isNaN()函数判断一个值是否是NaN。注意:isNaN函数内部的强制转换规则十分有趣; 另一个可供选择的是ECMAScript 6 中定义Number.isNaN() , 或者使用 typeof来判断数值类型。
parseFloat()
parseFloat() 函数解析字符串参数,并返回一个浮点数。
parseInt()
parseInt() 函数解析字符串参数,并返回指定的基数(基础数学中的数制)的整数。
decodeURI()
decodeURI() 函数对先前经过encodeURI函数或者其他类似方法编码过的字符串进行解码。
decodeURIComponent()
decodeURIComponent()方法对先前经过encodeURIComponent函数或者其他类似方法编码过的字符串进行解码。
encodeURI()
encodeURI()方法通过用以一个,两个,三个或四个转义序列表示字符的UTF-8编码替换统一资源标识符(URI)的某些字符来进行编码(每个字符对应四个转义序列,这四个序列组了两个”替代“字符)。
encodeURIComponent()
encodeURIComponent() 方法通过用以一个,两个,三个或四个转义序列表示字符的UTF-8编码替换统一资源标识符(URI)的每个字符来进行编码(每个字符对应四个转义序列,这四个序列组了两个”替代“字符)。