var、let、const
作用域:var 声明的变量是函数作用域或全局作用域。如果在函数内部声明,它的作用域是整个函数;如果在函数外部声明,它的作用域是全局的。
变量提升:var 声明的变量会被提升。可以在声明之前使用,但值是 undefined。
可重复声明:同一作用域内可以多次使用 var 声明同一个变量。
作用域:let 声明的变量是块作用域。
变量提升:let 声明的变量不会被提升。
不可重复声明:同一作用域内不能多次使用 let 声明同一个变量。
作用域:const 声明的变量也是块作用域。
变量提升:const 声明的变量不会被提升。
不可重复声明:同一作用域内不能多次使用 const 声明同一个变量。
不可重新赋值:const 声明的变量必须在声明时初始化,并且不能重新赋值。不过,如果 const 声明的是一个对象或数组,其内容是可以改变的。
函数作用域
### 定义
作用域(scope)指的是变量存在的范围。在 ES5 的规范中,JavaScript 只有两种作用域:一种是全局作用域,变量在整个程序中一直存在,所有地方都可以读取;另一种是函数作用域,变量只在函数内部存在。
函数内部定义的变量,会在该作用域内覆盖同名全局变量。
var v = 1;
function f(){
var v = 2;
console.log(v);
}
f() // 2
v // 1
上面代码中,变量`v`同时在函数的外部和内部有定义。结果,在函数内部定义,局部变量`v`覆盖了全局变量`v`。
注意,对于`var`命令来说,局部变量只能在函数内部声明,在其他区块中声明,一律都是全局变量。
if (true) {
var x = 5;
}
console.log(x); // 5
上面代码中,变量`x`在条件判断区块之中声明,结果就是一个全局变量,可以在区块之外读取。
函数本身也是一个值,也有自己的作用域。它的作用域与变量一样,就是其声明时所在的作用域,与其运行时所在的作用域无关。
var a = 1;
var x = function () {
console.log(a);
};
function f() {
var a = 2;
x();
}
f() // 1
上面代码中,函数`x`是在函数`f`的外部声明的,所以它的作用域绑定外层,内部变量`a`不会到函数`f`体内取值,所以输出`1`,而不是`2`。
总之,函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域。
参数
js里的函数的参数,不用跟实际传过来的值一一对应
### arguments 对象
由于 JavaScript 允许函数有不定数目的参数,所以需要一种机制,可以在函数体内部读取所有参数。这就是`arguments`对象的由来。
`arguments`对象包含了函数运行时的所有参数,`arguments[0]`就是第一个参数,`arguments[1]`就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用
var f = function (one) {
console.log(arguments[0]);
console.log(arguments[1]);
console.log(arguments[2]);
}
f(1, 2, 3)
需要注意的是,虽然`arguments`很像数组,但它是一个对象。数组专有的方法(比如`slice`和`forEach`),不能在`arguments`对象上直接使用。
如果要让`arguments`对象使用数组方法,真正的解决方法是将`arguments`转为真正的数组。
关于slice函数的语法
let fruits = ['apple', 'banana', 'cherry', 'date', 'fig'];
// 提取从索引1开始到索引3的元素
let slicedFruits = fruits.slice(1, 3); // 返回 ["banana", "cherry"]
// 如果不指定end参数,则提取从start到数组末尾的元素
let slicedFruitsToEnd = fruits.slice(1); // 返回 ["banana", "cherry", "date", "fig"]
// 如果start参数是负数,则表示从数组末尾开始计算的位置
let slicedFruitsFromEnd = fruits.slice(-3); // 返回 ["date", "fig"]
// 如果省略start和end参数,则返回数组的一个完整副本
let fruitsCopy = fruits.slice(); // 返回 ["apple", "banana", "cherry", "date", "fig"]
语句与表达式的区别
##### 语句(Statement)
语句是执行动作的指令。它告诉JavaScript执行某些操作,通常会有副作用(即改变程序的状态或环境)。语句的结束通常需要一个分号(`;`),但这不是强制性的,因为在JavaScript中,分号是语句的可选结束符。例如:
// 赋值语句
let x = 10;
// 函数调用语句
doSomething();
// 控制流语句
if (condition) {
// ...
}
// 循环语句
for (let i = 0; i < 10; i++) {
// ...
}
##### 表达式(Expression)
表达式是一个计算并返回值的代码片段。它可以是一个变量、一个函数调用、一个算术运算或者任何能够产生值的组合。表达式可以是语句的一部分,但单独的表达式不能作为语句存在(除非它是返回值的函数或语句末尾有分号)。 例如:
// 变量名是一个表达式
let y = 20;
// 算术运算是一个表达式
let sum = 10 + 5; // 这里的 10 + 5 是一个表达式
// 函数调用也是一个表达式,特别是当它返回一个值时
let result = doSomething(); // doSomething() 是一个返回值的表达式
立即调用的函数表达式(IIFE)
根据 JavaScript 的语法,圆括号`()`跟在函数名之后,表示调用该函数。比如,`print()`就表示调用`print`函数。
有时,我们需要在定义函数之后,立即调用该函数。这时,你不能在函数的定义之后加上圆括号,这会产生语法错误。
function(){ /* code */ }();
// SyntaxError: Unexpected token ()
产生这个错误的原因是,`function`这个关键字既可以当作语句,也可以当作表达式。
// 语句
function f() {}
// 表达式
var f = function f() {}
当作表达式时,函数可以定义后直接加圆括号调用。
var f = function f(){ return 1}();
f // 1
上面的代码中,函数定义后直接加圆括号调用,没有报错。原因就是`function`作为表达式,引擎就把函数定义当作一个值。这种情况下,就不会报错。
为了避免解析的歧义,JavaScript 规定,如果`function`关键字出现在行首,一律解释成语句。因此,引擎看到行首是`function`关键字之后,认为这一段都是函数的定义,不应该以圆括号结尾,所以就报错了。
函数定义后立即调用的解决方法,就是不要让`function`出现在行首,让引擎将其理解成一个表达式。最简单的处理,就是将其放在一个圆括号里面。
(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();
上面两种写法都是以圆括号开头,引擎就会认为后面跟的是一个表达式,而不是函数定义语句,所以就避免了错误。这就叫做“立即调用的函数表达式”(Immediately-Invoked Function Expression),简称 IIFE。
注意,上面两种写法最后的分号都是必须的。如果省略分号,遇到连着两个 IIFE,可能就会报错。
// 报错
(function(){ /* code */ }())
(function(){ /* code */ }())
上面代码的两行之间没有分号,JavaScript 会将它们连在一起解释,将第二行解释为第一行的参数。
推而广之,任何让解释器以表达式来处理函数定义的方法,都能产生同样的效果,比如下面三种写法。
var i = function(){ return 10; }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();
甚至像下面这样写,也是可以的。
!function () { /* code */ }();
~function () { /* code */ }();
-function () { /* code */ }();
+function () { /* code */ }();
(function () {
var tmp = newData;
processData(tmp);
storeData(tmp);
}());
eval 命令
`eval`命令接受一个字符串作为参数,并将这个字符串当作语句执行。
eval('var a = 1;');
a // 1
数组的本质
本质上,数组属于一种特殊的对象。`typeof`运算符会返回数组的类型是`object`。
typeof [1, 2, 3] // "object"
但是,对于数值的键名,不能使用点结构
var arr = [1, 2, 3];
arr.0 // SyntaxError
上面代码中,`arr.0`的写法不合法,因为单独的数值不能作为标识符(identifier)。所以,数组成员只能用方括号`arr[0]`表示(方括号是运算符,可以接受数值)。
length 属性
数组的`length`属性,返回数组的成员数量。
['a', 'b', 'c'].length // 3
JavaScript 使用一个32位整数,保存数组的元素个数。这意味着,数组成员最多只有 4294967295 个(232 - 1)个,也就是说`length`属性的最大值就是 4294967295。
只要是数组,就一定有`length`属性。该属性是一个动态的值,等于键名中的最大整数加上`1`。
var arr = ['a', 'b'];
arr.length // 2
arr[2] = 'c';
arr.length // 3
arr[9] = 'd';
arr.length // 10
arr[1000] = 'e';
arr.length // 1001
上面代码表示,数组的数字键不需要连续,`length`属性的值总是比最大的那个整数键大`1`。另外,这也表明数组是一种动态的数据结构,可以随时增减数组的成员。
`length`属性是可写的。如果人为设置一个小于当前成员个数的值,该数组的成员数量会自动减少到`length`设置的值。
in 运算符
检查某个键名是否存在的运算符`in`,适用于对象,也适用于数组。
var arr = [ 'a', 'b', 'c' ];
2 in arr // true
'2' in arr // true
4 in arr // false
上面代码表明,数组存在键名为`2`的键。由于键名都是字符串,所以数值`2`会自动转成字符串。
注意,如果数组的某个位置是空位,`in`运算符返回`false`。
var arr = [];
arr[100] = 'a';
100 in arr // true
1 in arr // false
上面代码中,数组`arr`只有一个成员`arr[100]`,其他位置的键名都会返回`false`。
数组遍历
var a = [1, 2, 3];
// for循环
for(var i = 0; i < a.length; i++) {
console.log(a[i]);
}
// while循环
var i = 0;
while (i < a.length) {
console.log(a[i]);
i++;
}
var l = a.length;
while (l--) {
console.log(a[l]);
}
上面代码是三种遍历数组的写法。最后一种写法是逆向遍历,即从最后一个元素向第一个元素遍历。
数组的`forEach`方法,也可以用来遍历数组
var colors = ['red', 'green', 'blue'];
colors.forEach(function (color) {
console.log(color);
});
// red
// green
// blue