一、JavaScript数据类型
原始类型是不可改变的,而对象类型则是可变的
变量
var关键字
var关键字的意义是值绑定,而非赋值。
同时var关键字存在变量提升的现象,即通过var关键字声明的变量会先声明为undefined,而后初始化。
let和const关键字
let和const关键字不存在变量提升的现象。
作用域
所有“var 声明”和函数声明的标识符都登记为 varNames,使用“变量作用域”管理;
其它情况下的标识符 / 变量声明,都作为 lexicalNames 登记,使用“词法作用域”管理。
Symbol类型
根据规范,只有两种原始类型可以用作对象属性键:
- 字符串类型
- symbol 类型
否则,如果使用另一种类型,例如数字,它会被自动转换为字符串。所以 obj[1] 与 obj["1"] 相同,而 obj[true] 与 obj["true"] 相同。
symbol 是带有可选描述的“原始唯一值”。
let id1 = Symbol("id");
let id2 = Symbol("id");
alert(id1 == id2); // false
注意:symbol 不会被自动转换为字符串
JavaScript 中的大多数值都支持字符串的隐式转换。例如,我们可以 alert 任何值,都可以生效。symbol 比较特殊,它不会被自动转换。
如果我们真的想显示一个 symbol,我们需要在它上面调用 .toString(),如下所示:
let id = Symbol("id");
alert(id.toString()); // Symbol(id),现在它有效了
或者获取 symbol.description 属性,只显示描述(description):
let id = Symbol("id");
alert(id.description); // id
隐藏属性
symbol 允许我们创建对象的“隐藏”属性,代码的任何其他部分都不能意外访问或重写这些属性。
let user = { // 属于另一个代码
name: "John"
};
let id = Symbol("id");
user[id] = 1;
alert( user[id] ); // 我们可以使用 symbol 作为键来访问数据
使用 Symbol("id") 作为键,比起用字符串 "id" 来有什么好处呢?
由于 user 对象属于另一个代码库,所以向它们添加字段是不安全的,因为我们可能会影响代码库中的其他预定义行为。但 symbol 属性不会被意外访问到。第三方代码不会知道新定义的 symbol,因此将 symbol 添加到 user 对象是安全的。
如果我们要在对象字面量 {...} 中使用 symbol,则需要使用方括号把它括起来。
这是因为我们需要变量 id 的值作为键,而不是字符串 “id”。
let id = Symbol("id");
let user = {
name: "John",
[id]: 123 // 而不是 "id":123
};
symbol 属性不参与 for..in 循环。
Object.keys()方法 也会忽略它们。这是一般“隐藏符号属性”原则的一部分。如果另一个脚本或库遍历我们的对象,它不会意外地访问到符号属性。
相反,Object.assign()方法会同时复制字符串和 symbol 属性:
let id = Symbol("id");
let user = {
[id]: 123
};
let clone = Object.assign({}, user);
alert( clone[id] ); // 123
这里并不矛盾,就是这样设计的。这里的想法是当我们克隆或者合并一个 object 时,通常希望 所有 属性被复制(包括像 id 这样的 symbol)。
全局Symbol
正如我们所看到的,通常所有的 symbol 都是不同的,即使它们有相同的名字。但有时我们想要名字相同的 symbol 具有相同的实体。例如,应用程序的不同部分想要访问的 symbol "id" 指的是完全相同的属性。
为了实现这一点,全局symbol注册表诞生了。我们可以在其中创建 symbol 并在稍后访问它们,它可以确保每次访问相同名字的 symbol 时,返回的都是相同的 symbol。
要从注册表中读取(不存在则创建)symbol,请使用 Symbol.for(key)。
该调用会检查全局注册表,如果有一个描述为 key 的 symbol,则返回该 symbol,否则将创建一个新 symbol(Symbol(key)),并通过给定的 key 将其存储在注册表中。
注册表内的 symbol 被称为 全局 symbol。如果我们想要一个应用程序范围内的 symbol,可以在代码中随处访问 —— 这就是它们的用途。
// 从全局注册表中读取
let id = Symbol.for("id"); // 如果该 symbol 不存在,则创建它
// 再次读取(可能是在代码中的另一个位置)
let idAgain = Symbol.for("id");
// 相同的 symbol
alert( id === idAgain ); // true
我们已经看到,对于全局 symbol,Symbol.for(key) 按名字返回一个 symbol。相反,通过全局 symbol 返回一个名字,我们可以使用 Symbol.keyFor(sym):
// 通过 name 获取 symbol
let sym = Symbol.for("name");
let sym2 = Symbol.for("id");
// 通过 symbol 获取 name
alert( Symbol.keyFor(sym) ); // name
alert( Symbol.keyFor(sym2) ); // id
Symbol.keyFor 内部使用全局 symbol 注册表来查找 symbol 的键。所以它不适用于非全局 symbol。如果 symbol 不是全局的,它将无法找到它并返回 undefined。
也就是说,所有 symbol 都具有 description 属性。
二、JavaScript函数
在JavaScript中,函数是一种特殊的对象,它和对象一样可以拥有属性和值,但是函数和普通对象不同的是,函数可以被调用。
函数声明与函数表达式
- 函数声明
function say(){
...
}
say()
- 函数表达式
let say=function(){
...
}
say()
函数特性
函数将从内到外依次在对应的词法环境中寻找目标变量,它使用最新的值。
旧变量值不会保存在任何地方。当一个函数想要一个变量时,它会从自己的词法环境或外部词法环境中获取当前值。
let name = "John";
function sayHi() {
alert("Hi, " + name);
}
name = "Pete";
sayHi(); //Pete
function makeWorker() {
let name = "Pete";
return function() {
alert(name);
};
}
let name = "John";
// 创建一个函数
let work = makeWorker();
// 调用它
work(); // 会显示什么?
还可以立即调用函数(IIFE):
(function (){
var test = 1
console.log(test)
})()
所以这里的结果是 "Pete"。
但如果在 makeWorker() 中没有 let name,那么将继续向外搜索并最终找到全局变量,正如我们可以从上图中看到的那样。在这种情况下,结果将是 "John"。
…语法
Rest参数
我们可以在函数定义中声明一个数组来收集参数。语法是这样的:...变量名,这将会声明一个数组并指定其名称,其中存有剩余的参数。这三个点的语义就是“收集剩余的参数并存进指定数组中”。
function sumAll(...args) { // 数组名为 args
let sum = 0;
for (let arg of args) sum += arg;
return sum;
}
alert( sumAll(1) ); // 1
alert( sumAll(1, 2) ); // 3
alert( sumAll(1, 2, 3) ); // 6
我们也可以选择将第一个参数获取为变量,并将剩余的参数收集起来。
下面的例子把前两个参数获取为变量,并把剩余的参数收集到 titles 数组中:
function showName(firstName, lastName, ...titles) {
alert( firstName + ' ' + lastName ); // Julius Caesar
// 剩余的参数被放入 titles 数组中
// i.e. titles = ["Consul", "Imperator"]
alert( titles[0] ); // Consul
alert( titles[1] ); // Imperator
alert( titles.length ); // 2
}
showName("Julius", "Caesar", "Consul", "Imperator");
Rest 参数必须放到参数列表的末尾。
arguments类数组对象
有一个名为 arguments 的特殊类数组对象可以在函数中被访问,该对象以参数在参数列表中的索引作为键,存储所有参数。
例如:
function showName() {
alert( arguments.length );
alert( arguments[0] );
alert( arguments[1] );
// 它是可遍历的
// for(let arg of arguments) alert(arg);
}
// 依次显示:2,Julius,Caesar
showName("Julius", "Caesar");
// 依次显示:1,Ilya,undefined(没有第二个参数)
showName("Ilya");
Spread语法
Spread 语法 看起来和 rest 参数很像,也使用 ...,但是二者的用途完全相反。
当在函数调用中使用 ...arr 时,它会把可迭代对象 arr “展开” (打平)到参数列表中。
let str = "Hello";
alert( [...str] ); // H,e,l,l,o
Spread 语法内部使用了迭代器来收集元素,与 for..of 的方式相同。
因此,对于一个字符串,for..of 会逐个返回该字符串中的字符,...str 也同理会得到 "H","e","l","l","o" 这样的结果。随后,字符列表被传递给数组初始化器 [...str]。
对于这个特定任务,我们还可以使用 Array.from 来实现,因为该方法会将一个可迭代对象(如字符串)转换为数组:
let str = "Hello";
// Array.from 将可迭代对象转换为数组
alert( Array.from(str) ); // H,e,l,l,o
不过 Array.from(obj) 和 [...obj] 存在一个细微的差别:
Array.from适用于类数组对象也适用于可迭代对象。- Spread 语法只适用于可迭代对象。
因此,对于将一些“东西”转换为数组的任务,Array.from 往往更通用。
使用场景:
- Rest 参数用于创建可接受任意数量参数的函数。
- Spread 语法用于将数组传递给通常需要含有许多参数的函数。
我们可以使用这两种语法轻松地互相转换列表与参数数组。
旧式的 arguments(类数组且可迭代的对象)也依然能够帮助我们获取函数调用中的所有参数。