什么,都2022年了,你还没掌握这些js基础知识?

前言

  上个月阅读了一本好书你不知道的JavaScript,特在此记录下从第1章到第3章的学习笔记及引发的知识点扩展(包含手敲代码尝试及翻阅各路大神笔记free style,在文中我均以注明出处,并加入了一些自己的思考和扩展)。各位看官可以选择自己感兴趣的部分进行阅读,相信大家看完一定会有所收获。同时,大家如果有什么高见,欢迎在评论区指出哦~

0. 数据类型

  1. JavaScript有八种内置数据类型(基本类型:nnsbusb,引用类型: object)。

    其中,nnsbusb(number、null、symbol、BigInt、undefined、string、boolean)基本类型

    object是引用类型。

  • 数字(number)
  • 空值(null)
  • 符号(symbol, ES6新增)
  • BigInt
  • 未定义(undefined)
  • 字符串(string)
  • 布尔值(boolean)
  • 对象(object)
  1. 可以使用typeof运算符查看类型(记录几种结果):
// typeof返回结果为首字母小写的字符串
// typeof这两个单词首字母都是小写字符
typeof undefined   "undefined"

NaN也是一个数字,只不过是无效数值或者坏数值
typeof NaN 'number'

// ES6中新加入的类型
typeof Symbol()  "symbol"

typeof {life: 42}  "object"
typeof [1,2,3]  "object"

typeof null  "object" 

// 真正检测是否为null的方法
var a = null;
(!a && typeof a === "object");  // true

typeof function a(b,c){ /* .. */ }  "function";

返回函数的命名参数b和c
a.length; // 2

typeof typeof 42; // "string"
复制代码

3.undefinedundeclared

var a;
a;  // undefined(未定义)
b;  // ReferenceError: b is not defined(其实是undeclared,未声明)
复制代码

4.关于Symbol (参考: 公司大佬的github博客)

// Symbol值都是唯一的,它能作为对象属性的标识符。
// Symbol 不用 new 直接使用就行
let smb0 = Symbol('abc')
let smb1 = Symbol('abc') // 还是新创建的一个
smb0 === smb1 // false

Symbol.for 有自己的一个注册池,使用 Symbol.for 时会看看注册池里有没有注册过,如果没有就
会注册一个 Symbol 值,并把这个值放入注册池,如果有,就把注册过的那个 Symbol 值拿出来使用 
let smb2 = Symbol.for('abc') // 对全局进行注册并保存
let smb3 = Symbol.for('abc') // 从全局注册中获取
smb3 === smb2 // true
复制代码

5.关于BigInt (参考: 公司大佬的github博客)

  BigInt,它提供了一种方法来表示大于 2^53 - 1 的整数。2^53 - 1是 Javascript 中可以用 Number 表示的最大数字。可以用 Number.MAX_SAFE_INTEGER(最大安全数)来得到2^53 - 1这个最大值。但是这个值再大之后就会导致Js计算不准确。BigInt 可以展示任意大的整数。可以解决这个问题。

let max = Number.MAX_SAFE_INTEGER; // 最大安全整数
let max1 = max + 1
let max2 = max + 2
max1 === max2 // true 超出阈值,计算不准确

BigInt 也不用 new 直接使用,BigInt 类型的值不能与普通的数值相加
// let bi1 = 9007199254740991n 与下一行代码效果一致,数字后加一个 n 是 BigInt 的字面量创建方式
let bi1 = BigInt(Number.MAX_SAFE_INTEGER)
let max3 = bi1 + 1n
let max4 = bi1 + 2n
max3 === max4 // false
复制代码

1. 值

1. 数组

   数组可以容纳任何类型的值,可以是字符串、数字、对象(object),甚至是其他数组(多维数组就是通过这种方式来实现的)

var a = [ 1, "2", [3] ];

a.length; // 3
a[0] === 1; // true
a[2][0] === 3; // true
复制代码

   对数组声明后即可向其中加入值,不需要预先设定大小

var a = [ ];

a.length; // 0

a[0] = 1;
a[1] = "2";
a[2] = [3];
a.length; // 3
复制代码

   在创建“稀疏”数组(sparse array,即含有空白或空缺单元的数组)时要特别注意:

var a = [ ];
a[0] = 1;

// 此处没有设置a[1]单元

a[2] = [ 3 ];
a[1]; // undefined
a.length; // 3
复制代码

   数组通过数字进行索引,但有趣的是它们也是对象,所以也可以包含字符串键值和属性(但这些并不计算在数组长度内):

var a = [];

a[0] = 1;
a["foobar"] = 2;

a.length; // 1
a["foobar"]; // 2
a.foobar; // 2
复制代码

   这里有个问题需要特别注意,如果字符串键值能够被强制类型转换为十进制数字的话,它就会被当作数字索引来处理:

var a = [ ];

a["13"] = 42;
a.length; // 14
复制代码

   数组展开方法:

 let arr=  [1,2,[[3,4],[5,6]]]
 
 console.log(arr.flat(Infinity));   // 展开所有嵌套层 [ 1, 2, 3, 4, 5, 6 ]
 console.log(arr.flat());   // 默认展开一层,得到 [ 1, 2, [3,4],[5,6] ]
 console.log(arr.flat(1));  // [ 1, 2, [3,4],[5,6] ]
 console.log(arr.flat(2));  // [ 1, 2, 3, 4, 5, 6 ]
复制代码

   数组填充方法fill: MDN之fill传送门

1.重在理解fill函数的参数:

arr.fill(value, start, end)

`value`: 用来填充数组元素的值。
`start(可选)`: 填充的起始索引,默认为0`end(可选)`:  终止索引,默认为`this.length`2.new Array(11).fill([1,2])创建二维数组时所有的[1,2]都会指向同一个数组,这也就意味着,
修改数组中某一个数组的值,数组中其它数组的值也会相应发生变化。
复制代码

2. 类数组

   一些 DOM 查询操作会返回 DOM 元素列表,它们并非真正意义上的数组,但十分类似。另一个例子是通过 arguments 对象(类数组)将函数的参数当作列表来访问(从ES6 开始已废止)。

function foo() {
    // 方法一
    // 改变array的this指向,使其指向arguments
    // 或者[].slice.call(arguments)
    // [] instanceof Array  true
    var arr = Array.prototype.slice.call(arguments);
    // 方法二
    var arr = Array.from(arguments);
    // 方法三
    var arr = [...arguments] ;
    arr.push("bam");
}

foo("bar", "baz"); // ["bar","baz","bam"]
复制代码

   call方法用于修改this的指向并立即执行,一个函数后面直接加.call(),表示直接执行函数。

求一个数组中的最大值(call和apply都会改变函数内部this指向,并执行函数)
方法一:Math.max.call(...[1,2,3])
方法二:Math.max.call(null,...[1,2,3]) 
方法三:Math.max.apply(null,[1,2,3]) // apply方法会自动展开数组,供Math.max方法传参
复制代码

   call方法还有一个重要的用法就是继承:

使用call方法可以使子构造函数继承父构造函数的属性和方法
function Father(uname, age, sex) {
   this.uname = uname
   this.age = age
   this.sex = sex
}

function Son(uname, age, sex) {
   使用call方法调用Father方法,并将Father构造函数实例的this指向子构造函数的实例
   Father.call(this, uname, age, sex)
}
var son = new Son('刘德华', 18, '男')

// console.log(son)
// Son {uname: '刘德华', age: 18, sex: '男'}
复制代码

   使用bind方法返回的是原函数改变this之后产生的新函数。如果有些函数我们不需要立即调用,但是又想改变这个函数内部的this指向,此时使用bind方法。eg:

var a = {name: 'fl'}
// 定义一个函数fn
fn.bind(a) // 这个返回的是函数,如果需要执行,需要在其后再加入一个()

比如我们有一个按钮,当我们点击之后,就禁用这个按钮,并在3秒之后开启这个按钮
var btn = document.querySelector('button')

错误的写法1:
btn.addEventListener('click', function () {
    this.disabled = true
    setTimeout(function () { 
        this.disabled = false // setTimeout是window.setTimeout,其中的this指向window,因此无效
    },3000)
})

正确的写法(推荐):
btn.addEventListener('click', function () {
    this.disabled = true // 这个this指向的是btn这个按钮
    setTimeout(function () { // 定时器函数里面的this 指向的是window
        this.disabled = false
    }.bind(this), 3000) // 此时定时器里面的this指向了btn
    // 这里使用this最好,不会随btn名称的变化而变化
    // bind()是在定时器的外面绑定的,所以这个this指的是btn这个对象
})

正确的写法(通过定义一个全局变量来改变this指向,不推荐):
btn.addEventListener('click', function () {
    this.disabled = true 
    var that = this
    setTimeout(function () {
        that.disabled = false
    }, 3000)
})
复制代码
比如我们有多个按钮,对每个按钮,当我们点击之后,就禁用这个按钮,并在3秒之后开启这个按钮
var btns = document.querySelectorAll('button')
for(var i = 0; i < btns.length; i++) {
    btns[i].onclick = function() {
         this.disabled = true  // 指向循环的每一个btn
         setTimeout(function(){
             // 这里不能用btns[i].disabled = false,因为随着for循环完,这里变成3
             // 除非把var改成let,形成块级作用域
             this.disabled = false
         }).bind(this),2000)
    }
}
复制代码

   this.function()代表立刻执function函数,this.function代表调用函数。

   扩展: call、bind、apply的区别

      MDN之slice方法

slice数组拷贝,如果数组里面是基本数据类型,则是深拷贝
const a = [1,2,3]
const b = a.slice()

b[0] = 4
// b [4,2,3]
// a [1,2,3]

如果数组里面是对象类型,则是浅拷贝
const d = [{name: '1',age: 20},{name: '2',age: 30}]
const e = d.slice()

d[0].name = "3"
// d和e中的第一个name属性的值都变成了3
复制代码

3. 字符串

   字符串具有一些和数组一样的方法:

var a = "foo";
var b = ["f","o","o"];
a.length; // 3
b.length; // 3

a.indexOf( "o" ); // 1
b.indexOf( "o" ); // 1

var c = a.concat( "bar" ); // "foobar"
var d = b.concat( ["b","a","r"] );  // ["f","o","o","b","a","r"]

JavaScript 中字符串是不可变的,而数组是可变的。
a[1] = "O";
b[1] = "O";

a; // "foo"
b; // ["f","O","o"]

charAt(1) 表示获取字符串第二位的值,不能赋值
a.charAt(1) = "O" // 此种方法会报错,因为是获取值而不是赋值

charCodeAt(index) 获取字符串第index位置上的字符的ASCII'Flavio'.charCodeAt(0) //70
'Flavio'.charCodeAt(1) //108
'Flavio'.charCodeAt(2) //97

扩展: 那么如何修改字符串对应位置的值呢?

// 1. 将第一个出现的指定字母转换为其它字母
"hello".replace("l","a") //'healo'

// 2. 将所有出现的指定字母转换为其它字母
"hello".replaceAll("l","a") //'heaao'

// 3. 将指定索引位置的字符转换为其它字符
// 比如将'hello'第三位字符变为'a'

// 方法一(通过字符串的substring方法去截取): 
// 自己封装的一个小方法:
function getString(string,index,char) {
  if(typeof string == 'string' && typeof index == 'number'  && index <= string.length) {
    return string.substring(0, index - 1) + char + string.substring(index, string.length)
  } else if(typeof string != 'string') {
   throw new Error("请输入字符串")
  } else if(index > string.length) {
   throw new Error("修改的索引值不能超过字符串长度")
  }
}

getString('hello',3,'a')

// 方法二(先将字符串转换为数组,对数组操作后,再转换为字符串)
// 就不封装方法了
const arr = "hello".split("")
arr[2] = "a"
arr.join("")
复制代码

   字符串反转(面试常考):

对于split方法需要特别主要的是:
将字符串转换为数组

// "hello world".split() // ['hello world']
// "hello world".split('') // ['h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd']
// "hello world".split(" ") // ['hello', 'world']

对于join方法需要特别注意的是:
将数组转换为字符串

//['h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'].join()  // 'h,e,l,l,o, ,w,o,r,l,d'
//['h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'].join() // 'hello world'

// 第一种方法:
var c = a
.split("")
// 将数组中的字符进行倒转
.reverse()
// 将数组中的字符拼接回字符串
.join("");

// 第二种方法:
var c = [...a].reverse().join("");

c; // "oof"

重点:字符串运用展开运算符:
[..."hello world"]   // ['h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd']

["hello world"].reverse().join("")  // 'hello world',因为数组只有一项
["hello world","a"].reverse()  // ['a', 'hello world']
["hello world","a"].reverse().join(""); // 'ahello world'
复制代码

   字符串不可变是指字符串的成员函数不会改变其原始值,而是创建并返回一个新的字符串。而数组的成员函数都是在其原始值上进行操作。

c = a.toUpperCase();
a === c; // false

a; // "foo"
c; // "FOO"

b.push( "!" );
b; // ["f","O","o","!"]
复制代码

  重点:虽然字符串没有这些函数,但可以通过“借用”数组的非变更方法来处理字符串:

a.join; // undefined
a.map; // undefined

使得字符串类型具有join的作用
// var c= [].join.call(a,"_");
var c = Array.prototype.join.call( a, "-" );

使得字符串类型具有map的作用
var d = Array.prototype.map.call( a, function(v){
  return v.toUpperCase() + ".";
} ).join( "" );

c; // "f-o-o"
d; // "F.O.O."

// 下面这种方式会报错,因为字符串具有不变性
Array.prototype.reverse.call( a );
复制代码

4. 数字

   JavaScript 中的数字字面量一般用十进制表示。

// 小数点前面的 0 可以省略:
var a = 0.42;
var b = .42;

// 小数点后小数部分最后面的 0 也可以省略:
var a = 42.0;
var b = 42.;

var a = 5E10;
a; // 50000000000

// 转换为指数形式
a.toExponential(); // "5e+10"

var b = a * a;
b; // 2.5e+21

var c = 1 / a;
c; // 2e-11
复制代码

   toFixed()方法和toPrecision()方法

var a = 42.59;

a.toFixed( 0 ); // "43"
a.toFixed( 1 ); // "42.6"
a.toFixed( 2 ); // "42.59"
a.toFixed( 3 ); // "42.590"
a.toFixed( 4 ); // "42.5900"

// 特别注意:
// 无效语法:
重点: JS引擎无法确定这里的`.`是什么意思,是点运算符(对象方法)还是浮点数
// 42.toFixed(3) 是无效语法,因为.被视为常量 42. 的一部分(如前所述),
// 所以没有.属性访问运算符来调用 toFixed 方法。
// 1.toString()同样也会因为这个原因而报错
42.toFixed( 3 ); // SyntaxError

// 下面的语法都有效:
(42).toFixed( 3 ); // "42.000"
0.42.toFixed( 3 ); // "0.420"
42..toFixed( 3 ); // "42.000"
42 .toFixed(3); // "42.000"

var a = 42.59;

// toPrecision(..) 方法用来指定有效数位的显示位数:
a.toPrecision( 1 ); // "4e+1"
a.toPrecision( 2 ); // "43"
a.toPrecision( 3 ); // "42.6"
a.toPrecision( 4 ); // "42.59"
a.toPrecision( 5 ); // "42.590"
a.toPrecision( 6 ); // "42.5900"

// 指数形式
var onethousand = 1E3; // 即 1 * 10^3
var onemilliononehundredthousand = 1.1E6; // 即 1.1 * 10^6
复制代码

   重点:判断0.1 + 0.2和0.3是否相等

// 通过比较两个数差值的绝对值是否小于机器精度2^-52 (2.220446049250313e-16)来判断
function numbersCloseEnoughToEqual(n1,n2) {
    return Math.abs( n1 - n2 ) < Number.EPSILON;
}

var a = 0.1 + 0.2;
var b = 0.3;

numbersCloseEnoughToEqual( a, b ); // true
numbersCloseEnoughToEqual( 0.0000001, 0.0000002 ); // false
复制代码

   最小/大整数和最大/小浮点数

Number.MAX_VALUE // 最大浮点数 1.798e+308
Number.MIN_VALUE // 最小浮点数 5e-324
Number.MAX_SAFE_INTEGER // 最大整数 2^53 - 1(9007199254740991)
Number.MIN_SAFE_INTEGER // 最小整数 -9007199254740991
复制代码

   整数/安全的整数判断

// 判断一个值是否为整数
Number.isInteger( 42 ); // true
Number.isInteger( 42.000 ); // true
Number.isInteger( 42.3 ); // false

// 判断一个值是否为安全的整数
Number.isSafeInteger( Number.MAX_SAFE_INTEGER );  // true
Number.isSafeInteger( Math.pow( 2, 53 ) );  // false
Number.isSafeInteger( Math.pow( 2, 53 ) - 1 );  // true
复制代码

5. 特殊数值

   undefined和null的比较

null 指空值(empty value)
• undefined 指没有值(missing value)

• undefined 指从未赋值
• null 指曾赋过值,但是目前没有值

• null 是一个特殊关键字,不是标识符,我们不能将其当作变量来使用和赋值。
• undefined 却是一个标识符,可以被当作变量来使用和赋值。
复制代码

   使用void运算符返回undefined

void让表达式返回undefined
var a = 42;
console.log(void a, a ); // undefined 42
复制代码

   不是一个数字的数字NaN

NaN和自身不相等

NaN == NaN // false
NaN === NaN // false
复制代码

   window.isNaN()用来判断参数是否不是NaN,也不是数字(即用于判断参数是否不为除NAN以外的数字),Number.isNaN()用来判断参数是否为NaN

var a = 2 / "foo";
var b = "foo";

a; // NaN
b; // "foo"

window.isNaN( a ); // true
window.isNaN( b ); // true
window.isNaN(2); // false

Number.isNaN( a ); // true
Number.isNaN( b ); // false

NaNJavaScript 中唯一一个不等于自身的值,因此可以进行判断:

function isNaN(n) {
    return n !== n;
};
复制代码

   无穷数

var a = 1 / 0; // Infinity(Number.POSITIVE_INfINITY)
var b = -1 / 0; // -Infinity(Number.NEGATIVE_INfINITY)

var a = Number.MAX_VALUE; // 1.7976931348623157e+308
a + a; // Infinity
a + Math.pow( 2, 970 ); // Infinity
a + Math.pow( 2, 969 ); // 1.7976931348623157e+308

Infinity / Infinity // NaN
复制代码

   -0的奇怪操作

加法和减法运算不会得到负零
// 负零在开发调试控制台中通常显示为 -0,但在一些老版本的浏览器中仍然会显示为 0
var a = 0 / -3; // -0
var b = 0 * -3; // -0

-0从数字转为字符串会得到"0"
a.toString(); // "0"
a + ""; // "0"
String( a ); // "0"
JSON.stringify( a ); // "0"

"-0"从字符串转换为数字,会得到正确的结果
+"-0"; // -0
Number( "-0" ); // -0
JSON.parse( "-0" ); // -0

-0 == 0  // true
-0 === 0 // true
"-0" === "0" // false
复制代码

6. 值和引用

var a = 2;
var b = a; // b是a的值的一个复本

b++;

a; // 2
b; // 3

var c = [1,2,3];
var d = c; // d是[1,2,3]的一个引用

d.push( 4 );
c; // [1,2,3,4]
d; // [1,2,3,4]
复制代码
一种特殊情况是,重新分配地址
var a = [1,2,3];
var b = a;

a; // [1,2,3]
b; // [1,2,3]


b = [4,5,6];

a; // [1,2,3]
b; // [4,5,6]
复制代码

  重新分配地址的应用: 用于交换

                  image.png

2. 原生函数

1. Object.prototype.toString.call方法

  重点:深度解析Object.prototype.toString.call(obj)的功能及原理(比typeof判断得到的数据类型更加全面)

  所有数据类在继承Object的时候,改写了toString()方法,所以都拥有一套属于自己的toString()方法。当删除对应数据类原型上的toString()方法时,就会向上查找,继承Object原型链上的toString()方法,用于区分不同的数据类别。

// 定义一个数组
var arr = [1, 2, 3]
// 数组原型上是否具有 toString() 方法
console.log(Array.prototype.hasOwnProperty('toString')) //true
// 数组直接使用自身的 toString() 方法
console.log(arr.toString()) // '1,2,3'
delete操作符删除数组原型上的 toString()
delete Array.prototype.toString
// 删除后,数组原型上是否还具有 toString() 方法
console.log(Array.prototype.hasOwnProperty('toString')) //false
删除后的数组再次使用 toString() 时,会向上层访问这个方法,即 ObjecttoString()

```重点1:``` 此时因为改变了原型链上Object.toString()的指向,指向了arr,所以返回'[object Array]'
console.log(arr.toString()) // '[object Array]'

否则,Object原型链上的toString方法的参数无论是什么(只要不是给没定义的值),都会返回'[object Object]':
因为它都指向Object:
Object.prototype.toString(undefined) // '[object Object]'
Object.prototype.toString(nul) // 'Uncaught ReferenceError: nul is not defined'
Object.prototype.toString('123') // '[object Object]'
Object.prototype.toString([]) // '[object Object]'
Object.prototype.toString({}) // '[object Object]'

```重点2:``` 构造函数上的toString()方法返回本身,且调用的是Function原型链上的方法,
             结果只与构造函数的名称有关,与传递的参数无关。
             
需要注意区分:
Object.prototype.toString('123') // '[object Object]'
Object.toString()  // 'function Object() { [native code] }'

Array.toString()  // 'function Array() { [native code] }'
Function.toString()  // 'function Function() { [native code] }'

Object.toString({a: 1}) === Object.toString({b:2}) // true
复制代码

2. 封装对象包装及拆封

  包装的方法一:js中的原生(内建)函数String()java中的字符串构造函数String()的对比(构造函数名称的首字母必须大写)

                image.png

1.a是继承了string原型的一个包装类,其本质是对象。当a找不到String的方法时,就会向上查找继承的原型,从而使用字符串的方法。
2.String构造函数中的参数需要可以隐式转换为字符串,相当于调用自身数据类的toString()方法
3.String构造函数只接受一个参数
const c = new String(1,2,3)  // String {'1'}
a == 'abc' // true 这里为true的原型是因为使用了隐式转换,调用了字符串的toString方法
a === 'abc' // false 这里不能强制隐式转换
a.toString() // 'abc'
a.toString() === 'abc' // true
typeof a.toString() // 'string'
a.length // 3
b.length // 3
复制代码

                 image.png

  包装的方法二:直接使用Object()封装对象的包装

var a = "abc";
var b = new String( a );
var c = Object( a );

typeof a; // "string"
typeof b; // "object"
typeof c; // "object"

```重点:```
a instanceof String; // false
b instanceof String; // true
c instanceof String; // true

Object.prototype.toString.call(b); // "[object String]"
Object.prototype.toString.call(c); // "[object String]"

b == c // false, 因为地址不一样
b === c // faslse, 因为地址不一样

a == b // true,触发隐式转换,b会调用valueOf()方法
a === b // false,不会触发隐式转换
复制代码

  给Object()方法传入不同的参数类型,会得到不同类型的包装类。传入什么参数类型(除null和undefined),就会返回什么参数类型且值为自身的包装类。

                  image.png

                  image.png

  拆封的方法一: 使用valueOf()方法

var a = new String( "abc" );
var b = new Number( 42 );
var c = new Boolean( true );

a.valueOf(); // "abc"
b.valueOf(); // 42
c.valueOf(); // true
复制代码

  拆封的方法二: 使用加号运算符的隐式拆封

var a = new String( "abc" );
var b = a + ""; // b的值为"abc"

typeof a; // "object"
typeof b; // "string"
复制代码

3. 暗号-北京烤鸭

  这一小节主要用于记录探索js中一些不为人知的秘密:

  (1)valueOf方法: valuOf方法的传送门

1.对于valueOf方法,我们可以手动进行重写:
var a = {}
a.valueOf = function(){return true}
1 + a  // 2
2.(重点) 在valueOf方法下,还有一个比较底层的方法`Symbol.toPrimitive`,可以重新定义数据类型构造函数的原型对象的方法(优先级
高于valueOf):
复制代码

  对于对象构造函数的原型对象方法:

        image.png

  对于字符串构造函数的原型对象方法:

         image.png

  (2){}.toString()报错原因分析: 理解js机制(把自己想象成机器)

// https://www.zhihu.com/question/55823852
1 + {}  // '1[object Object]',{}调用valueOf()方法,没得到基本的值类型,继续调用toString()方法
{} + 1  // 1  +号左边的{}被视作了空代码块,执行了 + 1
{a: 1} + 1 // 1 理由同上
({}) + 1  // '[object Object]1'
复制代码

  (3)Number({})和Number([])

Number({}) // NaN, 这是因为{}会调用toString()方法,得到'[object Object]',再进行类型转换
Number([]) // 0, []调用toString()方法,得到'',再转换为Number类型
复制代码

  (4)[] == [] 和 {} == {}: 引用地址不同

[] == [] // false
{} == {} // false
[] === [] // false
{} === {} // false
复制代码

  (5)[] == ![]类似问题解析: 问题原理

重点:!可将变量转换成boolean类型,nullundefinedNaN以及空字符串('')取反都为true,其余都为false。
!{} // fasle
![] // false
[] == ![] // true, [] == false => '' == 0 => 0 == 0 => true
{} == !{} // false, '[object Object]' == false => NaN == 0 => false
[] == !{} // true, [] == false => true
{} == ![] // false, '[object Object]' == false => false
复制代码

  (6)+号运算符的理解: 加号运算符的认知

加号运算符存在隐式转换,单独对某一个数据类型使用加法运算符,会将其转换为数字类型
1.当“+”号左右两边都是数值时,有以下特殊情况:
(1). 当有一个操作数是NAN,则结果是NAN;
(2). 如果是Infinity加上Infinity,则结果是InfinityInfinity表示无穷大);
(3). 如果是 - Infinity + -Infinity,则结果是-InfinityInfinity表示无穷大);
(4). 如果是 Infinity + -Infinity,则结果是 NaN;
2.字符串 + 其他基本数据类型和function类型,字符串优先级最高,直接拼接;
3.字符串 + 引用类型数据(除function类型),先调用自身toString()方法,再相加;
'1' + {name: 'fl'} // '1[object Object]'
'1'+ [1,2,3] // '11,2,3'
4.数字 + 其他非字符串的原始数据类型
1 + true  // 2
1 + null // 1
1 + undefined // NaN
5.当数字与字符串以外的,其他原始数据类型直接使用加号运算时,就是转为数字再运算,这与字符串完全无关。
Number(undefined) NaN
Number(NaN) NaN
Number(null) 0
true + true // 2
true + null // 1
undefined + null // NaN
6.空数组 + 空数组以及空对象 + 空对象
[] + [] // '',先调用valueOf方法,再调用toString()方法,''
({}) + {} // "[object Object][object Object]",原理同上,如果对象不加括号,在不同浏览器会得到不同结果
// 主要是两种,"[object Object][object Object]" 以及 NaN
7.空对象+空数组 VS 空数组+空对象
{} + []  // {}被视为代码块,[]强制转换为number类型,得到0
({}) + [] //'[object Object]'
[] + {} //'[object Object]'
[] + {} == ({}) + []  // true,比较的是字符串
[] + {} === ({}) + [] // true
8.data类型
(1) valueOf方法返回值: 给定的时间转为UNIX时间(自1 January 1970 00:00:00 UTC起算),但是以微秒计算的数字值;
(2) toString方法返回值: 本地化的时间字符串;
(3) 其他数据类型在做加法运算的转换时都会先调用valueOf()方法,再调用toString()方法。而data类型在做加法运算的转换时,会调用toString()方法;

1 + (new Date()) // '1Sat Jun 25 2022 11:04:18 GMT+0800 (中国标准时间)'
+new Date() // 1656126287960,将字符串类型强制转换为数字类型
复制代码

4. 回归正传: 原生函数作为构造函数

  使用构造函数new Array()创建数组的弊端

使用Array()或者new Array()构造函数创建数组,两者会得到相同的结果
var a = new Array(1,2,3) // [1,2,3]
var b = [1,2,3] // [1,2,3]

弊端在于:
(1) 当传入的参数只有一个值时,他会默认这个传参为数组的长度,而非给数组赋值,得到一个空数组:
    var a = new Array(3) // (3) [empty × 3]
(2) 构造函数得到的空数组无法使用map进行遍历,因为数组中并不存在任何单元:
    a.map((item,index) => index) // (3) [empty × 3]
    var b = [undefined,undefined,undefined]
    b.map((item,index) => index) // (3) [0, 1, 2]
(3) 所以,尽量不要创造空数组,或者使用`apply()`函数创造包含`undefined`单元的数组:
    var a = Array.apply( null, { length: 3 } )  // (3) [ undefined, undefined, undefined ]
(4) 当传入的参数不止一个值时,它会返回一个包裹这些参数的数组;
(5) 也可以使用Array.of(根据传递的参数)来创建数组
    Array() // []  
    Array(3// [, , ,]  
    Array(3118// [3, 11, 8]  

    Array.of() // []  
    Array.of(3// [3],相比构造函数的好处是,只传递一个参数不会被视为数组长度,
    //因此除了不传递参数外,不会产生空数组,方便后续数组的循环操作
    Array.of(3118// [3,11,8]  

    Array.of(3).length // 1   
    Array.of(undefined// [undefined]  
复制代码

  Array.apply的用法:

  (1) Array的原型链上并不存在apply方法,但因为它是构造函数,是函数的实例。而apply是Function原型链Function.prototype上的方法,即Array.__proto__ === Function.prototype。因此Array中找不到apply方法时,会根据原型链逐级向上查找的原则,找到Array.__proto__中的apply方法。可以调用apply方法,改变this指向,并调用Array构造函数。

  (2) 从ES5开始,apply的参数可以是一个通用的类数组对象,而不是必须为一个数组。

            image.png

5. 重中之重:类、构造函数、原型对象、原型链

  黑马js进阶-类、原型链

  原型链部分笔记

  借用公司大佬的图片镇楼 image.png

在此记录b站未列出的知识点
typeof Number.prototype // 'object'
typeof String.prototype // 'object'
typeof Array.prototype // 'object'
typeof Object.prototype // 'object'

1. 比较特殊的是函数的原型对象 
typeof Function.prototype // 'function'

2. 也只有这个比较特殊(注意__proto__左右两边都是两下划线)
Function.prototype === Function.__proto__  // true

3. 任何构造函数的原型对象都是函数的原型对象
Array.__proto__ === Function.__proto__  // true
Object.__proto__ === Function.__proto__ // true
所以,任何构造函数的原型对象都是完全相同的
Array.__proto__ === Object.__proto__ // true

4. 同时,也因为相等性,可以转换为如下式子
Array.__proto__ === Function.prototype // true

5. 原型对象上的constructor方法用于查看原型对象上的构造方法
typeof(Object.prototype.constructor)  // 'function'
Object.prototype.constructor  // ƒ Object() { [native code] }
Array.prototype.constructor   // ƒ Array() { [native code] }
Function  // ƒ Function() { [native code] }
也可以创建一个原型对象,两者本质是相同的
const superman= {name: 'fl'}
superman.constructor  // ƒ Object() { [native code] }
所以,它们之间的比较是不相等的
Array.prototype.constructor === Object.prototype.constructor  // false
Array.prototype.constructor === Function // false
因此,它们之间的比较:
Array.prototype == Function.prototype // false
Array.prototype == Object.prototype  // false

原型对象上的constructor指向自身的构造函数
Array.prototype.constructor === Array // true
Object.prototype.constructor === Object // true

6. 但是对每个类型的构造函数:
Array.constructor // ƒ Function() { [native code] }
Object.constructor // ƒ Function() { [native code] }
Function.constructor // ƒ Function() { [native code] }

任何一个类型构造函数的constructor都指向Function,因为他们都是Function的实例
Array.constructor === Function // true
Array.constructor === Function.constructor // true,可以一直迭代,因为Function.constructor指向Function
Array.constructor === Function.constructor.constructor.constructor.constructor // true
Array.constructor === Object.constructor // true
复制代码

6. 函数闭包

  高阶函数

1.第一种,函数作为参数传递给另一个函数进行调用,一般用于回调
function sum(a,b,callback) {
    callback && callback(a,b)
}
sum(1,2,function(a,b){
    console.log(a + b)
    return a + b
})
2.第二种,一个函数中返回另外一个函数(主要用于闭包,所以闭包本质是一种高阶函数)
function fn() {
    return function() {}
}
fn()
复制代码

  变量作用域

变量根据作用域的不同分为两种: 全局变量和局部变量。
1. 函数内部可以使用全局变量。
2. 函数外部不可以使用局部变量。
3. 当函数执行完毕,本作用域的局部变量会销毁。
复制代码

  闭包

(1) 闭包的定义:有权访问另一个函数作用域中变量的函数  -来自JS高级程序设计的定义
   简单来说,就是一个作用域可以访问另一个函数内部的局部变量。
(2) 闭包的主要作用:延伸了变量的作用范围,被引用的变量不会被释放,未被引用的变量会被释放。
(3) 闭包的主要特点:
    1.函数套函数,闭包一定有嵌套函数;
    2.外层函数一定有局部变量,且内层函数一定操作了外层函数的这个变量;
    3.内层函数会使用return返回(如果不返回这个内层函数,你就没办法使用这个闭包,返回内层函数的最终的目的就是让外部可以访问到这个闭包);
(4) 立即执行函数中的this指向window1.在外层函数中执行定义好的内层函数,此时内层函数可以访问外层函数作用域中的变量:
function fn() {
   var num = 10
   function fun() {
       // fun中的作用域可以访问到fn中的变量num
       // 那么这个函数fn()就是一个闭包
       console.log(num)
   }
   fun()
}

// 执行函数
fn()

2.直接在外层函数中返回定义好的内层函数,此时全局作用域中的f()访问到fn()中的局部变量num:
function fn() {
   var num = 10
   function fun() {
      // 内层函数访问到外层函数作用域中的变量
      console.log(num)
   }
   return fun
}

var f = fn() // 此时返回的是一个函数,如需调用,还要在后面再加上一个()执行
f() //

3.直接在外层函数中返回匿名内层函数,此时全局作用域中的f()访问到fn()中的局部变量num:
function fn() {
   var num = 10
   return function() {
      // 内层函数访问到外层函数作用域中的变量
      console.log(num)
   }
}

var f = fn() // 此时返回的是一个函数,如需调用,还要在后面再加上一个()执行
f()
复制代码

  闭包的应用(面试常考,使用闭包和立即执行函数获取当前li的索引):

点击li输出当前li的索引号
var lis = document.querySelector('.nav').querySelectorAll('li')
for(var i = 0; i < lis.length; i++) {
    lis[i].onclick = function() {
        1. 因为有4个li,for循环是同步执行,点击li是异步执行,所以最后使用var声明的变量i最后为4,分别点击,输出44
        2. 如果想解决这个问题,第一种方法,可以使用let来声明变量
        3. 第二种方法,可以给每个li增加一个index属性,点击打印每个li的index值即可
        // lis[i].index = i
        // console.log(this.index)  此时的this指向每个li
        console.log(i) 
    }
}

4. 第三种方法,使用闭包和立即执行函数相结合的方式获取li的索引,传入一个参数i,并接收它
var lis = document.querySelector('.nav').querySelectorAll('li')
for(var i = 0; i < lis.length; i++) {
    (function(i) {
        lis[i].onclick = function() {
            // 内层函数使用到了外层函数中的变量i
            console.log(i)
        }
    })(i)
}
复制代码

3. 碎碎念

1. 浅谈Object.assign()方法

  浅析Object.assign()方法的作用: 接收多个参数来合并对象,如果有相同的属性,后面的值会覆盖前面的值,第一个参数如果是变量,那么它的值也会发生变化。

1. 合并多个对象
   Object.assign({name: 'fl'}, {age: 28}, {sex: '男'}) // {name: 'fl', age: 28, sex: '男'}
2. 如果不传递任何参数,则会报错
3. 如果只传递一个参数,则相当于赋值
   var a = {name: 'fl'}
   var b = Object.assign(a)
   b // {name: 'fl'}
   a.name = 'admin'
   a // {name: 'admin'}
   b // {name: 'admin'}
   或者
   b.name = 'admin'
   a // {name: 'admin'}
   b // {name: 'admin'}
   重点:
   (1) Object.assign(10) // 会强制转换为对象(包装类) Object(10)
    // 因为null或者undefined没有对应的构造函数,所以他们无法被包装
   (2) Object.assign(null) // 报错,Cannot convert undefined or null to object
   (3) Object.assign(undefined) // 报错,Cannot convert undefined or null to object
   (4) Object(null) // {},得到一个空对象
 4. 接受多个参数的情况下,如果将定义的变量放在第一个参数,则这个参数会是合并的结果,同时也相当于赋值
   var a = {name: 'fl'}
   var b = Object.assign(a, {age: 28, sex: '男'})
   a // {name: 'fl', age: 28, sex: '男'}
   a === b // true
 5. 接受多个参数的情况下,如果将定义的变量不放在第一位,则这个参数的值不会被赋值
   var a = {name: 'fl'}
   var b = Object.assign({age: '28',job: 'front'}, a)
   b  // {age: '28', job: 'front', name: 'fl'} 
   a  // {name: 'fl'}
 6. (重点)如果非对象参数出现在源对象的位置(即非首参数),那么处理规则有所不同。
    首先,这些参数都会转成对象,如果无法转成对象,就会跳过。
    这意味着,如果 `undefined` 和 `null` 不在首参数,就不会报错。
    let obj = { a: 1 } 
    Object.assign(obj, undefined, null) === obj // true
 7. (重点)布尔值,字符串,数值都会被转为包装对象,但是布尔值,数值转成的包装对象没有枚举属性
    所以被忽略, 只有字符串的包装对象会产生可枚举属性,所以字符串会被合入目标对象。
    const a = 'abc'
    const b = true 
    const c = 10 
    const obj = Object.assign({}, v1, v2, v3)   // { "0": "a", "1": "b", "2": "c" }
 8. (重点)参数可以是数组
    Object.assign([1, 2, 3], [4, 5])   // [4, 5, 3]
复制代码

  Object.assign的应用:浅拷贝对象

// a和b外层地址不一样,其余都一样
// 改变a中的基本数据类型时,b对应的基本数据类型不会发生变化
// 改变b中的引用数据类型时,a和b对应的引用数据会同时变化,因为他们指向同一个地址。
var a = {name: 'fl'}
var b = Object.assign({},a)
复制代码

  Object.assign的特别注意事项:当定义的变量为参数之一,且变量里面有引用类型,且合并结果中的引用类型取的是变量中的引用类型地址。那么合并结果中引用类型的key和value会与原变量相互影响,因为他们指向同一个地址。

(1) 相当于赋值
var a = {name: 'fl',job: ['front']}
var b = Object.assign(a, {name: 'admin'}) 
a === b // true
(2) 浅拷贝(拷贝了a中的job属性,这个属性是地址)
var b = Object.assign({name: 'admin'}, a) // 后面覆盖前面的,浅拷贝
a === b // false
a.job === b.job  // true
复制代码

  Object.assign变量地址改变的实例:

var a = {name: 'fl',job: ['front']}
var b = Object.assign({},a)
a.job === b.job // true
b.job = ['IT'] // 重点:此时b发生变化,a没变,这是因为给b重新分配了地址
b.job[0] = 'IT' // 重点:此时a和b都发生了变化,这是因为它们地址相同
复制代码

2. Object.defineProperty()

  方法的定义(如果prop不存在,则是给对象添加新属性;如果prop存在,则是修改原属性的值): image.png

var obj = {
   id: 1,
   name: 'fl'
} 
Object.defineProperty(obj, 'sex', {
   value: 'male'
})

// {id: 1, name: 'fl', sex: 'male'}

配置属性:
1. writable属性: 指定对象的某个属性是否能被修改,默认为false
2. enumerable属性: 指定对象的某个属性是否可以使用Object.keys进行遍历,默认为false
3. configurable属性: 指定对象的某个属性是否可以被删除或者重新修改定义,默认为false
复制代码

  (重点)Object.defineProperty函数中的get与set:

Tips:personObj的sex属性其实已经发生过变化,只不过在重新打印的过程中,触发了personObj中sex属性的get方法,又重置了sex属性的值。

              image.png

3. 浅析Object.create()方法

1.Object.create()方法有两个参数proto、propertiesObject
    第一个参数是必须的,是新建对象的原型对象
    第二个参数是可选的,是新创建的实例对象上的属性
    // 原型对象上的属性
    var skill = {
       sing: function() {console.log("他唱歌贼溜")},
       game: function() {consol.log("他游戏贼溜")}
    }
    var personObj = Object.create(skill, {
         // 对象上的属性,使用{}包裹自定义的属性
            name: {
               value: 'fl'// 下面还有四个参数可选,和Object.defineProperty的参数一样
               ```
               writable: false // 设置该 key 的值是否可改/写,默认为 false。
               enumerable: false, // 是否可以枚举出内部属性,默认为 false,是否可以使用object.keys遍历
               configurable: false // 默认为false
               ```
            },
            age: {
                value: '28'
            }
        }
    )

    var student = {
        school: '北京大学'
    };
    var stu = Object.create(student, {
        name: {
            value: '张三',
            writable: true,
            configurable: true,
            enumerable: true
        }
    });
    console.log(stu);//{name: "张三"}
    console.log(stu.__proto__ === student); //true:stu的原型对象为student
    console.log(stu.school); //北京大学
    for (var key in stu) {
        console.log(key); //name school
    }
2. new Object()也是用于创建对象,但是new Object()生成对象的速度要比Object.create()快大约433. 同理,也可以使用原型对象.`constructor`创建对象,这个等价于new Object(),会指向到Object构造函数
   const name = new {}.constructor('fl')
4. 最后,也可以使用`ES6`Reflect的construct方法来创建对象(第一个参数必须为构造函数,且必须有两个参数,不然会报错)
   当第一个参数为Object构造函数时,Reflect中的第二个参数必须为类数组结构(且必须为数组才能成功创建对象),否则会报错
   Reflect.construct(Object,[{name: 'fl'}]) // 和 new Object({name: 'fl'})一样
   // new Object({})、new Object(null或者undefinde)不会报错,只是返回空对象
   Reflect.construct(Object,{name: 'fl'}) // 和 new Object({})、new Object(null或者undefinde)一样
   Reflect的construct方法第二个参数只接收数组中的第一个值
   Reflect.construct(Object,[1,2,3,4]) // Number {1}  
   Reflect.construct(Object,['fl','admin']) // String {'fl'}
复制代码

               image.png

               image.png

               image.png

               image.png

4.对象定义中的get和set

  敲代码偶然间发现的知识点,以前从未见过,特此记录下。个人理解比较像vue计算属性中的getset:

const person = {
  nickName: 'fl',
  // 如果前面有定义name字段,则会被下面的这个方法覆盖掉
  // 获取name的值会调用这个函数
  get name() {
    console.log('我在获取name的值',this.nickName)
    // return this.name 这样会报栈溢出的错误,因为会一直调用自身
    return this.nickName
  },
  set name(val) {
    console.log('我在修改name的值',val)
    this.nickName = val
  }
}

person.name  // 触发name的get方法,给perosn的name字段赋值(结果见图一)
// 再次输入person,会触发name的get方法(结果见图一)

person.name = 'admin' // 触发name的set方法,修改perosn的nickName字段的值(结果见图二)
// 再次输入person,会触发name的get方法(结果见图二)
复制代码

                image.png

                image.png

5.Object.freeze方法解析: Object.freeze传送门

1. Object.freeze方法用来冻结数组或者对象,但它本身属于浅冻结
var obj = {
    name: '张三',
    info: {
        a: 1,
        b: 2
    }
}
Object.freeze(obj)
obj.name = '李四'
console.log(obj)    // {info: {a: 1, b: 2},name: "张三"}
obj.info.a = 66
console.log(obj.info)   // {a: 66, b: 2}

2. 使用递归的思想封装深冻结方法
function deepFreeze(obj) {
    // 得到一个获取所有属性的数组
    var propNames = Object.getOwnPropertyNames(obj)
 
    // 遍历属性数组
    propNames.forEach(item => {
        var prop = obj[item]
        // 如果某个属性的属性值是对象,则递归调用
        if (prop instanceof Object && prop !== null) {
            deepFreeze(prop)
        }
    })
    // 冻结自身
    return Object.freeze(obj)
}
复制代码
1.Object.freeze方法在vue中的应用,使data中的变量不具有响应式的效果,提升网页性能。
 //https://blog.csdn.net/muzidigbig/article/details/122121002

new Vue({
    data: {
        // vue不会对list里的object做getter、setter绑定
        list: Object.freeze([
            { value: 1 },
            { value: 2 }
        ])
    },
    created () {
        // 界面不会有响应
        this.list[0].value = 100;
 
        // 下面两种做法,界面都会响应
        this.list = [
            { value: 100 },
            { value: 200 }
        ];
        this.list = Object.freeze([
            { value: 100 },
            { value: 200 }
        ]);
    }
})
复制代码

6. ES6之Proxy、Reflect:阮一峰ES6——非官网(风格比较淡雅)

关于Reflect`apply`:
const ages = [11, 33, 12, 54, 18, 96]

旧写法:
const youngest = Math.min.apply(Math, ages);
const oldest = Math.max.apply(Math, ages)
const type = Object.prototype.toString.call(youngest)

新写法:
const youngest = Reflect.apply(Math.min, Math, ages)
const oldest = Reflect.apply(Math.max, Math, ages)
const type = Reflect.apply(Object.prototype.toString, youngest, [])    // '[object Number]'

重点:这句话的意思就是调用`Object.prototype.toString`方法,并将this指向youngest 11。因此等价于
Object.prototype.toString.call(youngest, [])   // '[object Number]', 与第二个参数的类型无关

关于Math.floor()向下取整的四种调用方法:
1.直接调用Math.floor()  无参数会返回NaN,不是Number类型的参数会自动转换为Number类型:
Math.floor([1.75])  // 1
2.通过使用call、apply、bind方法执行(在此只演示call方法):
Math.floor.call(undefined,1.75) // 1
3.通过apply和call`(狗都不愿意使用系列)`这两个方法来调用:
Function.prototype.apply.call(Math.floor,undefined,[1.75]) // 1
怎么理解这行代码呢? 
(1) Function.prototype.apply可以看作是Function原型链上的apply方法;
(2) 后面的call方法是调用Function原型链上的apply方法,call方法需要指定一个对象Math.floor来改变this的指向;
(3) 整行代码就变成了Math.floor.apply(undefined,[1.75]),最后就得到了结果14.通过ReflectAPI进行调用(因为使用了apply方法,因此特别注意,传参需要使用数组的形式):
Reflect.apply(Math.floor,undefined,[1.75]) // 1
复制代码
关于`proxy`: 个人理解是将person与personProxy之间做了一层浅拷贝,会相互影响
但只有修改或者赋值personProxy的属性时,才会触发代理拦截的get和set方法
let person = {
  name: 'fl',
  age: 28,
  skill: {
    sing: 'rainbow',
    game: 'Glory of Kings'
  }
}

let handler = {
  get(obj,key) {
    // 切记这个key值是最外层的属性
    return key in obj ? obj[key] : 666
  },
  set(obj,key,val) {
    obj[key] = val
  }
}

let personProxy = new Proxy(person,handler)

此处为undefined的原因是,personProxy.skill(skill为person的key,返回一个对象)
然后对象中找不到eat这个属性,所以返回undefined 
personProxy.skill.eat   // undefined
personProxy.skill.sing  // 'rainbow'
复制代码
拓展: JS引擎无法确定这里的`.`是什么意思,是点运算符(对象方法)还是浮点数(对应上文`toFixed`)
123.ad  // 报错
123.0.ad // undefined
(123).ad // undefined
123..ad // undefined
var age = 28
age['name'] = 'fl'
age['name']  // undefined
复制代码

  关于Object.defineProperty和Proxy的比较:

传送门:`https://blog.csdn.net/yuqing1008/article/details/104421552`

个人总结:
1. Object.defineProperty中的`get``set`方法是针对对象/数组中存在的字段属性触发的。如果有新增字段,
   又想达到响应式的效果,需要再次手动`observe`。如果删掉原对象/数组中的值,则再次访问或者赋值也不会触发方法。
2. Object.defineProperty之所以不能通过`数组.push`方法来触发`set`方法的原因是,因为会先调用get方法,
   获取数组的值,然后手动增加了一个新的索引值,而这个索引值在第一次`observe`时时没有的,无法触发set方法。
   所以,`vue`通过修改索引的方式重构了数组原型链上的方法,在`vue2`中使用`defineProperty`作为双向绑定原
   理的底层方法。
3.`proxy``defineProperty`最大的不同之处至于,`proxy`可以监测到对象/数组上新增的字段,并触发get
   和set方法(包括数组上的一些方法,比如`push`,因为它能监测到新增的key)。所以这也是`vue3`使用`proxy`
   做响应式的原因。
4. 对于对象,`key`是键名,`value`是值;对于数组,`key`是索引,`value`是值。
 
传送门:`https://juejin.cn/post/7069397770766909476#heading-4`

1. 使用`Object.defineProperty``vue`响应式:
(1) 错误的定义(`无限循环调用,导致栈溢出的结果`):
let person = {name: 'fl', age: 28}

Object.keys(person).forEach(function (key) {
    Object.defineProperty(person, key, {
        enumerable: true,
        configurable: true,
        // 默认会传入this
        get() {
            // person[key]会触发get方法,get方法又会获取person[key]的值,最终导致栈溢出
            return person[key]
        },
        set(val) {
            console.log(`对person中的${key}属性进行了修改`)
            person[key] = val
            // 修改之后可以执行渲染操作
        }
    })
})

(2) 正确的定义(`未实现数组的响应式`):
 既然`person[key]`又会触发`get`方法,干脆直接将这个作为值传递到函数中进行调用,于是有了下面的思路:
 
 // 实现一个响应式函数
function defineReactive(obj, key, val) {
  //如果某对象的属性也是一个对象,递归进入该对象,进行监听
  if (typeof val === 'object') {
    observer(val)
  }
  Object.defineProperty(obj, key, {
    get() {
      console.log(`访问了${key}属性`)
      return val
    },
    set(newVal) {
      // 如果newVal是一个新对象,递归添加对象属性,以达到响应式效果
      // 因为defineProperty是无法对新添加的对象属性进行响应式监听的
      if (typeof newVal === 'object') {
        observer(key)
      }
      console.log(`${key}属性被修改为${newVal}了`)
      val = newVal
    }
  })
}

// 实现一个遍历函数Observer
function observer(obj) {
  //如果传入的不是一个对象,则直接return
  if (typeof obj !== 'object' || obj === null) {
    return
  }
  Object.keys(obj).forEach((key) => {
    defineReactive(obj, key, obj[key])
  })
}

传送门:`https://blog.csdn.net/pagnzong/article/details/120389514`

2. 使用`proxy``vue`响应式:
// 创建响应式
function reactive(target = {}) {
  if (typeof target !== 'object' || target == null) {
    // 不是对象或数组,则返回
    return target
  }

  // proxy的代理配置,单独拿出来写
  const proxyConf = {
    get(target, key, receiver) {
      // 只处理本身(非原型的)属性
      const ownKeys = Reflect.ownKeys(target)
      if (ownKeys.includes(key)) {
        console.log('get', key) // 监听
      }

      const result = Reflect.get(target, key, receiver)
      // 深度监听,vue获取数据,会调用get方法,收集依赖
      // 如果不递归调用,则只会触发一次get方法,获取对象类型的数据需要多次调用get方法
      return reactive(result)
    },
    set(target, key, val, receiver) {
      // 重复的数据,不处理
      if (val === target[key]) {
        return true
      }

      const ownKeys = Reflect.ownKeys(target)
      if (ownKeys.includes(key)) {
        console.log('已有的 key', key)
      } else {
        console.log('新增的 key', key)
      }

      const result = Reflect.set(target, key, val, receiver)
      console.log('set', key, val)
      return result // 是否设置成功
    },
    deleteProperty(target, key) {
      const result = Reflect.deleteProperty(target, key)
      console.log('delete property', key)
      return result // 是否删除成功
    }
  }

  // 生成代理对象
  return new Proxy(target, proxyConf)
}

// 测试数据
const data = {
  name: 'xiaoming',
  age: {
    young: 18,
    old: 26
  }
}

const proxyData = reactive(data)
复制代码

7. 深拷贝 vs 浅拷贝 (重点)

  浅拷贝:个人理解浅拷贝就是对数据a做拷贝得到数据b。a和b除了最外层的地址不一样,其他都一样。就好比相同规格的风扇一样,零件的构造完全相同,不同的只是包装袋。因此,a的基本数据类型值发生变化时,b对应的基本数据类型的值不会发生变化。而a或者b中的引用数据类型发生变化时,另外一个引用数据类型的值也会发生相应变化,因为他们指向的是相同的地址。

  常见的浅拷贝类型:

传送门: https://yang-zebo.github.io/guide/JavaScript/ECMAScript/04/04_02.html

(1) 对于数组:
var arr = [1, 2, 3]

1.`第一种方式`var arr1 = arr.slice()  // 如果不传参数,那么会返回一个一样的新数组,完成浅拷贝

2.`第二种方式:`
var arr2 = [].concat(arr) // 拼接数组,创建一个空数组,并把原数组拼接上去,完成浅拷贝

3.`第三种方式:`
var arr3 = [...arr] //展开运算符进行浅拷贝

4.`第四种方式:`
var arr4 = Array.from(arr)

5.`第五种方式:`
var arr5 = _.clone(arr)  // 使用lodash的_.clone方法

6.`第六种方式:`
var arr6 = Object.assign([],arr) // 使用Object.assign对数组做浅拷贝

PS: Array.from 只针对类数组数据类型(`数组、集合、字符串、函数传参arguments等`)进行数组的转换
Array.from({name: 'fl',sex: 'male'})  // []
Array.from({name: 'fl',sex: 'male',length: 2})  // [undefined, undefined]
Array.from({0: 'fl',1: 'male',length: 2})  // ['fl', 'male']

Array.from第二个参数用于做map循环,和map的参数`(item,index,arr)`一样
Array.from({0: 'fl',1: 'male',length: 2},(item,index)=> item = index)  // [0, 1]

(2) 对于对象:
var obj = { a: 1, b: 2 }

1.`第一种方式`var obj1 = { ...obj }  //展开运算符进行浅拷贝

2.`第二种方式`var obj2 = Object.assign({}, obj) // 使用Object.assign做浅拷贝

3.`第三种方式`var obj3 = _.clone(obj)  // 使用lodash的_.clone方法
复制代码

  深拷贝:个人理解就是对数据a做拷贝得到数据b。a和b除了里面的数据一样,其他都不一样,包括存储地址。他们相互独立,互不关联。深拷贝的深是体现在先对数据做浅拷贝,然后对浅拷贝中的引用数据类型做递归浅拷贝,直到浅拷贝中的引用数据类型完全展开为基础数据类型为止,相当于对里面展开的每一项基础数据类型做浅拷贝。

  常见的深拷贝类型:


var obj = { a: { b: { c: 2 } } } 

1.`第一种方式`(对普通对象或者数组):
var obj1 = JSON.parse(JSON.stringify(obj))

2.`第二种方式`var obj2 = _.cloneDeep(obj)
复制代码

  深拷贝的实现:

1. MapSetRegExpDate都是Object类型;
2. 环形数据就是a中有b,b中有a,这样无限循环嵌套下去的结构,是无法通过遍历循环完的,最终导致栈溢出;

例如:
var a = {name: 'fl'}
var b = {age: 18}
a.b = b
b.a = a
这样得到的a和b都是环形数据,无限循环调用自身。
复制代码
实现的传送门:https://yang-zebo.github.io/guide/JavaScript/ECMAScript/04/04_02.html
深入理解MapWeakMaphttp://t.zoukankan.com/Leophen-p-14794561.html

实现思路:
1.先判断数据类型,如果是null或者不是object类型就返回自身;
2.对object类型的数据进行判断,分别使用对应的构造函数创建并赋值,最后返回;
3.针对环形结构,如果这个key值已经存在过,就直接返回对应的value,不再递归调用循环;

function deepClone(obj) {
  // 先判断出基础数据类型和方法
  if (obj === null || typeof obj !== 'object') {
    return obj
    // null undefined number string boolen symbol bigint function
    // 遇到这几个类型直接返回出去
  }
  // 预防环形对象的处理
  if (!deepClone.cached) {
    // 创建一个 WeakMap 对象挂载在 deepClone 的属性 cached 上
    deepClone.cached = new WeakMap() // 虚引用map
  }
  // 如果 obj 已经保存过,无需执行后续
  if (deepClone.cached.has(obj)) {
    return deepClone.cached.get(obj)
  }

  // 判断几大类型
  if (obj instanceof Map) {
    // 新建一个map 替代老Map
    let tmp = new Map()
    // 让 obj 作为 key, tmp 作为 value 保存在 deepClone.cached 的属性上
    deepClone.cached.set(obj, tmp)
    for (let [key, value] of obj) {
      // of 取值 in 是取 key
      tmp.set(key, deepClone(value))
      // value可能是引用数据类型,需要递归处理!!
    }
    return tmp
  } else if (obj instanceof Set) {
    let tmp = new Set()
    deepClone.cached.set(obj, tmp)
    for (let val of obj) {
      tmp.add(deepClone(val))
      // value可能是引用数据类型,需要递归处理!!
    }
    return tmp
  } else if (obj instanceof RegExp) {
    let tmp = new RegExp(obj)
    deepClone.cached.set(obj, tmp)
    return tmp
  } else if (obj instanceof Date) {
    let tmp = new Date(obj)
    deepClone.cached.set(obj, tmp)
    return tmp
  } else {
    // 还剩下数组和普通对象类型的数据
    // 创建一个跟 obj 相同的数据类型
    // 由于实例的 constructor 指向 它的构造器(构造函数),所以可以 new 出相同数据类型
    let tmp = new obj.constructor()
    deepClone.cached.set(obj, tmp)
    for (let key in obj) {
      tmp[key] = deepClone(obj[key])
      // obj[key]可能是引用数据类型, 需要递归处理!!
    }
    return tmp
  }
}
复制代码

8. 对象遍历的方法

  ES5 引入了Object.keys方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名。

1.在对象上使用:
Object.keys({name: 'fl',sex:'male'})  // ['name', 'sex']

2.在数组上使用
Object.keys(['a','b','c'])  // ['0', '1', '2']
复制代码

  ES2017 引入了跟Object.keys配套的Object.valuesObject.entries,作为遍历一个对象的补充手段,供for...of循环使用。

传送门1https://yang-zebo.github.io/guide/JavaScript/ECMAScript/02/02_07.html
传送门2https://blog.csdn.net/weixin_46151381/article/details/121703298

let {keys, values, entries} = Object;
let obj = { a: 1, b: 2, c: 3 };
 
for (let key of keys(obj)) {
  console.log(key); // 'a', 'b', 'c'
}
 
for (let value of values(obj)) {
  console.log(value); // 1, 2, 3
}
 
for (let [key, value] of entries(obj)) {
  console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3]
}

(1) `Object.values`的使用:

1. `Object.values`方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历`(enumerable)`属性的键值。

针对对象:const obj = { name: 'fl', age: 18 }  Object.values(obj)  // ['fl', 18]

针对数组: const Arr = ['a','b','c']  Object.values(Arr) //  ['a', 'b', 'c']

2.`Object.create`方法的第二个参数添加的对象属性,如果不显式声明,默认是不可遍历的。属性描述对象的
  `enumerable`默认是`false`。 `Object.values`不会返回这个属性,只要把`enumerable`改成`true``Object.values`就会返回属性的值。
  
const obj = Object.create({}, { p: { value: 42 } }) 
Object.values(obj) // [] 
const obj = Object.create( {}, { p: { value: 42, enumerable: true } } ) 
Object.values(obj) // [42]

3. `Object.values` 会过滤属性名为 `Symbol` 值的属性。
Object.values({ [Symbol()]: 123, foo: 'abc' }) // ['abc']

4. 如果 `Object.values` 方法的参数是一个字符串,会返回各个字符组成的一个数组。
Object.values('foo') // ['f', 'o', 'o']

5. 如果参数不是对象,`Object.values` 会先将其转为对象。由于数值和布尔值的包装对象,都不会为实例添加非继承的属性。
  所以,`Object.values` 会返回空数组。
Object.values(42) // [] Object.values(true) // []

(2) `Object.entries`的使用:
1. 返回数组
const obj = { foo: 'bar', baz: 42 };
Object.entries(obj)// [ ["foo", "bar"], ["baz", 42] ]
Object.entries({ [Symbol()]: 123, foo: 'abc' }); // [ [ 'foo', 'abc' ] ]

2. 遍历对象的属性
let obj = { one: 1, two: 2 };
for (let [k, v] of Object.entries(obj)) {
  console.log(
    `${JSON.stringify(k)}: ${JSON.stringify(v)}`
  );
}
// "one": 1
// "two": 2

3. 对象转为真正的Map结构
const obj = { foo: 'bar', baz: 42 };
const map = new Map(Object.entries(obj));
map // Map(2) {'foo' => 'bar', 'baz' => 42}

(3) `Object.fromEntries`的使用:

1.`Object.fromEntries()`方法是`Object.entries()`的逆操作,用于将一个键值对数组转为对象。

Object.fromEntries([
  ['foo', 'bar'],
  ['baz', 42]
]) // { foo: "bar", baz: 42 }

2. Map 结构转为对象
// 例一
const entries = new Map([
  ['foo', 'bar'],
  ['baz', 42]
]);
Object.fromEntries(entries)
// { foo: "bar", baz: 42 }
 
// 例二
const map = new Map().set('foo', true).set('bar', false);
Object.fromEntries(map)// { foo: true, bar: false }
复制代码

9. 完美的文档整理集-改编自阮一峰老师

10.js操作中的与、或、非

Boolean(null) // false
Boolean(undefined) // false
Boolean(NaN) // false

传送门:https://juejin.cn/post/6844903944817213448

1. 与操作, 默认会隐式转换为布尔型进行判断

如果有一个操作数是null,返回null
如果有一个操作数是undefined,返回undefined
如果有一个操作数是NaN,返回NaN
如果第一个操作数是nullundefined或者NaN之一,则返回对应操作数

第一个操作数是对象,返回第二个操作数
例子: ({}) && 2  // 2

第二个操作数是对象,只有第一个操作数为true的时候,才会返回该对象
反例子: 0 && {}  // 0

如果两个操作数都是对象,返回第二个操作数

2. 或操作(短路运算符), 默认会隐式转换为布尔型进行判断

第一个操作数是对象,返回第一个对象
第一个操作数是false,返回第二个操作数
如果两个操作数都是对象,则返回第一个对象

3.非操作

对任何类型的数据,都会返回一个布尔值
对任何对象,返回false
任意非空字符串,返回false
空字符串,返回true
任意非0数字,返回false
0,返回true
nullundefinedNaN,返回true
复制代码

11.事件循环

传送门:https://juejin.cn/post/7016298598883131423

总结:

```1.微任务:```
(1) Promise.prototype.then catch finally
(2) process.nextTick
(3) MutationObserver

```2.宏任务:```
(1) I/O
(2) setTimeout
(3) setInterval
(4) setImmediate
(5) requestAnimationFrame

```3.微任务执行时机要优于宏任务,先执行微任务,再执行宏任务。一个宏任务中的所有微任务执行完毕之后,如果该宏任务中
还有嵌套的宏任务,则将这个宏任务放入放入事件队列中的宏任务的最下方。```

关卡(文章自带及```思维发散```案例):

```关卡1```:
console.log(1)
setTimeout(() => {
  console.log(2)
  Promise.resolve().then(() => {
    console.log(3)
  })
});
console.log(4)
new Promise((resolve,reject) => {
  // 注意:Promise的executor是同步的哦!!!
  console.log(5)
  resolve()
}).then(() => {
  console.log(6)
  setTimeout(() => {
    console.log(7)
  })
})
console.log(8)

```输出```1 4 5 8 6 2 3 7

```关卡2(注意与关卡1的区别)```:
console.log(1)
setTimeout(() => {
  console.log(2)
   setTimeout(() => {
    console.log(3)
  })
});
console.log(4)
new Promise((resolve,reject) => {
  ```注意:Promise的executor是同步的哦!!!```
  console.log(5)
  resolve()
}).then(() => {
  console.log(6)
  setTimeout(() => {
    console.log(7)
  })
})
console.log(8)

```输出```1 4 5 8 6 2 7 3

```关卡3```:
setTimeout(() => {
  console.log(1)
}, 0)
console.log(2)
const p = new Promise((resolve) => {
  ```注意:promise中不会返回其他东西```
  console.log(3)
  resolve()
}).then(() => {
  console.log(4)
  ```注意:then中就算不返回,也会默认返回成功回调,即promise.resolve(undefined)或者return undefined```
}).then(() => {
  console.log(5)
})
console.log(6)

```输出```2 3 6 4 5 1

```关卡4```:
new Promise((resolve,reject)=>{
  console.log(1)
  resolve()
}).then(()=>{
  console.log(2)
  new Promise((resolve,reject)=>{
      console.log(3)
      resolve()
      ```注意:resolve的作用只是将它加入到异步队列中,下方的两个then进入异步队列```
      ```代码同步向下执行,return undefined,将第二个then加入异步队列```
      ```取出异步队列中的第一个微任务,打印4,返回undefined,将5加入异步队列下方,所以先打印4```
      ```再执行6,最后打印最后加入异步队列中的5```
  }).then(()=>{
      console.log(4)
  }).then(()=>{
      console.log(5)
  })
}).then(()=>{
  console.log(6)
})

```输出```1 2 3 4 6 5

```转化:```
new Promise((resolve, reject) => {
  console.log(1)
  resolve()
})
.then(() => {
  console.log(2)
  return new Promise((resolve, reject) => {
    console.log(3)
    resolve()
    // 异步加入任务队列
  }).then(() => {
    console.log(4)
    promise.resolve(undefined)
  }).then(() => {
    console.log(5)
  })

  console.log('end')
  //异步加入任务队列
  return undefined
})
.then(() => {
  console.log(6)
})

```关卡5(注意与关卡4的区别)```:
new Promise((resolve,reject)=>{
  console.log(1)
  resolve()
}).then(()=>{
  console.log(2)
  new Promise((resolve,reject)=>{
      console.log(3)
      ```因为此时没有改变promise,处于pending状态,就不会向下调用then方法```
      ```但另外一个then方法是由上一个then方法的状态决定的,所以会执行```
  }).then(()=>{
      console.log(4)
  }).then(()=>{
      console.log(5)
  })
}).then(()=>{
  console.log(6)
})

```输出```1 2 3 6

```关卡6(注意与关卡4的区别)```:
new Promise((resolve, reject) => {
  console.log(1)
  resolve()
}).then(() => {
  console.log(2)
  ```多了个return,此时then方法有返回,then方法返回值之后,再执行下一个then方法```
  return new Promise((resolve, reject) => {
    console.log(3)
    resolve()
  }).then(() => {
    console.log(4)
  }).then(() => { // 相当于return了这个then的执行返回Promise
    console.log(5)
  })
}).then(() => {
  console.log(6)
})

```输出```1 2 3 4 5 6

```关卡7(注意与关卡6的区别)```:
new Promise((resolve, reject) => {
  console.log(1)
  resolve()
}).then(() => {
  console.log(2)
  return new Promise((resolve, reject) => {
    console.log(3)
    ```这个promise和then方法的状态都是pending,就不会向下执行后一个then方法了```
  }).then(() => {
    console.log(4)
  }).then(() => { // 相当于return了这个then的执行返回Promise
    console.log(5)
  })
}).then(() => {
  console.log(6)
})

```输出```1 2 3

```关卡8```:
new Promise((resolve, reject) => {
  console.log(1)
  resolve()
}).then(() => {
  console.log(2)
  new Promise((resolve, reject) => {
    console.log(3)
    resolve()
  }).then(() => {
    console.log(4)
  }).then(() => {
    console.log(5)
  })
}).then(() => {
  console.log(6)
})
new Promise((resolve, reject) => {
  console.log(7)
  resolve()
}).then(() => {
  console.log(8)
})

```输出```1 7 2 3 8 4 6 5

```关卡9: async与await: 相当于微任务:```
async function async1() {
  console.log(1);
  await async2();
  console.log(2);
  ```转换:```
  ```————转换开始:————```
  console.log(1) // 同步
  new Promise((resolve, reject) => {
    console.log(3) // 同步
    resolve()
  }).then(() => { // 异步:微任务 then1
    console.log(2)
  })
 ``` ————转换结束:————```
}
async function async2() {
  console.log(3);
}
console.log(4);
setTimeout(function () {
  console.log(5);
});
async1()
new Promise(function (resolve, reject) {
  console.log(6);
  resolve();
}).then(function () {
  console.log(7);
});
console.log(8);

```输出```4 1 3 6 8 2 7 5
复制代码

12.函数的length属性

传送门:https://segmentfault.com/a/1190000041201277

123['toString'].length + 123 = 124

原理:123['toString'] === Number.prototype.toString  // true
      Number.prototype.toString === Number.toString // false
      Number.toString === Function.prototype.toString // true

Number.toString函数的```length```0
Number.prototype.toString函数的```length```1,参数为基数,即转换的进制数,默认为10
也只有Number原型链上的toString方法的参数的长度为1,其余为0
所以,结果为123 + 1 = 124
复制代码

13.(a == 1 && a == 2 && a == 3) 有可能是 true 吗?

传送门:https://juejin.cn/post/6950664413317693470#comment

答案:```可以```

方法一:```对象类型转换```

当两个类型不同时进行==比较时,会将一个类型转为另一个类型,然后再进行比较。
比如`Object`类型与`Number`类型进行比较时,`Object`类型会转换为`Number`类型。
`Object`转换为`Number`时,会尝试调用`Object.valueOf()``Object.toString()`来获取对应的数字基本类型。

var a = {
    i: 1,
    toString: function () {
        return a.i++;
    }
}
console.log(a == 1 && a == 2 && a == 3) // true

方法二:```数组类型转换```
数组调用`toString()`会隐含调用`Array.join()`方法 而数组`shift`方法的用法:
`shift()` 方法用于把数组的第一个元素从其中删除,并返回第一个元素的值。
如果数组是空的,那么 `shift()` 方法将不进行任何操作,返回 `undefined` 值。
请注意,该方法不创建新数组,而是直接修改原有的数组。 所以我们可以看到,
`a == 1`时会调用`toString()`,`toString()`调用`join()``join()`等于`shift`,则转换为`Number`类型后为1var a = [1, 2, 3];
a.join = a.shift;
console.log(a == 1 && a == 2 && a == 3); // true

方法三:```使用defineProperty```

var val = 0;
Object.defineProperty(window, 'a', { // 这里要window,这样的话下面才能直接使用a变量去 ==
    get: function () {
        return ++val;
    }
});

console.log(a == 1 && a == 2 && a == 3) // true
复制代码

14. ?.的妙用

传送门:https://juejin.cn/post/6995334897065787422#heading-40

```1. 对象?.属性:```
const obj = {
  cat: {
    name: '哈哈'
  }
}

const dog = obj?.dog?.name // undefined

```2. 数组?.索引:```
const item = arr?.[1]

```3. 函数?.执行:```
const res = fn?.()

```4. 数组?.长度:```
const length = list?.length

```5. 数组?.循环```
priceList?.map(({ price }) => price)
复制代码

15.Set和Map的基本使用:

传送门:https://juejin.cn/post/6995334897065787422#heading-21

```set的使用:```

```1.set的基本用法```
// 可不传数组
const set1 = new Set()
set1.add(1)
set1.add(2)
console.log(set1) // Set(2) { 1, 2 }

// 也可传数组
const set2 = new Set([1, 2, 3])
// 增加元素 使用 add
set2.add(4)
set2.add('林三心')
console.log(set2) // Set(5) { 1, 2, 3, 4, '林三心' }
// 是否含有某个元素 使用 has
console.log(set2.has(2)) // true
// 查看长度 使用 size
console.log(set2.size) // 5
// 删除元素 使用 delete
set2.delete(2)
console.log(set2) // Set(4) { 1, 3, 4, '林三心' }

```2.set的不重复性```
// 增加一个已有元素,则增加无效,会被自动去重
const set1 = new Set([1])
set1.add(1)
console.log(set1) // Set(1) { 1 }

// 传入的数组中有重复项,会自动去重
const set2 = new Set([1, 2, '林三心', 3, 3, '林三心'])
console.log(set2) // Set(4) { 1, 2, '林三心', 3 }

```注意: 两个对象都是不同的指针,所以没法去重```
const set1 = new Set([1, {name: '林三心'}, 2, {name: '林三心'}])
console.log(set1) // Set(4) { 1, { name: '林三心' }, 2, { name: '林三心' } }


```注意:如果是两个对象是同一指针,则能去重```
const obj = {name: '林三心'}
const set2 = new Set([1, obj, 2, obj])
console.log(set2) // Set(3) { 1, { name: '林三心' }, 2 }

```注意:咱们都知道 NaN !== NaN,NaN是自身不等于自身的,但是在Set中他还是会被去重```
const set = new Set([1, NaN, 1, NaN])
console.log(set) // Set(2) { 1, NaN }

```map的使用:```

// 定义map
const map1 = new Map()
// 新增键值对 使用 set(key, value)
map1.set(true, 1)
map1.set(1, 2)
map1.set('哈哈', '嘻嘻嘻')
console.log(map1) // Map(3) { true => 1, 1 => 2, '哈哈' => '嘻嘻嘻' }
// 判断map是否含有某个key 使用 has(key)
console.log(map1.has('哈哈')) // true
// 获取map中某个key对应的value 使用 get(key)
console.log(map1.get(true)) // 2
// 删除map中某个键值对 使用 delete(key)
map1.delete('哈哈')
console.log(map1) // Map(2) { true => 1, 1 => 2 }

// 定义map,也可传入键值对数组集合
const map2 = new Map([[true, 1], [1, 2], ['哈哈', '嘻嘻嘻']])
console.log(map2) // Map(3) { true => 1, 1 => 2, '哈哈' => '嘻嘻嘻' }
复制代码

结语

  大概就这样吧~

猜你喜欢

转载自juejin.im/post/7128195844871815204