JavaScript


本文引用自 https://zh.javascript.info/

一、基本数据类型

JS中一共分成六种数据类型 5个基本数据类型+object

  • String 字符串
  • Number 数值
  • Boolean 布尔值
  • Null 空值
  • Undefined 未定义
  • Object 对象

typeof运算符检查数据类型

1、String 字符串

JS中的字符串需要使用引号引起来双引号或单引号都行
在字符串中使用\作为转义字符

\'  ==> '  
\"  ==> "  
\n  ==> 换行  
\t  ==> 制表符  
\\  ==> \

使用typeof运算符检查字符串时,会返回 string

var a = 'abc'
console.log(typeof a)

2、Number 数值

JS中所有的整数和浮点数都是Number类型

最大能表示的值:Number.MAX_VALUE= 1.7976931348623157e+308

Number.MIN_VALUE :大于 0 的最小正值

除了常规的数字,还包括所谓的“特殊数值(“special numeric values”)”也属于这种类型:Infinity-InfinityNaN

  • Infinity 代表数学概念中的 无穷大 ∞。是一个比任何数字都大的特殊值。

    我们可以通过除以 0 来得到它:

    alert( 1 / 0 ); // Infinity

    或者在代码中直接使用它:

    alert( Infinity ); // Infinity
  • NaN 代表一个计算错误。它是一个不正确的或者一个未定义的数学操作所得到的结果,比如:

    alert( "not a number" / 2 ); // NaN,这样的除法是错误的

    NaN 是粘性的。任何对 NaN 的进一步数学运算都会返回 NaN

    alert( NaN + 1 ); // NaN
    alert( 3 * NaN ); // NaN
    alert( "not a number" / 2 - 1 ); // NaN

    所以,如果在数学表达式中有一个 NaN,会被传播到最终结果(只有一个例外:NaN ** 0 结果为 1

其他进制的数字的表示:

0b 开头表示二进制,但是不是所有的浏览器都支持

0 开头表示八进制

0x 开头表示十六进制

使用typeof检查一个Number类型的数据时,会返回 number(包括NaN 和 Infinity)

3、Boolean 布尔值

布尔值主要用来进行逻辑判断,布尔值只有两个

true 逻辑的真

false 逻辑的假

使用typeof检查一个布尔值时,会返回 boolean

4、Null 空值

空值专门用来表示为空的对象,Null类型的值只有一个 null

使用typeof检查一个Null类型的值时会返回 object

5、Undefined 未定义

如果声明一个变量但是没有为变量赋值此时变量的值就是undefined

该类型的值只有一个 undefined

使用typeof检查一个Undefined类型的值时,会返回 undefined

undefined 的值是派生自 null 的,因此根据 ECMA-262 的规定 null == undefined // true。两者作为判断条件时都会返回 false,具体的区别是在含义上。

当一个变量未声明就被使用时,返回的是 undefined,可以理解为这个变量在内存中都不存在。
而一个变量声明了,只是暂时不需要赋值时应该赋 null 代表这个变量目前没有对应的值。在实际应用中也会把 null 作为一个初始值。

因此从语义上来讲,不应该给变量赋 undefined

6、BigInt

在 JavaScript 中,“number” 类型无法表示大于 (253-1)(即 9007199254740991),或小于 -(253-1) 的整数。这是其内部表示形式导致的技术限制。

在大多数情况下,这个范围就足够了,但有时我们需要很大的数字,例如用于加密或微秒精度的时间戳。

BigInt 类型是最近被添加到 JavaScript 语言中的,用于表示任意长度的整数。

可以通过将 n 附加到整数字段的末尾来创建 BigInt 值。

// 尾部的 "n" 表示这是一个 BigInt 类型
const bigInt = 1234567890123456789012345678901234567890n;

二、对象

1、操作对象

(1)创建对象

我们可以用下面两种语法中的任一种来创建一个空的对象:

let user = new Object(); // “构造函数” 的语法
let user = {};  // “字面量” 的语法

let user  = {
    name: "John",
    "likes birds": true  // 多词属性名必须加引号,且操作时必须使用 对象["属性"]
}

(2)对象属性

对象属性的读取

console.log( user.name );
console.log( user["name"] );

对象属性的赋值

user.name = "lisi";
user["name"] = "lisi";

对象属性的删除

delete user.name
delete user["name"]

(3)方括号

请注意方括号中的字符串要放在引号中,单引号或双引号都可以。

方括号同样提供了一种可以通过任意表达式来获取属性名的方法 —— 跟语义上的字符串不同 —— 比如像类似于下面的变量:

let key = "likes birds";

// 跟 user["likes birds"] = true; 一样
user[key] = true;

在这里,变量 key 可以是程序运行时计算得到的,也可以是根据用户的输入得到的。然后我们可以用它来访问属性。这给了我们很大的灵活性。

例如:

let user = {
  name: "John",
  age: 30
};

let key = prompt("What do you want to know about the user?", "name");

// 访问变量
alert( user[key] ); // John(如果输入 "name")

(4)计算属性

当创建一个对象时,我们可以在对象字面量中使用方括号。这叫做 计算属性

let fruit = prompt("Which fruit to buy?", "apple");

let bag = {
  [fruit]: 5, // 属性名是从 fruit 变量中得到的
};

alert( bag.apple ); // 5 如果 fruit="apple"

计算属性的含义很简单:[fruit] 含义是属性名应该从 fruit 变量中获取。

所以,如果一个用户输入 "apple"bag 将变为 {apple: 5}

本质上,这跟下面的语法效果相同:

let fruit = prompt("Which fruit to buy?", "apple");
let bag = {};

// 从 fruit 变量中获取值
bag[fruit] = 5;

我们可以在方括号中使用更复杂的表达式:

let fruit = 'apple';
let bag = {
  [fruit + 'Computers']: 5 // bag.appleComputers = 5
};

方括号比点符号更强大。它允许任何属性名和变量,但写起来也更加麻烦。

(5)属性简写

在实际开发中,我们通常用已存在的变量当做属性名。

可以用 name 来代替 name:name 像下面那样:

let user = {
  name,  // 与 name:name 相同
  age: 30
};

(6)遍历对象属性

检查属性是否存在的操作符 "in"

let user = { name: "John", age: 30 };

alert( "age" in user ); // true,user.age 存在
alert( "blabla" in user ); // false,user.blabla 不存在。

let key = "age";
alert( key in user ); // true,属性 "age" 存在

遍历属性

let user = {
  name: "John",
  age: 30,
  isAdmin: true
};

for (let key in user) {
  // keys
  alert( key );  // name, age, isAdmin
  // 属性键的值
  alert( user[key] ); // John, 30, true
}

2、对象引用和复制

(1)对象的引用

赋值了对象的变量存储的不是对象本身,而是该对象“在内存中的地址” —— 换句话说就是对该对象的“引用”。与 java 一致

let user = { name: 'John' };
let admin = user;

admin.name = 'Pete'; // 通过 "admin" 引用来修改
alert(user.name); // 'Pete',修改能通过 "user" 引用看到

(2)克隆与合并

那么,拷贝一个对象变量会又创建一个对相同对象的引用。

但是,如果我们想要复制一个对象,那该怎么做呢?创建一个独立的拷贝,克隆?

这也是可行的,但稍微有点困难,因为 JavaScript 没有提供对此操作的内建的方法。但很少需要这样做 —— 通过引用进行拷贝在大多数情况下已经满足了。

但是,如果我们真的想要这样做,那么就需要创建一个新对象,并通过遍历现有属性的结构,在原始类型值的层面,将其复制到新对象,以复制已有对象的结构。

就像这样:

let user = {
  name: "John",
  age: 30
};

let clone = {}; // 新的空对象

// 将 user 中所有的属性拷贝到其中
for (let key in user) {
  clone[key] = user[key];
}

// 现在 clone 是带有相同内容的完全独立的对象
clone.name = "Pete"; // 改变了其中的数据

alert( user.name ); // 原来的对象中的 name 属性依然是 John

我们也可以使用 Object.assign方法来达成同样的效果。

语法是:

Object.assign(dest, [src1, src2, src3...])
  • 第一个参数 dest 是指目标对象。
  • 更后面的参数 src1, ..., srcN(可按需传递多个参数)是源对象。
  • 该方法将所有源对象的属性拷贝到目标对象 dest 中。换句话说,从第二个开始的所有参数的属性都被拷贝到第一个参数的对象中。
  • 调用结果返回 dest

例如,我们可以用它来合并多个对象:

let user = { name: "John" };

let permissions1 = { canView: true };
let permissions2 = { canEdit: true };

// 将 permissions1 和 permissions2 中的所有属性都拷贝到 user 中
Object.assign(user, permissions1, permissions2);

// 现在 user = { name: "John", canView: true, canEdit: true }

如果被拷贝的属性的属性名已经存在,那么它会被覆盖:

let user = { name: "John" };

Object.assign(user, { name: "Pete" });

alert(user.name); // 现在 user = { name: "Pete" }

我们也可以用 Object.assign 代替 for..in 循环来进行简单克隆:

let user = {
  name: "John",
  age: 30
};

let clone = Object.assign({}, user);

它将 user 中的所有属性拷贝到了一个空对象中,并返回这个新的对象。

(3)深层克隆

到现在为止,我们都假设 user 的所有属性均为原始类型。但属性可以是对其他对象的引用。那应该怎样处理它们呢?

现在这样拷贝 clone.sizes = user.sizes 已经不足够了,因为 user.sizes 是个对象,它会以引用形式被拷贝。因此 cloneuser 会共用一个 sizes:

就像这样:

let user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};

let clone = Object.assign({}, user);

alert( user.sizes === clone.sizes ); // true,同一个对象

// user 和 clone 分享同一个 sizes
user.sizes.width++;       // 通过其中一个改变属性值
alert(clone.sizes.width); // 51,能从另外一个看到变更的结果

为了解决这个问题,我们应该使用一个拷贝循环来检查 user[key] 的每个值,如果它是一个对象,那么也复制它的结构。这就是所谓的“深拷贝”。

我们可以使用递归来实现它。或者为了不重复造轮子,采用现有的实现,例如 lodash 库的 _.cloneDeep(obj)

3、垃圾回收

三、其他

1、空值合并运算符 ‘??’

由于它对待 nullundefined 的方式类似,所以在本文中我们将使用一个特殊的术语对其进行表示。为简洁起见,当一个值既不是 null 也不是 undefined 时,我们将其称为已定义的(defined)。

a ?? b 的结果是:

  • 如果 a 是已定义的,则结果为 a
  • 如果 a 不是已定义的,则结果为 b

换句话说,如果第一个参数不是 null/undefined,则 ?? 返回第一个参数。否则,返回第二个参数。

空值合并运算符并不是什么全新的东西。它只是一种获得两者中的第一个“已定义的”值的不错的语法。

我们可以使用我们已知的运算符重写 result = a ?? b,像这样:

result = (a !== null && a !== undefined) ? a : b;

现在你应该清楚了 ?? 的作用。让我们来看看它的使用场景吧。

?? 的常见使用场景是提供默认值。

例如,在这里,如果 user 的值不为 null/undefined 则显示 user,否则显示 匿名

let user;
alert(user ?? "匿名"); // 匿名(user 未定义)

我们还可以使用 ?? 序列从一系列的值中选择出第一个非 null/undefined 的值。

假设我们在变量 firstNamelastNamenickName 中存储着一个用户的数据。如果用户决定不填写相应的值,则所有这些变量的值都可能是未定义的。

我们想使用这些变量之一显示用户名,如果这些变量的值都是 null/undefined,则显示 “匿名”。

让我们使用 ?? 运算符来实现这一需求:

let firstName = null;
let lastName = null;
let nickName = "Supercoder";

// 显示第一个已定义的值:
alert(firstName ?? lastName ?? nickName ?? "匿名"); // Supercoder

与 || 比较

或运算符 || 可以以与 ?? 运算符相同的方式使用。

例如,在上面的代码中,我们可以用 || 替换掉 ??,也可以获得相同的结果:

let firstName = null;
let lastName = null;
let nickName = "Supercoder";

// 显示第一个真值:
alert(firstName || lastName || nickName || "Anonymous"); // Supercoder

纵观 JavaScript 发展史,或 || 运算符先于 ?? 出现。它自 JavaScript 诞生就存在了,因此开发者长期将其用于这种目的。

另一方面,空值合并运算符 ?? 是最近才被添加到 JavaScript 中的,它的出现是因为人们对 || 不太满意。

它们之间重要的区别是:

  • || 返回第一个 值。
  • ?? 返回第一个 已定义的 值。

换句话说,|| 无法区分 false0、空字符串 ""null/undefined。它们都一样 —— 假值(falsy values)。如果其中任何一个是 || 的第一个参数,那么我们将得到第二个参数作为结果。

不过在实际中,我们可能只想在变量的值为 null/undefined 时使用默认值。也就是说,当该值确实未知或未被设置时。

例如,考虑下面这种情况:

let height = 0;

alert(height || 100); // 100
alert(height ?? 100); // 0

?? 运算符的优先级与 || 相同,它们的的优先级都为 4,出于安全原因,JavaScript 禁止将 ?? 运算符与 &&|| 运算符一起使用,除非使用括号明确指定了优先级。


  目录