本文主要介绍一下 JavaScript 的变量命名规则、简单使用方式及一些函数的使用。
# 命名规则
在 JavaScript 中,变量名称(包括函数名称)必须是有效的标识符。当你考虑使用 Unicode 等非传统字符时,标识符中有效字符约束规则可能会有点复杂。但是,如果你只考虑典型的 ASCII 中定义的字母、数字等其他字符时,则规则还是很简单的。
小测试,先看一下汉字命名是否可以。
(1)定义函数
function 哈哈(){ | |
return "123"; | |
} | |
var z = 哈哈(); | |
console.log(z); // "123" |
(2)定义变量
var 太阳大不大 = "大"; | |
console.info(太阳大不大); // "大" |
可见汉字命名是可以的,但是不推荐,原因大家都懂。
看一下具体的命名规则(适用于属性名称与变量标识符):
- 标识符必须以 a-z,A-Z,$ 或_开头。它可以包含任何这些字符加数字 0-9。
- 某些单词不能用作变量,但可以作为属性名称。这些单词被称为 “保留字”,包括 JS 关键字(for,in,if 等)以及 null,true 和 false。
# 变量作用域
就是简单讲,你定义一个 var 指定的变量,如果在函数内,则作用域就是当前函数,如果是所有函数外部,那么就是全局作用域。
# 变量提升(Hoisting)
无论 var 声明出现在哪一个范围内,该声明的变量被认为属于当前整个范围,并且在整个范围内都可以访问。 就是说无论在哪个位置定义变量,他都会提升到作用域最前端。
换句话说,当一个 var 声明在属性被 “移动” 到它的封闭范围的顶部时,这种行为就被称为提升。
可以看一个例子:
var a = 2; | |
foo(); // works because `foo()` | |
// declaration is "hoisted" | |
function foo() { | |
a = 3; | |
console.log( a ); // 3 | |
var a; // declaration is "hoisted" | |
// to the top of `foo()` | |
} | |
console.log( a ); // 2 |
注意,变量提升并不通用,也不是一个好主意,依靠变量提升来使变量的使用范围比其 var 变量声明的范围更早(会出现 undefined 等一系列不可预测问题),这可能会给程序制造混乱,难以理解。使用函数声明这种方式反而更为普遍和容易被接受,就像我们在正式声明之前出现的 foo()调用一样。
如果还是不理解什么是变量上升,请移步这里。
# 嵌套作用域
当你声明一个变量时,此时,这个变量就会在当前作用域中生效,包括当前作用域中的函数及其嵌套的函数,这类似一种继承关系,son 可以访问 foo,然而 foo 却不能访问 son
function foo() { | |
var a = 1; | |
function bar() { | |
var b = 2; | |
function baz() { | |
var c = 3; | |
console.log( a, b, c ); // 1 2 3 | |
} | |
baz(); | |
console.log( a, b ); // 1 2 | |
} | |
bar(); | |
console.log( a ); // 1 | |
} | |
foo(); |
如果 foo 想要访问 son,则会出现异常,但是有时我们想让 foo 访问 son 中变量,于是有了下面这种定义
function foo() { | |
a = 1; // `a` not formally declared | |
} | |
foo(); | |
a; // 1 -- oops, auto global variable :( |
这种单纯地想要实现可以访问的方式,非常 bad 的,虽然实现了,但是不提倡,是禁止的。记住,任何时刻,都要正式地声明你的变量。
除了在函数级别为变量创建声明外,ES6 还允许使用 let 关键字将变量声明为属于各个块({..})。除了一些微妙的细节之外,范围规则的行为与我们刚刚看到的函数大致相同:
function foo() { | |
var a = 1; | |
if (a >= 1) { | |
let b = 2; | |
while (b < 5) { | |
let c = b * 2; | |
b++; | |
console.log( a + c ); | |
} | |
} | |
} | |
foo(); // 5 7 9 |
如上,b 声明在 if 语句代码块,所以仅在 if 中生效,而 c 在 while 代码块中,固仅在 while 中生效,这是一种更细粒度的作用域控制,推荐使用,可以让你更好地管理你的变量。
# 条件语句
首先我们看一下 if else 语句,这个很常用,然而有时你会发现,如果条件很多,你不得不使用很多次 if..else..if...,如下
if (a == 2) { | |
// do something | |
} | |
else if (a == 10) { | |
// do another thing | |
} | |
else if (a == 42) { | |
// do yet another thing | |
} | |
else { | |
// fallback to here | |
} |
这时我们会觉得很啰嗦(verbose),此时可以尝试使用 switch
switch (a) { | |
case 2: | |
// do something | |
break; | |
case 10: | |
// do another thing | |
break; | |
case 42: | |
// do yet another thing | |
break; | |
default: | |
// fallback to here | |
} |
但是注意,在使用 switch 时,必要时必须存在 break 做结束,及时跳出,否则当你的条件满足第一个 case 时,她还会跳到下一个 case 中,这会执行下一个 case 的操作,会造成意外,但是某些时候我们会需要这样做,那便可以丢弃 break。
var a = 2; | |
switch (a) { | |
case 2: | |
console.log("haha"); | |
case 10: | |
console.log("hehe"); | |
break; | |
case 42: | |
console.log("xixi"); | |
break; | |
default: | |
console.log("你笑啥"); | |
} |
上面的例子会打印俩值,一个是 "haha", 一个是 "hehe",
当然,JavaScript 中还有一种三元运算符,即
var a = 42; | |
var b = (a > 41) ? "hello" : "world"; | |
// similar to: | |
// if (a > 41) { | |
// b = "hello"; | |
// } | |
// else { | |
// b = "world"; | |
// } |
条件运算符不一定是在赋值中使用,但这绝对是最常用的用法。
# 严格模式(Strict Mode)
ES5 为语言增加了一种 “严格模式”,从而加强了某些行为的规则。一般来说,这些限制被认为是保持代码更安全和更合适的方式。
另外,遵循严格的模式使得你的代码通常可以被引擎更好地优化。
严格模式是代码的一大改进,我们应该将其用于所有程序。 我们可以选择严格模式来执行单个函数或整个文件,具体取决于编译指示的位置:
function foo() { | |
"use strict"; | |
// this code is strict mode | |
function bar() { | |
// this code is strict mode | |
} | |
} | |
// this code is not strict mode |
和下面比较一下
"use strict"; | |
function foo() { | |
// this code is strict mode | |
function bar() { | |
// this code is strict mode | |
} | |
} | |
// this code is strict mode |
严格模式不允许省略 var 而直接定义全局变量,如下会提示异常
function foo() { | |
"use strict"; // turn on strict mode | |
a = 1; // `var` missing, ReferenceError | |
} | |
foo(); |
严格模式是 JavaScript 未来发展的方向,一定要重视在使用严格模式时遇到的异常,因为这些都是以后 JavaScript 将要改变的。
# 将函数作为变量值
看两个例子
var foo = function() { | |
// .. | |
}; | |
var x = function bar(){ | |
// .. | |
}; |
上面两个例子有区别,第一个 foo 指向的是匿名函数,而后一个不是,需要注意的是,尽管匿名函数赋值的情况还是比较多,但我们更推荐为其提供指定命名的赋值方式,也就是第二种。
# 立即执行函数表达式(Immediately Invoked Function Expressions (IIFEs))
或称立即执行函数,就是说直接可以自动执行,并输出或提供一个信息反馈。如
(function foo(){ | |
console.log( "Hello!" ); | |
})(); |
上面函数执行后,会直接打印值 "Hello!"
然而普通的函数呢?
function foo(){ | |
console.log("Hello!"); | |
} |
她是不会自己去执行的,需要赋予一个变量,或者等待着被调用。
可以对比一下下面的普通函数与立即执行函数:
function foo() { .. } | |
// `foo` function reference expression, | |
// then `()` executes it | |
foo(); | |
// `IIFE` function expression, | |
// then `()` executes it | |
(function IIFE(){ .. })(); |
在变量作用域上,立即执行函数中定义的变量和外部变量是相互独立的,互不影响
var a = 42; | |
(function IIFE(){ | |
var a = 10; | |
console.log( a ); // 10 | |
})(); | |
console.log( a ); // 42 |
立即执行函数可以包含返回值。
var x = (function IIFE(){ | |
return 42; | |
})(); | |
x; // 42 |
# 闭包(Closure)
正常情况下,函数内部可以直接读取全局变量,而在函数外部自然无法读取函数内的局部变量(除了没有 var 修饰的全局变量),而我们有时后又需要从外部获取某个局部变量,那么我们如何从外部读取局部变量呢?
我们可以尝试着做这样一个操作:在函数的内部,再定义一个函数
为什么呢?
先看个例子:
function foo(){ | |
var m = 123; | |
function bar(){ | |
return m; | |
} | |
return bar; | |
} | |
var result = foo(); | |
result(); |
上面 foo 函数中定义的 bar 函数,其实就是一个闭包。
大家可以将闭包看作是一种 “被记忆” 的方式,即使函数运行完毕,也可以继续访问函数的作用域(变量)。有时候这还是会让我们很疑惑,可以再看一下下面的例子:
function makeAdder(x) { | |
// parameter `x` is an inner variable | |
// inner function `add()` uses `x`, so | |
// it has a "closure" over it | |
function add(y) { | |
return y + x; | |
}; | |
return add; | |
} | |
// `plusOne` gets a reference to the inner `add(..)` | |
// function with closure over the `x` parameter of | |
// the outer `makeAdder(..)` | |
var plusOne = makeAdder( 1 ); | |
// `plusTen` gets a reference to the inner `add(..)` | |
// function with closure over the `x` parameter of | |
// the outer `makeAdder(..)` | |
var plusTen = makeAdder( 10 ); | |
plusOne( 3 ); // 4 <-- 1 + 3 | |
plusOne( 41 ); // 42 <-- 1 + 41 | |
plusTen( 13 ); // 23 <-- 10 + 13 |
当你执行过后,发现 plusOne (3) 的值为 4,plusOne ( 41 ) 的值是 42,plusTen ( 13 ) 的值是 23,
- 当我们调用 makeAdder(1)时,我们返回一个对它的内部 add(..)的引用,它将 x 记为 1. 我们把这个函数引用称为 plusOne(..)。
- 当我们调用 makeAdder(10)时,我们得到另一个它的内部 add(..)的引用,它将 x 记为 10. 我们称这个函数为引用 plusTen(..)。
- 当我们调用 plusOne(3)时,它将 3(它的内部 y)加到 1(被 x 记住),我们得到 4 结果。
- 当我们呼叫加十(13)时,它将十三(它的内部 y)加到 10(被 x 记住),结果我们得到 23。
闭包有下面两个用途,第一个我们在上面已经说明了:
- 一个是前面提到的可以读取函数内部的变量
- 另一个就是让这些变量的值始终保持在内存中(小心也会内存泄漏哦)
如果还是不理解,可以参考阮一峰大佬的博客。
# 模块化
先看一个例子
function User(){ | |
var username, password; | |
function doLogin(user,pw) { | |
username = user; | |
password = pw; | |
// do the rest of the login work | |
} | |
var publicAPI = { | |
login: doLogin | |
}; | |
return publicAPI; | |
} | |
// create a `User` module instance | |
var fred = User(); | |
fred.login( "fred", "12Battery34!" ); |
User()函数作为一个供外部调用的函数,它保存着变量的用户名和密码,以及内部的 doLogin()函数。这些都是该用户模块的内部细节,无法从外部访问。
警告:
- 我们不打算在这里调用并产生一个 new User(),尽管事实上大多数读者可能看起来更常见。
- User()只是一个函数,不是要实例化的类,所以它只能算是正常调用。使用 new 是不合适的,并且浪费资源。
- 我们首先执行 User(),此时会创建 User 函数的一个实例 - 创建一个全新的作用域,从而创建每个存在此作用域的内部变量 / 函数的全新副本。我们将这个实例分配给 fred。如果我们再次运行 User(),我们会得到一个完全独立于 fred 的新实例,这是不可取的。
- 内部的 doLogin()函数有一个关于用户名和密码的闭包,这意味着即使在 User()函数完成运行后,它仍然保留对它们的访问。
- publicAPI 是一个拥有一个属性 / 方法的对象,login 是一个对 doLogin()函数的引用。当我们从 User()返回 publicAPI 时,它成为我们调用的 fred 的实例。 此时,外部的 User()函数已经完成执行。
- 通常情况下,你会认为像用户名和密码的内部变量已经消失。但是在这里他们没有,因为在 login()函数中有一个闭包让它们活着。 这就是为什么我们可以调用 fred.login(..) - 就像调用内部的 doLogin(..)一样 - 它仍然可以访问用户名和密码的内部变量。
# this 标识
在 JavaScript 中,如果一个函数里面有这个引用,那么这个引用通常指向一个对象。但是指向哪个对象取决于函数的具体调用方式。
如:
function foo() { | |
console.log( this.bar ); | |
} | |
var bar = "global"; | |
var obj1 = { | |
bar: "obj1", | |
foo: foo | |
}; | |
var obj2 = { | |
bar: "obj2" | |
}; | |
// -------- | |
foo(); // "global" | |
obj1.foo(); // "obj1" | |
foo.call( obj2 ); // "obj2" | |
new foo(); // undefined |
下面解释一下代码:
- 这段代码中,foo()的最终设置为非严格模式下的全局对象,所以 “global” 是此调用时的结果。而如果在严格模式下,this.bar 其实是 undefined 的,并且在访问 bar 属性时会出错 。
- obj1.foo()将其设置为 obj1 对象。
- foo.call(obj2)将其设置为 obj2 对象。
- new foo()将其设置为一个全新的空对象。
# 原型机制
JavaScript 中的原型机制比较复杂,我们这里只瞄一眼即可。
假设我们拥有一个对象,当我们在对象上引用另一个属性时,如果该属性不存在,JavaScript 将自动使用该对象的内部原型引用来查找另一个对象以查找属性。当属性缺失时,我们可以把其看作是后备资源。
考虑下下面这个例子:
var foo = { | |
a: 42 | |
}; | |
// create `bar` and link it to `foo` | |
var bar = Object.create( foo ); | |
bar.b = "hello world"; | |
bar.b; // "hello world" | |
bar.a; // 42 <-- delegated to `foo` |
可以想象成下面这幅图去理解
a 属性实际上并不存在于 bar 对象上,但是因为 bar 是与 foo 原型链接的,所以 JavaScript 会自动回退到在 foo 对象上寻找它的位置。
这种联系可能看起来很奇怪。这种方式引发的最常见的弊端就是滥用 - 就是试图用 “继承” 模仿 / 伪造一个 “类” 机制。
但更自然的应用原型机制的方式是一种称为 “行为委托” 的模式,在这种模式中,你可以特意设计你的链接对象,以便能够将所需行为的一部分委托给另一个。