JavaScript数据类型与函数 | 青训营

99 阅读7分钟

一、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(类数组且可迭代的对象)也依然能够帮助我们获取函数调用中的所有参数。