JavaScript 第1章 简述 JavaScript

本文详细介绍了JavaScript的基础知识,包括引擎的运行环境、代码限制、严格模式的作用、类型转换的规则,以及数字、字符串和数组的特性和操作方法。同时,还探讨了对象的属性、引用、垃圾回收以及Map和Set数据结构的使用。此外,介绍了函数的定义、解构赋值的语法以及JSON相关的方法。最后,提到了浏览器调试工具的使用技巧。
摘要由CSDN通过智能技术生成

1、简介

1)引擎

  • JavaScript 不仅可以在浏览器中执行,也可以在服务端执行,甚至可以在任意搭载了 JavaScript 引擎 的设备中执行。

  • 不同的引擎有不同的“代号”,V8 指 Chrome、Opera 和 Edge 中的 JavaScript 引擎。

2)限制性

  • 为了用户信息安全,在浏览器中 JavaScript 的能力是受限的。目的是防止有人恶意利用网页获取用户私人信息或损害用户数据。

  • 网页中的 JavaScript 不能读、写、复制和执行硬盘上的任意文件。它没有直接访问操作系统的功能。

2、基础

1)严格模式

  • "use strict" 处于脚本文件的顶部时,整个脚本文件都将以“严格模式”进行工作。
(1)限制
  1. 未声明的变量在赋值时会报错。

2)类型转换

(1)字符串转换
  • String(value) 可以将 value 转换为字符串类型。

    let value = true;
    alert(typeof value); // boolean
    
    value = String(value); // 现在,值是一个字符串形式的 "true"
    alert(typeof value); // string
    
(2)数字型转换
  • 在算术函数和表达式中,会自动进行 number 类型转换。比如,当把除法 / 用于非 number 类型:

    alert( "6" / "2" ); // 3, string 类型的值被自动转换成 number 类型后进行计算
    
  • number 类型转换规则:

    变成……
    undefinedNaN
    null0
    true 和 false1 and 0
    string“按原样读取”字符串,两端的空白会被忽略。空字符串变成 0。转换出错则输出 NaN
  • 例子

    alert( Number("   123   ") ); // 123
    alert( Number("123z") );      // NaN(从字符串“读取”数字,读到 "z" 时出现错误)
    alert( Number(true) );        // 1
    

3)基础运算符

(1)运算符 + 连接字符串
  • 如果加号 + 被应用于字符串,它将合并(连接)各个字符串。只要任意一个运算元是字符串,那么另一个运算元也将被转化为字符串。

    alert( '1' + 2 ); // "12"
    alert( 2 + '1' ); // "21"
    alert(2 + 2 + '1' ); // "41",不是 "221"
    
(2)运算符 + 数字转化
  • 加号 + 应用于单个值,对数字没有任何作用。但是如果运算元不是数字,加号 + 则会将其转化为数字。

    // 对数字无效
    let x = 1;
    alert( +x ); // 1
    
    // 转化非数字
    alert( +true ); // 1
    alert( +"" );   // 0
    
  • 效果和 Number(...) 相同,但是更加简短。

    let apples = "2";
    let oranges = "3";
    
    // 在二元运算符加号起作用之前,所有的值都被转化为了数字
    alert( +apples + +oranges ); // 5
    
    // 更长的写法
    // alert( Number(apples) + Number(oranges) ); // 5
    
(3)运算符 ?比较
  • 使用一系列问号 ? 运算符可以返回一个取决于多个条件的值。

    let message = (age < 3) ? 'Hi, baby!' :
      (age < 18) ? 'Hello!' :
      (age < 100) ? 'Greetings!' :
      'What an unusual age!';
    
    --- 等价于
    
    if (age < 3) {
      message = 'Hi, baby!';
    } else if (age < 18) {
      message = 'Hello!';
    } else if (age < 100) {
      message = 'Greetings!';
    } else {
      message = 'What an unusual age!';
    }
    
(4)运算符 &&
  • 运算符 " && " 的优先级高于 " | | "

    alert( null || 2 && 3 || 4 ); // 3
    

4)空值合并运算符 ??

(1)定义
  • a ?? b 的结果是:如果第一个参数不是 null/undefined,则返回第一个参数。否则返回第二个参数。

  • 应用场景:提供默认值

    let user;
    alert(user ?? "匿名"); // 匿名(user 未定义)
    
(2)与 || 比较
  • || 返回第一个 值,?? 返回第一个 已定义的 值。
  • 换句话说,|| 无法区分 false0、空字符串 ""null/undefined
(3)注意
  • ?? 运算符的优先级非常低,仅略高于 ?=,因此在表达式中使用它时请考虑添加括号。
  • 如果没有明确添加括号,不能将其与 ||&& 一起使用。

5)值的比较

(1)类型间比较
  • 当对不同类型的值进行比较时,JavaScript 会首先将其转化为数字(number)再判定大小。

    alert( '2' > 1 ); // true,字符串 '2' 会被转化为数字 2
    alert( '01' == 1 ); // true,字符串 '01' 会被转化为数字 1
    alert( true == 1 ); // true
    alert( false == 0 ); // true
    
(2)严格相等
  • 严格相等运算符 === 在进行比较时不会做任何的类型转换。

  • 在非严格相等 == 下,nullundefined 相等且各自不等于任何其他的值。

6)循环的结束

(1)单层
  • break 指令被激活,会立刻终止循环,将控制权传递给循环后的第一行。

  • continue 指令是 break 的“轻量版”。它不会停掉整个循环。而是停止当前这一次迭代,并强制启动新一轮循环(如果条件允许的话)。

(2)多层
  • break <labelName> 语句跳出循环至标签处,可以结束多层循环。

    outer: 
    for (let i = 0; i < 3; i++) {
      for (let j = 0; j < 3; j++) {
        let input = prompt(`Value at coords (${i},${j})`, '');
        // 如果是空字符串或被取消,则中断并跳出这两个循环。
        if (!input) break outer;
        ...
      }
    }
    alert('Done!');
    
  • 标签不允许我们跳到代码的任意位置。

    break label;  // 跳转至下面的 label 处(无效)
    label: for (...)
    
  • continue 指令也可以与标签一起使用。在这种情况下,执行跳转到标记循环的下一次迭代。

3、数据类型

1)数字

(1)书写形式
  • 可以使用下划线 _ 作为分隔符:

    let billion = 1_000_000_000;
    
  • 可以通过在数字后面附加字母 "e" 并指定零的个数来缩短数字:

    let billion = 1e9;  // 10 亿,字面意思:数字 1 后面跟 9 个 0
    
    1.23e6 === 1.23 * 1000000; // e6 表示 *1000000
    
    let mcs = 1e-6; // 1 的左边有 6 个 0
    1.23e-6 === 1.23 / 1000000; // 0.00000123
    
(2)进制形式
  • 方法 num.toString(base) 返回在给定 base 进制数字系统中 num 的字符串表示形式。

    base 的范围可以从 236。默认情况下是 10

    let num = 255;
    
    alert( num.toString(16) );  // ff
    alert( num.toString(2) );   // 11111111
    
    // JavaScript 语法隐含了第一个点之后的部分为小数部分。如果我们再放一个点,那么 JavaScript 就知道小数部分为空,现在使用该方法。 
    
    alert( 123456..toString(36) ); // 2n9c
    (123456).toString(36);
    
(3)舍入函数
  • Math.floor
    

    向下舍入:3.1 变成 3-1.1 变成 -2

  • Math.ceil
    

    向上舍入:3.1 变成 4-1.1 变成 -1

  • Math.round
    

    四舍五入:3.1 变成 33.6 变成 4,中间值 3.5 变成 4

  • Math.trunc
    

    移除小数点后的所有内容而不舍入:3.1 变成 3-1.1 变成 -1

  • 函数 toFixed(n) 将数字舍入到小数点后 n 位,并以字符串形式返回结果。

    let num = 12.34;
    alert( num.toFixed(1) ); // "12.3"
    
    let num = 12.34;
    alert( num.toFixed(5) ); // "12.34000",在结尾添加了 0,以达到小数点后五位
    
(4)不精确计算
  • 一个数字真的很大,则可能会溢出 64 位存储,变成一个特殊的数值 Infinity

    alert( 1e500 ); // Infinity
    
  • 经常会发生的精度损失。相等性测试:

    alert( 0.1 + 0.2 == 0.3 ); // false
    
(5)符号函数
  • parseIntparseFloat 可以从字符串中“读取”数字,直到无法读取为止。如果发生 error,则返回收集到的数字。

    alert( parseInt('100px') ); // 100
    alert( parseFloat('12.5em') ); // 12.5
    
    alert( parseInt('12.3') ); // 12,只有整数部分被返回了
    alert( parseFloat('12.3.4') ); // 12.3,在第二个点出停止了读取
    
  • 某些情况下,parseInt/parseFloat 会返回 NaN。当没有数字可读时会发生这种情况:

    alert( parseInt('a123') ); // NaN,第一个符号停止了读取
    
  • parseInt() 函数具有可选的第二个参数。它指定了数字系统的基数,因此 parseInt 还可以解析十六进制数字、二进制数字等的字符串:

    alert( parseInt('0xff', 16) ); // 255
    alert( parseInt('ff', 16) ); // 255,没有 0x 仍然有效
    
    alert( parseInt('2n9c', 36) ); // 123456
    
(6)特殊函数
  • isNaN(value) 将其参数转换为数字,然后测试它是否为 NaN
alert( isNaN(NaN) ); // true
alert( isNaN("str") ); // true
  • 值 “NaN” 是独一无二的,它不等于任何东西,包括它自身:

    alert( NaN === NaN ); // false
    
  • isFinite(value) 将其参数转换为数字,如果是常规数字而不是 NaN/Infinity/-Infinity,则返回 true

    alert( isFinite("15") ); // true
    alert( isFinite("str") ); // false,因为是一个特殊的值:NaN
    alert( isFinite(Infinity) ); // false,因为是一个特殊的值:Infinity
    

2)字符串

(1)查找子字符串
  • str.indexOf (): 从给定位置 pos 开始,在 str 中查找 substr,没找到则返回 -1,否则返回匹配成功的位置。

    let str = 'Widget with id';
    
    alert( str.indexOf('Widget') ); // 0,因为 'Widget' 一开始就被找到
    alert( str.indexOf('Dget') ); // -1,没有找到,检索是大小写敏感的
    
    alert( str.indexOf("id") ); // 1,"id" 在位置 1 处(……idget 和 id)
    
    alert( str.indexOf('id', 2) ) // 12
    
  • str.includes( substr , pos ): 根据 str 中是否包含 substr 来返回 true/false

    alert( "Midget".includes("id") ); // true
    alert( "Midget".includes("id", 3) ); // false, 从位置 3 开始没有 "id"
    
  • str.startsWith | endsWith:查找首尾。

    alert( "Widget".startsWith("Wid") ); // true,"Widget" 以 "Wid" 开始
    
    alert( "Widget".endsWith("get") ); // true,"Widget" 以 "get" 结束
    
(2)获取子字符串
  • str.slice(start [, end])
    

    返回字符串从 start 到(但不包括)end 的部分。

    let str = "stringify";
    alert( str.slice(0, 5) ); // 'strin',从 0 到 5 的子字符串(不包括 5)
    alert( str.slice(0, 1) ); // 's',从 0 到 1,但不包括 1,所以只有在 0 处的字符
    

    如果没有第二个参数,slice 会一直运行到字符串末尾:

    let str = "stringify";
    alert( str.slice(2) ); // 从第二个位置直到结束
    

    start/end 也有可能是负值。它们的意思是起始位置从字符串结尾计算:

    let str = "stringify";
    
    // 从右边的第四个位置开始,在右边的第一个位置结束
    alert( str.slice(-4, -1) ); // 'gif'
    
  • str.substr(start [, length])
    

    返回字符串从 start 开始的给定 length 的部分。

    let str = "stringify";
    alert( str.substr(2, 4) ); // 'ring',从位置 2 开始,获取 4 个字符
    

    第一个参数可能是负数,从结尾算起:

    let str = "stringify";
    alert( str.substr(-4, 2) ); // 'gi',从第 4 位获取 2 个字符
    

3)数组

(1)定义
  • 数组可以存储任何类型的元素。

    // 混合值
    let arr = [ 'Apple', { name: 'John' }, true, function() { alert('hello'); } ];
    
    // 获取索引为 1 的对象然后显示它的 name
    alert( arr[1].name ); // John
    
    // 获取索引为 3 的函数并执行
    arr[3](); // hello
    
(2)执行机制
  • 数组是一种特殊的对象。使用方括号来访问属性 arr[0] 实际上是来自于对象的语法。

    它其实与 obj[key] 相同,其中 arr 是对象,而数字用作键(key)

  • 通过引用进行复制, 两个变量引用的是相同的数组 。

    let fruits = ["Banana"]
    let arr = fruits; // 通过引用复制 (两个变量引用的是相同的数组)
    
    alert( arr === fruits ); // true
    
    arr.push("Pear"); // 通过引用修改数组
    alert( fruits ); // Banana, Pear — 现在有 2 项了
    
  • 数组真正特殊的是它们的内部实现。JavaScript 引擎尝试把这些元素一个接一个地存储在连续的内存区域,而且还有一些其它的优化,以使数组运行得非常快。

  • 数组是基于对象的。我们可以给它们添加任何属性。但是 Javascript 引擎会发现,我们在像使用常规对象一样使用数组,那么针对数组的优化就不再适用了,然后对应的优化就会被关闭。

    /*
    数组误用的几种方式:
    添加一个非数字的属性,比如 arr.test = 5。
    制造空洞,比如:添加 arr[0],然后添加 arr[1000] (它们中间什么都没有)。
    以倒序填充数组,比如 arr[1000],arr[999] 等等。
    */
    let fruits = []; // 创建一个数组
    
    fruits[99999] = 5; // 分配索引远大于数组长度的属性
    
    fruits.age = 25; // 创建一个具有任意名称的属性
    
(3)获取元素
  • arr.at(i)
    

    返回数组元素,参数可以为负数。

    let fruits = ["Apple", "Orange", "Plum"];
    
    alert( fruits.at(-1) ); // Plum
    
(4)数组循环
  • **for..in 循环适用于普通对象,并且做了对应的优化。但是不适用于数组,因此速度要慢 10-100 倍。 **

  • for..of 不能获取当前元素的索引,只是获取元素值,但大多数情况是够用的。

let fruits = ["Apple", "Orange", "Plum"];

// 遍历数组元素
for (let fruit of fruits) {
  alert( fruit );
}
(5)Length 属性
  • **length 属性会自动更新。准确来说,它实际上不是数组里元素的个数,而是最大的数字索引值加一。 **
let fruits = [];
fruits[123] = "Apple";
alert( fruits.length ); // 124
  • length 属性是可写的。手动增加不会发生任何事。但是减少它,数组就会被截断。该过程是不可逆的。

    let arr = [1, 2, 3, 4, 5];
    
    arr.length = 2; // 截断到只剩 2 个元素
    alert( arr ); // [1, 2]
    
    arr.length = 5; // 又把 length 加回来
    alert( arr[3] ); // undefined:被截断的那些数值并没有回来
    

4、数组方法

1)修改:splice

  • arr.splice(start[, deleteCount, elem1, ..., elemN])
    

    arr.splice 可以添加,删除和插入元素。

    它从索引 start 开始修改 arr,删除 deleteCount 个元素并在当前位置插入 elem1, ..., elemN。最后返回已被删除元素的数组。

    let arr = ["I", "study", "JavaScript", "right", "now"];
    
    // 删除数组的前三项,并使用其他内容代替它们
    arr.splice(0, 3, "Let's", "dance");
    
    

alert( arr ) // 现在 [“Let’s”, “dance”, “right”, “now”]


-  负向索引都是被允许的。 

```javascript
let arr = [1, 2, 5];

// 从索引 -1(尾端前一位) 删除 0 个元素,然后插入 3 和 4
arr.splice(-1, 0, 3, 4);

alert( arr ); // 1,2,3,4,5

2)子数组副本:slice

  • arr.slice() 会创建一个 arr 的副本。其通常用于获取副本,以进行不影响原始数组的进一步转换。

    arr.slice([start], [end])
    
  • arr.slice 返回一个新数组,将所有从索引 startend(不包括 end)的数组项复制到一个新的数组。startend 都可以是负数,在这种情况下,从末尾计算索引。

    let arr = ["t", "e", "s", "t"];
    alert( arr.slice(1, 3) ); // e,s(复制从位置 1 到位置 3 的元素)
    alert( arr.slice(-2) ); // s,t(复制从位置 -2 到尾端的元素)
    

3)合并:concat

  • arr.concat 创建一个新数组,其中包含来自于其他数组和其他项的值。

    let arr = [1, 2];
    
    alert( arr.concat([3, 4]) ); // 1,2,3,4
    
    alert( arr.concat([3, 4], 5, 6) ); // 1,2,3,4,5,6
    

4)遍历:foreach

  • arr.forEach 方法允许为数组的每个元素都运行一个函数。

    arr.forEach(function(item, index, array) {
      // ... do something with item
    });
    
    ["Bilbo", "Gandalf", "Nazgul"].forEach((item, index, array) => {
      alert(`${item} is at index ${index} in ${array}`);
    });
    

5)查找特定对象:find

  • 可以使用 arr.find 方法 查找对象数组中的特定对象

    let users = [
      {id: 1, name: "John"},
      {id: 2, name: "Pete"},
      {id: 3, name: "Mary"}
    ];
    
    let user = users.find(item => item.id == 1);
    
    alert(user.name); // John
    

6)结果:map

  • arr.map 方法是最有用和经常使用的方法之一。它对数组的每个元素都调用函数,并返回结果数组。

    let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length);
    alert(lengths); // 5,7,6
    

7)辨别:Array.isArray

  • 数组是基于对象的,不构成单独的语言类型。所以 typeof 不能帮助从数组中区分出普通对象:

    alert(typeof {}); // object
    
    alert(typeof []); // object
    
  • 如果 value 是一个数组,则返回 true;否则返回 false

    alert(Array.isArray({a:b})); // false
    
    alert(Array.isArray([a,b])); // true
    

5、对象

1)属性

(0)访问属性
  • 点符号: obj.property
  • 方括号 obj["property"],方括号允许从变量中获取键,例如 obj[varWithKey]
(1)多词属性
  • 多字词语来作为属性名,但必须给它们加上引号:
let user = {
  name: "John",
  age: 30,
  "likes birds": true  // 多词属性名必须加引号
};

// 这将提示有语法错误
user.likes birds = true

// 设置
user["likes birds"] = true;

let key = "likes birds";

// 跟 user["likes birds"] = true; 一样
user[key] = true;
(2)移除属性
  • 可以用 delete 操作符移除属性:
let user = {     // 一个对象
  name: "John",  // 键 "name",值 "John"
  age: 30        // 键 "age",值 30
};

delete user.age;
(3)计算属性
  • 当创建一个对象时,我们可以在对象字面量中使用方括号。

    let fruit = prompt("Which fruit to buy?", "apple");
    
    let bag = {
      [fruit]: 5, // 属性名是从 fruit 变量中得到的
    };
    
    alert( bag.apple ); // 5 如果 fruit="apple"
    
    ---本质上,这跟下面的语法效果相同:
    
    let fruit = prompt("Which fruit to buy?", "apple");
    let bag = {};
    
    // 从 fruit 变量中获取值
    bag[fruit] = 5;
    
(4)遍历属性
  • 遍历一个对象的所有键(key),可以使用一个特殊形式的循环:for..in

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
}


#### (5)属性存在性

-  JavaScript 的对象有一个需要注意的特性:能够被访问任何属性。即使属性不存在也不会报错!  读取不存在的属性只会得到 `undefined`。 

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

alert( "age" in user ); // true,user.age 存在
alert( "blabla" in user ); // false,user.blabla 不存在。
(6)属性排序
  • 对象有 “特别的顺序”:整数属性会被进行排序,其他属性则按照创建的顺序显示 。

    let codes = {
      "49": "Germany",
      "41": "Switzerland",
      "44": "Great Britain",
      // ..,
      "1": "USA"
    };
    
    for(let code in codes) {
      alert(code); // 1, 41, 44, 49
    }
    

2)引用

(1)定义
  • 当一个对象变量被复制 —— 引用被复制,而该对象自身并没有被复制。

    let user = { name: 'John' };
    let admin = user;
    admin.name = 'Pete'; // 通过 "admin" 引用来修改
    alert(user.name); // 'Pete',修改能通过 "user" 引用看到
    
(2)引用比较
  • 仅当两个对象为同一对象时,两者才相等。
let a = {};
let b = a; // 复制引用

alert( a === b ); // true,都引用同一对象
  • 而这里两个独立的对象则并不相等,即使它们看起来很像(都为空):
let a = {};
let b = {}; // 两个独立的对象

alert( a == b ); // false
(3)合并与浅拷贝
  • 对象合并可以使用 Object.assign(dest, [src1, src2, src3...])

    • 第一个参数 dest 是指目标对象。
    • 更后面的参数 src1, ..., srcN(可按需传递多个参数)是源对象。
    • 该方法将所有源对象的属性拷贝到目标对象 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
    };
    
    // 它将 `user` 中的所有属性拷贝到了一个空对象中,并返回这个新的对象。
    let clone = Object.assign({}, user);
    
(4)深拷贝
  • 使用一个拷贝循环来检查 user[key] 的每个值,如果它是一个对象,那么也复制它的结构。这就是所谓的“深拷贝”。

  • 为了不重复造轮子,采用现有的实现,例如 lodash 库的 _.cloneDeep(obj)

3)垃圾回收

(1)可达性
  • JavaScript 中主要的内存管理概念是可达性。简而言之,“可达”值是那些以某种方式可访问或可用的值。它们一定是存储在内存中的。

  • 全局变量和当前嵌套调用链上的局部变量不能被释放,这些值被称为

  • **如果一个值可以通过引用或引用链从根访问任何其他值,则认为该值是可达的。 如对象A的一个属性可以访问对象B,则对象A被认为是可达的, 而且它引用的内容也是可达的。 **

4)可选连 ?.

(1)定义
  • 可选链 ?. 是一种访问嵌套对象属性的安全的方式。即使中间的属性不存在,也不会出现错误。
let user = {}; // user 没有 address 属性

alert( user?.address?.street ); // undefined(不报错)
      
let html = document.querySelector('.elem')?.innerHTML; // 如果没有符合的元素,则为 undefined      
(2)形式
  1. obj?.prop —— 如果 obj 存在则返回 obj.prop,否则返回 undefined
  2. obj?.[prop] —— 如果 obj 存在则返回 obj[prop],否则返回 undefined
  3. obj.method?.() —— 如果 obj.method 存在则调用 obj.method(),否则返回 undefined

5)Symbol 类型

(1)定义
  • symbol 保证是唯一的。即使我们创建了许多具有相同描述的 symbol,它们的值也是不同。描述只是一个标签,不影响任何东西。
let id1 = Symbol("id");
let id2 = Symbol("id");

alert(id1 == id2); // false
  • symbol 不会被自动转换为字符串 , 想显示一个 symbol,我们需要在它上面调用 .toString()
let id = Symbol("id");
alert(id.toString()); // Symbol(id),现在它有效了
  • 在对象字面量 {...} 中使用 symbol,需要方括号把它括起来。因为需要变量 id 的值作为键,而不是字符串 “id”。

    let id = Symbol("id");
    
    let user = {
      name: "John",
      [id]: 123 // 而不是 "id":123
    };
    
(2)全局 symbol
  • 想要名字相同的 symbol 具有相同的实体,需要 一个全局 symbol 注册表

  • 从注册表中读取(不存在则创建)symbol,使用 Symbol.for(key)

// 从全局注册表中读取
let id = Symbol.for("id"); // 如果该 symbol 不存在,则创建它

// 再次读取(可能是在代码中的另一个位置)
let idAgain = Symbol.for("id");

// 相同的 symbol
alert( id === idAgain ); // true
  • 反向调用:Symbol.keyFor(sym),它的作用完全反过来:通过全局 symbol 返回一个名字。 如果 symbol 不是全局的,它将无法找到它并返回 undefined
// 通过 name 获取 symbol
let sym = Symbol.for("name");
let sym2 = Symbol.for("id");

// 通过 symbol 获取 name
alert( Symbol.keyFor(sym) ); // name
alert( Symbol.keyFor(sym2) ); // id

6、可迭代对象

1)自定义

  • 一个 range 对象,它代表了一个数字区间:
let range = {
  from: 1,
  to: 5
};

// 我们希望 for..of 可以这样运行:
// for(let num of range) ... num=1,2,3,4,5
  • 为了让 range 对象可迭代(也就让 for..of 可以运行),我们需要为对象添加一个名为 Symbol.iterator 的方法(一个专门用于使对象可迭代的内建 symbol)。

    1. for..of 循环启动时,它会调用这个方法(如果没找到,就会报错)。这个方法必须返回一个 迭代器(iterator) —— 一个有 next 方法的对象。
    2. for..of 循环希望取得下一个数值,它就调用这个对象的 next() 方法。
    3. next() 方法返回的结果的格式必须是 {done: Boolean, value: any},当 done=true 时,表示循环结束,否则 value 是下一个值。
    let range = {
      from: 1,
      to: 5
    };
    
    // 1. for..of 调用首先会调用这个:
    range[Symbol.iterator] = function() {
    
      // ……它返回迭代器对象(iterator object):
      // 2. 接下来,for..of 仅与下面的迭代器对象一起工作,要求它提供下一个值
      return {
        current: this.from,
        last: this.to,
    
        // 3. next() 在 for..of 的每一轮循环迭代中被调用
        next() {
          // 4. 它将会返回 {done:.., value :...} 格式的对象
          if (this.current <= this.last) {
            return { done: false, value: this.current++ };
          } else {
            return { done: true };
          }
        }
      };
    };
    
    // 现在它可以运行了!
    for (let num of range) {
      alert(num); // 1, 然后是 2, 3, 4, 5
    }
    

    请注意可迭代对象的核心功能:关注点分离。

    • range 自身没有 next() 方法。
    • 相反,是通过调用 range[Symbol.iterator]() 创建了另一个对象,即所谓的“迭代器”对象,并且它的 next 会为迭代生成值。

    因此,迭代器对象和与其进行迭代的对象是分开的。

2)映射:Map

(1)定义
  • Map 允许任何类型的键(key),方法和属性如下:

  • new Map() —— 创建 map。

  • map.set(key, value) —— 根据键存储值。

  • map.get(key) —— 根据键来返回值,如果 map 中不存在对应的 key,则返回 undefined

  • map.has(key) —— 如果 key 存在则返回 true,否则返回 false

  • map.delete(key) —— 删除指定键的值。

  • map.clear() —— 清空 map。

  • map.size —— 返回当前元素个数。

  • map[key] 不是使用 Map 的正确方式

  • 虽然 map[key] 也有效,例如我们可以设置 map[key] = 2,这样会将 map 视为 JavaScript 的 plain object,因此它暗含了所有相应的限制(仅支持 string/symbol 键等)。

  • 应该使用 map 方法:setget 等。

  • 链式调用:每一次 map.set 调用都会返回 map 本身,所以我们可以进行“链式”调用:

    map.set('1', 'str1')
      .set(1, 'num1')
      .set(true, 'bool1');
    
(2)迭代
  • map 里使用循环,可以使用以下三个方法:
  • map.keys() —— 遍历并返回一个包含所有键的可迭代对象,
  • map.values() —— 遍历并返回一个包含所有值的可迭代对象,
  • map.entries() —— 遍历并返回一个包含所有实体 [key, value] 的可迭代对象,for..of 在默认情况下使用的就是这个。
(3)对象 转 Map
  • 通过对象创建一个 Map,可以使用内建方法 Object.entries(obj),该方法返回对象的键/值对数组,该数组格式完全按照 Map 所需的格式。

    let obj = {
      name: "John",
      age: 30
    };
    
    let map = new Map(Object.entries(obj));
    
    alert( map.get('name') ); // John
    
(4)Map 转 对象
  • 可以使用 Object.fromEntriesMap 得到一个普通对象 。
let map = new Map();
map.set('banana', 1);
map.set('orange', 2);
map.set('meat', 4);

let obj = Object.fromEntries(map); 

// 完成了!
// obj = { banana: 1, orange: 2, meat: 4 }

3)弱映射:WeakMap

(1)定义
  • WeakMapMap 的第一个不同点就是,WeakMap 的键必须是对象,不能是原始值:

    let weakMap = new WeakMap();
    
    let obj = {};
    
    weakMap.set(obj, "ok"); // 正常工作(以对象作为键)
    
    // 不能使用字符串作为键
    weakMap.set("test", "Whoops"); // Error,因为 "test" 不是一个对象
    
  • 在 weakMap 中使用一个对象作为键,并且没有其他对这个对象的引用 —— 该对象将会被从内存(和map)中自动清除。

    let john = { name: "John" };
    
    let weakMap = new WeakMap();
    
    weakMap.set(john, "...");
    
    john = null; // 覆盖引用
    
    // john 被从内存中删除了!
    
(2)无迭代
  • **WeakMap 不支持迭代以及 keys()values()entries() 方法。所以没有办法获取 WeakMap 的所有键或值。原因是垃圾回收机制的时间是不确定的,当程序忙碌时,时间可能晚点。 **
  • WeakMap 只有以下的方法:
    • weakMap.get(key)
    • weakMap.set(key, value)
    • weakMap.delete(key)
    • weakMap.has(key)

4)集合:Set

(1)定义
  • 每一个值只能出现一次。它的主要方法如下:
    • new Set(iterable) —— 创建一个 set,如果提供了一个 iterable 对象(通常是数组),将会从数组里面复制值到 set 中,并移除重复的元素。
    • set.add(value) —— 添加一个值,返回 set 本身。
    • set.delete(value) —— 删除值,如果 value 在这个方法调用的时候存在则返回 true ,否则返回 false
    • set.has(value) —— 如果 value 在 set 中,返回 true,否则返回 false
    • set.clear() —— 清空 set。
    • set.size —— 返回元素个数。
(2)迭代
  • 可以使用 for..offorEach 来遍历 Set:

    let set = new Set(["oranges", "apples", "bananas"]);
    
    for (let value of set) alert(value);
    
    // 与 forEach 相同:
    set.forEach((value, valueAgain, set) => {
      alert(value);
    });
    
(3)弱集合:WeakSet
  • Set 类似,但是我们只能向 WeakSet 添加对象(而不能是原始值)。
  • 对象只有在能被引用的时候,才能留在 WeakSet 中。
  • Set 一样,WeakSet 支持 addhasdelete 方法,但不支持 sizekeys(),并且不可迭代。

5)转换对象

  • 对象缺少数组的许多方法,例如 mapfilter 等。可以使用 Object.entries,然后使用 Object.fromEntries

    1. 使用 Object.entries(obj)obj 获取由键/值对组成的数组。
    2. 对该数组使用数组方法,例如 map,对这些键/值对进行转换。
    3. 对结果数组使用 Object.fromEntries(array) 方法,将结果转回成对象。
    // 例如,我们有一个带有价格的对象,并想将它们加倍:
    let prices = {
      banana: 1,
      orange: 2,
      meat: 4,
    };
    
    let doublePrices = Object.fromEntries(
      // 将价格转换为数组,将每个键/值对映射为另一对
      // 然后通过 fromEntries 再将结果转换为对象
      Object.entries(prices).map(entry => [entry[0], entry[1] * 2])
    );
    
    alert(doublePrices.meat); // 8
    

7、函数

1)定义

  • 函数是值。它们可以在代码的任何地方被分配,复制或声明。
  • 函数总是返回一些东西。如果没有 return 语句,那么返回的结果是 undefined
(1)函数声明
  • 如果函数在主代码流中被声明为单独的语句,则称为“函数声明”。
  • 在执行代码块之前,内部算法会先处理函数声明。所以函数声明在其被声明的代码块内的任何位置都是可见的。
(2)函数表达式
  • 如果该函数是作为表达式的一部分创建的,则称其“函数表达式”。
  • 函数表达式在执行流程到达时创建。
(3)箭头函数
  1. 不带花括号:(...args) => expression —— 右侧是一个表达式:函数计算表达式并返回其结果。如果只有一个参数,则可以省略括号,例如 n => n*2

  2. 带花括号:(...args) => { body } —— 花括号允许我们在函数中编写多个语句,但是我们需要显式地 return 来返回一些内容。

    // 表达式在右侧
    let sum = (a, b) => a + b;
    
    // 或带 {...} 的多行语法,此处需要 return:
    let sum = (a, b) => {
      // ...
      return a + b;
    }
    
    // 没有参数
    let sayHi = () => alert("Hello");
    
    // 有一个参数
    let double = n => n * 2;
    

8、进阶

1)解构赋值

(1)数组解构
  • 可以通过添加额外的逗号来丢弃数组中不想要的元素:

    // 不需要第二个元素
    let [firstName, , title] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
    
    alert( title ); // Consul
    
  • 等号左侧使用任何“可以被赋值的”东西。

    let user = {};
    [user.name, user.surname] = "John Smith".split(' ');
    
    alert(user.name); // John
    alert(user.surname); // Smith
    
  • 收集其余参数,可以使用三个点 "..." 来再加一个参数以获取其余数组项:

    let [name1, name2, ...rest] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
    
    // rest 是包含从第三项开始的其余数组项的数组
    alert(rest[0]); // Consul
    alert(rest[1]); // of the Roman Republic
    alert(rest.length); // 2
    
  • 可以设置默认值:

    // 默认值
    let [name = "Guest", surname = "Anonymous"] = ["Julius"];
    
    alert(name);    // Julius(来自数组的值)
    alert(surname); // Anonymous
    
(2)对象解构
  • 在最简单的情况下,等号左侧的就是 {...} 中的变量名列表。

    let options = {
      title: "Menu",
      width: 100,
      height: 200
    };
    
    let {title, width, height} = options;
    
    alert(title);  // Menu
    alert(width);  // 100
    alert(height); // 200
    
  • 变量的顺序并不重要,下面这个代码也是等价的:

    // 改变 let {...} 中元素的顺序
    let {height, width, title} = { title: "Menu", height: 200, width: 100 }
    
  • 可以使用冒号来设置变量名称:

    let options = {
      title: "Menu",
      width: 100,
      height: 200
    };
    
    // { sourceProperty: targetVariable }
    let {width: w, height: h, title} = options;
    
    alert(title);  // Menu
    alert(w);      // 100
    alert(h);      // 200
    
  • 收集其余参数,但形式与数组解构有所不同:

    let options = {
      title: "Menu",
      height: 200,
      width: 100
    };
    
    let {title, ...rest} = options;
    
    // 现在 title="Menu", rest={height: 200, width: 100}
    alert(rest.height);  // 200
    alert(rest.width);   // 100
    
(3)嵌套解构
  • 如果一个对象或数组嵌套了其他的对象和数组,我们可以在等号左侧使用更复杂的模式来提取更深层的数据。
let options = {
  size: {
    width: 100,
    height: 200
  },
  items: ["Cake", "Donut"],
  extra: true
};

// 为了清晰起见,解构赋值语句被写成多行的形式
let {
  size: { // 把 size 赋值到这里
    width,
    height
  },
  items: [item1, item2], // 把 items 赋值到这里
  title = "Menu" // 在对象中不存在(使用默认值)
} = options;

alert(title);  // Menu
alert(width);  // 100
alert(height); // 200
alert(item1);  // Cake
alert(item2);  // Donut

2)Spread 扩展语法

  • **当我们在代码中看到 "..." 时,它要么是 rest 参数,要么就是 spread 语法。 **
  • spread 把一个数组展开为列表。当在函数调用中使用 ...arr 时,它会把可迭代对象 arr “展开”到参数列表中。
(1)定义
  • 可以将 spread 语法与常规值结合使用:

    let arr1 = [1, -2, 3, 4];
    let arr2 = [8, 3, -8, 1];
    
    alert( Math.max(1, ...arr1, 2, ...arr2, 25) ); // 25
    
(2)浅拷贝
  • 这种方式比使用 let arrCopy = Object.assign([], arr) 来复制数组,或使用 let objCopy = Object.assign({}, obj) 来复制对象写起来要短得多。
let obj = { a: 1, b: 2, c: 3 };

let objCopy = { ...obj }; // 将对象 spread 到参数列表中,然后将结果返回到一个新对象

// 两个对象中的内容相同吗?
alert(JSON.stringify(obj) === JSON.stringify(objCopy)); // true

// 两个对象相等吗?
alert(obj === objCopy); // false (not same reference)

// 修改我们初始的对象不会修改副本:
obj.d = 4;
alert(JSON.stringify(obj)); // {"a":1,"b":2,"c":3,"d":4}
alert(JSON.stringify(objCopy)); // {"a":1,"b":2,"c":3}

3)JSON 方法

(1) 对象转 JSON
【1】限制
  • JSON 是与语言无关的纯数据规范,因此一些特定于 JavaScript 的对象属性会被 JSON.stringify 跳过。

    • 函数属性(方法)。
    • Symbol 类型的键和值。
    • 存储 undefined 的属性。
    let user = {
      sayHi() { // 被忽略
        alert("Hello");
      },
      [Symbol("id")]: 123, // 被忽略
      something: undefined // 被忽略
    };
    
    alert( JSON.stringify(user) ); // {}(空对象)
    
  • 不得有循环引用。

    let room = {
      number: 23
    };
    
    let meetup = {
      title: "Conference",
      participants: ["john", "ann"]
    };
    
    meetup.place = room;       // meetup 引用了 room
    room.occupiedBy = meetup; // room 引用了 meetup
    
    JSON.stringify(meetup); // Error: Converting circular structure to JSON
    
【2】排除和转换
  • JSON.stringify 的完整语法是:

    let json = JSON.stringify(value[, replacer, space])
    
  • 可以为下文的 occupiedBy 以外的所有内容按原样返回 value。为了 occupiedBy,下面代码返回 undefined

let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  participants: [{name: "John"}, {name: "Alice"}],
  place: room // meetup 引用了 room
};

room.occupiedBy = meetup; // room 引用了 meetup

alert( JSON.stringify(meetup, function replacer(key, value) {
  alert(`${key}: ${value}`);
  return (key == 'occupiedBy') ? undefined : value;
}));
(2)JSON 转 对象
  • 解码 JSON 字符串,需要另一个方法。

    let value = JSON.parse(str, [reviver]);
    
  • 将 reviver 函数传递给 JSON.parse 作为第二个参数,转换反序列为内建对象:

let schedule = `{
  "meetups": [
    {"title":"Conference","date":"2017-11-30T12:00:00.000Z"},
    {"title":"Birthday","date":"2017-04-18T12:00:00.000Z"}
  ]
}`;

schedule = JSON.parse(schedule, function(key, value) {
  if (key == 'date') return new Date(value);
  return value;
});

alert( schedule.meetups[1].date.getDate() ); // 正常运行了!

4)浏览器调试

(1)断点查看
  • 设置断点之后,打开控制台右侧的信息下拉列表。这里允许你查看当前的代码状态:

    1. 察看(Watch) —— 显示任意表达式的当前值。

      你可以点击加号 + 然后输入一个表达式。调试器将显示它的值,并在执行过程中自动重新计算该表达式。

    2. 调用栈(Call Stack) —— 显示嵌套的调用链。

      此时,调试器正在 hello() 的调用链中,被 index.html 中的一个脚本调用(这里没有函数,因此显示 “anonymous”)

      如果你点击了一个堆栈项,调试器将跳到对应的代码处,并且还可以查看其所有变量。

    3. 作用域(Scope) —— 显示当前的变量。

      Local 显示当前函数中的变量,你还可以在源代码中看到它们的值高亮显示了出来。

      Global 显示全局变量(不在任何函数中)。

(2)跟踪执行
  • 继续执行,快捷键 F8**。**
  • 运行下一条指令,快捷键 F9**。**
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值