你不知道的JavaScript(三)

334 阅读30分钟

1. this与箭头函数

1. this的指向

在函数中this到底取何值,是在函数真正被调用执行的时候确定的,函数定义的时候确定不了,也就是说,this的指向完全取决于函数调用的位置, 即this是在执行的时候被绑定的。

  • 函数调用: 当一个函数不是一个对象的属性时,直接作为函数来调用时,this指向全局对象,立即执行函数,默认的定时器等函数,this也是指向window
  • 方法调用: 如果一个函数作为一个对象的方法来调用时,this指向这个对象。
  • 构造函数调用: this指向这个用new新创建的对象。
  • apply 、 call 和 bind 调用模式: 这三个方法都可以显示的指定调用函数的 this 指向。
  • 箭头函数的this: 指向声明时所在作用域下 this 的值,即箭头函数的this去他的上级作用域下寻找,任何方法都改变不了他的指向

隐式绑定

  // 严格模式:
  // 'use strict'

  // 1. 独立函数调用
  function foo() {
    console.log(this === window); //true
  }
  foo();

  // 2. 方法调用
  var obj = {
    name: "zgc",
    running: function () {
      // 这里的上级作用域是window, 对象是数据类型, 不是代码块, 没有作用域
      console.log(this);
    },
  };

  obj.running(); //obj对象

  var fn = obj.running;
  fn(); // window对象

  function bar() {
    console.log(this);
  }
  var baz = {
    name: "wf",
    bar: bar,
  };
  baz.bar(); // baz对象

  function test(fn) {
    fn();
  }
  test(baz.bar); // window对象

  // 3. 构造函数调用(new) 绑定
  function Student(name) {
    this.name = name;
    console.log("构造函数", this); // {name: 'zgc'}
  }
  const stu = new Student("zgc");

  // 4. 严格模式下, 独立函数调用this指向的是undefined

显式绑定(apply & call & bind)

  • apply 、 call 、bind 三者都是用来改变函数的this对象的指向的;
  • apply 、 call 、bind 三者第一个参数都是this要指向的对象,也就是想指定的上下文;
  • apply是把参数放在一个数组里面作为它的第二个参数,而call、bind从第二个参数开始以参数列表的形式展现。
  • bind则是返回改变了this指向的一个函数,便于稍后调用;apply 、call 则是立即调用
  • bind方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数, 这个函数的 this指向除了使用new 时会被改变,其他情况下都不会改变
  var obj = {
    name: "zgc",
  };

  function foo(name, age) {
    console.log("foo", this, name, age);
  }

  // 让 foo的this指向obj
  // 1. call
  // 第一个参数, 绑定this
  // 第二个参数开始, 以参数列表的形式展现
  foo.call(obj, "zgc", 18); // foo {name: 'zgc'} zgc 18

  // 2. apply
  // 第一个参数, 绑定this
  // 第二个参数, 以数组的形式传入额外的实参
  foo.apply(obj, ["wf", 20]); // foo {name: 'zgc'} wf 20

  // 3. bind
  // 如果我们希望一个函数总是显示的绑定在一个对象上, 而不是每一次调用时再去绑定, 那么可以使用bind
  //  bind方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数, 便于稍后调用
  // 第一个参数, 绑定this
  // 第二个参数开始, 以参数列表的形式展现
  const baz = foo.bind(obj, "wlc", 22);
  baz(); // foo {name: 'zgc'} wlc 22]

  const bar = foo.bind(obj);
  bar("cx", 24); // foo {name: 'zgc'} cx 24

内置函数的this指向

 //  JavaScript的内置函数
  // 1. 定时器
  setTimeout(function () {
    console.log("定时器", this); // window
  }, 1000);

  // 2. 点击事件
  var btn = document.querySelector("button");
  // btn.onclick = function () {
  //   console.log("btn", this); // btn(<button>按钮</button>)
  // };
  btn.addEventListener("click", function () {
    console.log("btn", this); // btn(<button>按钮</button>)
  });

  // 3. forEach等方法
  //默认 window, 第二个参数可以绑定this
  var names = ["zgc", "wf"];
  var obj = { name: "zgc" };
  names.forEach(function () {
    console.log("forEach", this); // { name: "zgc" }
  }, obj);
  
  // 4. 补充: 立即执行函数this指向window
  (function () {
    console.log("立即执行函数", this);
  })();

2. this绑定的优先级

 function foo(age) {
    this.age = age;
    // console.log(this.__proto__ === foo.prototype);
    console.log(this);
  }

  var obj = {
    name: "zgc",
    foo: foo,
  };

  // 1. 隐式绑定(方法)的优先级高于默认绑定(独立函数调用)
  obj.foo(18); // this === obj, obj:{name: 'zgc', age: 18, foo: ƒ}

  // 2. 显式绑定的优先级高于隐式绑定
  var bar = {
    name: "cx",
  };

  obj.foo.call(bar, 22); // this === bar, bar:{name: 'cx', age: 22}

  // 3. new不可以和call/apply一起使用, 但new绑定的优先级高于bind绑定
  var info = {
    name: "wlc",
  };

  var baz = obj.foo.bind(info, 24);
  baz(); // this === info, info:{name: 'wlc', age: 24}

  var user = {
    name: "wf",
  };

  var test = obj.foo.bind(user);
  var result = new test(18); // this.__proto__ === foo.prototype
  // new 以foo为构造函数创建一个新的空对象, 并将this指向这个空对象, 指向代码为其添加age属性
  console.log("result", result); // result {age: 18}

  // 4. bind的优先级高于 apply/call
  baz.call(obj); // this === info, info:{name: 'wlc', age: 24}

  // 5: 在显式绑定中, 如果我们的第一个参数为null和undefined, 那么这个显式绑定会被忽略, 使用默认规则
  obj.foo.call(null); // window

3. 箭头函数

ES6允许使用箭头(=>)定义函数,箭头函数多用于匿名函数的定义

  1. 如果形参只有一个,则小括号可以省略;
  2. 函数体如果只有一条语句,则花括号可以省略,函数的返回值为该条语句的执行结果,return省略
  3. 箭头函数其实本身并没有绑定this,,即箭头函数的this去他的上级作用域下寻找,任何方法都改变不了他的指向
  4. 箭头函数没有显式原型(foo.prototype),不能作为构造函数实例化(无法将自己的显式原型赋值给实例对象的隐式原型);
  5. 不能使用 arguments,使用reset参数
  6. 当省略花括号与return, 并且返回值是一个对象时, 对象必须包一个小括号
  // 1. 箭头函数的定义
  // var foo = (参数1, 参数2) => { 代码块 };

  // 2. 当省略花括号与return, 并且返回值是一个对象时, 对象必须包一个小括号
  var num = () => 1;
  var obj = () => ({ name: "zgc" });
  console.log(num(), obj()); // 1 {name: 'zgc'}

  // 3. this的使用
  // 箭头函数其实本身并没有绑定this,,即箭头函数的this去他的上级作用域下寻找
  // 所以任何方法都无法改变该this的指向
  var foo = () => {
    console.log(this);
  };
  foo(); // window

  var obj = {
    name: "zgc",
    running: () => {
      // 这里的上级作用域是window, 对象是数据类型, 不是代码块, 没有作用域
      console.log("箭头函数", this); // window
    },
  };
  obj.running();

  var obj = {
    name: "wf",
    running: function () {
      // 这里的上级作用域是window, 对象是数据类型, 不是代码块, 没有作用域
      console.log("普通函数", this); // obj
      var bar = () => {
        console.log("bar", this); // obj
      };
      return bar;
    },
  };
  const fn1 = obj.running();
  fn1();

  var name = "xxxx";
  var obj = {
    name: "wf",
    running: function () {
      // 这里的上级作用域是window, 对象是数据类型, 不是代码块, 没有作用域
      console.log("普通函数", this); // window
      var bar = () => {
        console.log("bar", this, name); // window xxxx
      };
      return bar;
    },
  };
  const fn2 = obj.running;
  fn2();
  fn2()();

4. this习题

juejin.cn/post/700297…

2. 函数与对象的补充

1. 函数对象的属性

  // 定义对象并给对象添加属性和方法
  var obj = {};
  obj.name = "zgc";
  obj.bar = function () {};
  // console.log(obj);

  // 注意: 在 JS 中函数也属于对象的一种,所以可以给函数添加属性和方法
  function foo(a, b, c) {
    console.log(a, b, c);
  }
  var bar = function (m, n, ...args) {};
  function test(x, y = 0) {}

  // 1. 自定义属性
  foo.message = "Hello";
  console.log("访问foo的属性", foo.message); // Hello

  // 2. 默认函数对象中已经有两个自己的属性了
  // (1) name属性 (了解)
  console.log(foo.name, bar.name); // foo bar
  // 将多个函数放入数组中,可以以name区分
  var fns = [foo, bar];
  for (var fn of fns) {
    console.log(fn.name);
  }
  // (2) length属性, 参数个数的长度
  // 这个长度是指本来因该获取的参数的个数, 即形参的个数(因为函数在调用时参数可能多传或少传, 所以实参的个数未必等于形参的个数)
  // 剩余参数是不会算在length里面的, 赋默认值的参数也不会算在length里面
  console.log(foo.length, bar.length, test.length); // 3,2,1
  foo(1, 2); // 1 2 undefined
  foo(1, 2, 3, 4); // 1 2 3

2. 函数中arguments的使用

  // 1. arguments是一个类数组对象, 并不是真正的数组
  // 2. arguments包含所有的参数, 拥有length属性, 可以通过index获取参数
  // 3. 类数组不能够调用数组的内置方法, 如map, filter等

  function foo(m, n) {
    // console.log(m, n);
    // console.log(arguments);

    for (var i of arguments) {
      console.log(i); // 10 20 30 40
    }

    for (var i in arguments) {
      console.log(i); // 0 1 2 3
    }
  }
  foo(10, 20, 30, 40);

  // 4. 类数组转数组
  // (1) 遍历 arguments每个属性放入一个新数组
  function bar(a, b, c) {
    const newArr1 = [];
    for (var i of arguments) {
      newArr1.push(i);
    }
    console.log(newArr1); // [2, 4, 6]
  }
  bar(2, 4, 6);

  // (2) ...扩展运算符
  function bar(a, b, c) {
    const newArr1 = [...arguments];
    console.log(newArr1); // [2, 4, 8]
  }
  bar(2, 4, 8);

  // (3) Array.from(arguments)
  function bar(a, b, c) {
    const newArr1 = Array.from(arguments);
    console.log(newArr1); // [1, 3, 5]
  }
  bar(1, 3, 5);

  // (4) slice
  function bar(a, b, c) {
    // 注意, slice是实例方法, 不能通过Array之间调用
    // 能通过Array之间调用的都是类方法
    // console.log(typeof Array); // function
    const newArr1 = [].slice.apply(arguments);
    const newArr2 = Array.prototype.slice.apply(arguments);
    console.log(newArr1, newArr2); // [1, 3, 9] [1, 3, 9]
  }
  bar(1, 3, 9);

  // 4. 箭头函数没有 arguments
  const baz = () => {
    // console.log("箭头函数", arguments); 
    // arguments is not defined
  };
  baz();

  function test(a, b) {
    const baz = () => {
      console.log("箭头函数", arguments);
      // 这里的arguments是test的arguments
      // 当箭头函数本身找不到arguments时, 会去它的上层作用域下寻找
    };
    baz();
  }
  test(1, 2);

3. 函数的剩余参数

  // 剩余参数(亦称 rest 参数) 用于获取函数的多余参数,这样就不要使用 arguments 对象了
  // 如果函数的最后一个形参是...为前缀的,且搭配的变量是一个数组,该变量将多余的参数放入数组中
  // 注意1: 和参数对象不同, 剩余参数只包含那些没有对应形参的实参, 而且是真实的数组,可直接使用所有数组方法
  // 注意2: 函数剩余参数之后不能再有其他参数(即 只能是最后一个参数),否则会报错
  // 注意3: 函数的 length 属性,不包括函数剩余参数

  function add(x, y, ...values) {
    let sum = 0;
    console.log(x, y); // 2,4
    console.log(values); // [6, 8]

    for (var val of values) {
      sum += val;
    }

    console.log(sum); // 14
  }

  add(2, 4, 6, 8);

  console.log(add.length); // 2

4. 纯函数

  • 函数式编程: 通常我们对函数作为头等公民的编程方式, 称之为函数式编程
    • 函数可以赋值给变量(函数表达式写法)
    • 函数可以在变量之间来回传递
    • 函数可以作为另一个函数的参数
    • 函数作为另一个函数的返回值
    • 函数存储在另一个数据结构中
  • 函数式编程中有一个非常重要的概念叫做纯函数, JS符合函数式编程的范式 所以也有纯函数的概念
  • 纯函数: 相同的输入,总是会的到相同的输出,并且在执行过程中没有任何副作用。
  • 副作用: 指的是执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响
    • 网络请求
    • 输出数据 console.log()打印数据
    • 修改了 全局变量、参数、外部存储
    • DOM查询、操作
    • Math.random
    • 获取当前时间
  // 纯函数,符合函数在相同的输入值时,需产生相同的输出
  function sum(a, b) {
    return a + b;
  }

  // 不是一个纯函数,因为在我们程序执行的过程中,变量num很可能会发生改变
  let num = 1;
  function add(x) {
    return x + num;
  }
  add(1);

  // 不是一个纯函数
  var address = "北京";
  function printInfo(info) {
    console.log(info.name); // 有输出数据
    info.age = 18; // 对参数进行修改
    address = info.address; // 修改全局变量
  }
  var obj = {
    name: "zgc",
    address: "上海",
  };
  printInfo(obj);

  // 在JavaScript中内置的API也存在有纯函数,我们拿Array对象中的方法来说

  // filter 过滤数组中的元素,它不对原数组进行操作是一个纯函数
  var names = ["张三", "李四", "王五", "赵六"];
  var newNames1 = names.filter((n) => n !== "张三");
  console.log(newNames1); // [ '李四', '王五', '赵六' ]
  console.log(names); // [ '张三', '李四', '王五', '赵六' ]

  // splice 截取数组的时候会对原数组进行操作,所以不是一个纯函数。
  var newNames2 = names.splice(2);
  console.log(newNames2); // [ '王五', '赵六' ]
  console.log(names); // [ '张三', '李四' ]

5. 函数的柯里化(Currying)

  • 柯里化是一种关于函数的高阶技术。它不仅被用于 JavaScript,还被用于其他编程语言。
  • 柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。
  • 柯里化是一种函数的转换,它是指将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)(c)
  • 柯里化不会调用函数, 它只是对函数进行转换。
  // 普通函数
  function foo(x, y, z) {
    console.log(x + y + z);
  }

  foo(10, 20, 30); // 60

  // 柯里化函数
  function bar(x) {
    return function (y) {
      return function (z) {
        console.log(x + y + z);
      };
    };
  }
  bar(1)(2)(3); // 6

  // 柯里化箭头函数写法
  // const baz = (x) => {
  //   return (y) => {
  //     return (z) => {
  //       console.log(x + y + z);
  //     };
  //   };
  // };
  var baz = (x) => (y) => (z) => console.log(x + y + z);

  baz(1)(3)(5); // 9

  // 封装一个函数: 自动生成柯里化函数
  function currying(fn) {
    function curryFn(...args) {
      if (args.length >= fn.length) {
        // return fn(...args);
        return fn.apply(this, args);
      } else {
        return function (...newArgs) {
          // return curryFn(...args.concat(newArgs));
          return curryFn.apply(this, args.concat(newArgs));
        };
      }
    }
    return curryFn;
  }

  const curryFn1 = currying(foo);
  curryFn1(10, 20)(30); // 60
  curryFn1(10)(20)(30); // 60
  curryFn1(10).call("this", 20)(30); // 60

6. 组合函数

组合函数是在JS开发过程中一种对函数的使用技巧, 模式

  • 函数组合是指将多个函数按顺序执行,前一个函数的返回值作为下一个函数的参数,最终返回结果。
  • 比如我们现在需要对某一个数据进行函数的调用,执行两个函数fn1和fn2,这两个函数是依次进行的;
    • 那么如果每次我们都需要进行两个函数的调用,操作上就会显得很重复;
    • 那么是否可以将这两个函数组合起来,自动依次调用呢?
    • 这个过程就是对函数的组合,我们称之为 组合函数(Compose Function)
  // 要求: 需要将N个数字分别进行调用double方法乘以2,再调用square方法平方。
  var count = 10;

  // 普通情况下:

  function double(num) {
    return num * 2;
  }
  function square(num) {
    return num * num;
  }

  function add(num) {
    return num + 10;
  }

  var result = square(double(count));
  console.log("普通函数", result);

  // 组合函数情况下:
  function getNum(num) {
    return square(double(count));
  }
  var result = getNum(count);
  console.log("组合函数", result);

  // 封装通用性组合函数:
  function getComposeFn(...fns) {
    // 边界判断:
    // if (fns.length <= 0) return;
    // for (var fn of fns) {
    //   if (typeof fn !== "function") throw new Error("传入的参数必须为函数");
    // }
    return function (...args) {
      console.log(args);
      var result = fns[0].apply(this, args);
      for (var i = 1; i < fns.length; i++) {
        result = fns[i].apply(this, [result]);
      }
      return result;
    };
  }
  var composeFn = getComposeFn(double, square, add);
  const res = composeFn(10);
  console.log("res", res); // 410

7. with & eval(几乎不用)

  // 1. with: 扩展一个语句的作用域链
  // 不建议使用with语句,因为它可能是混淆错误和兼容性问题的根源
  // with (expression) {
  //   statement;
  // }
  // expression: 将给定的表达式添加到在评估语句时使用的作用域链上。表达式周围的括号是必需的。
  // statement: 任何语句。要执行多个语句,请使用一个块语句 ({ ... }) 对这些语句进行分组。
  var obj = {
    userName: "zgc",
    userAge: 18,
  };
  // console.log(userName); // userName is not defined
  // console.log(userAge); //  userAge is not defined
  with (obj) {
    console.log(userName); // zgc
    console.log(userAge); // 18
  }

  // 2. eval: 允许执行一个代码字符串
  // eval是一个特殊的函数, 它可以将传入的字符串当作JS语句执行
  // 调用 eval(code) 会运行代码字符串,并返回最后一条语句的结果
  var x = 10;
  var y = 20;
  var a = eval("x * y;")
  var b = eval("2 + 2;")
  var c = eval("x + 17; console.log(1111);") // 1111

  var res = a + b;
  console.log(res, a, b, c); // 204 200 4 undefined

8. 严格模式

JavaScript 严格模式(strict mode)即在严格的条件下运行。

使用 "use strict" 指令:

  • "use strict" 指令在 JavaScript 1.8.5 (ECMAScript5) 中新增。
  • 它不是一条语句,但是是一个字面量表达式,在 JavaScript 旧版本中会被忽略。
  • "use strict" 的目的是指定代码在严格条件下执行。
  • 严格模式下你不能使用未声明的变量。

为什么使用严格模式:

  • 明确禁止一些不合理、不严谨的语法,减少 JavaScript 语言的一些怪异行为。
  • 增加更多报错的场合,消除代码运行的一些不安全之处,保证代码运行的安全。
  • 提高编译器效率,增加运行速度。
  • 为未来新版本的 JavaScript 语法做好铺垫。

"严格模式"体现了Javascript更合理、更安全、更严谨的发展方向,包括IE 10在内的主流浏览器,都已经支持它,许多大项目已经开始全面拥抱它。 另一方面,同样的代码,在"严格模式"中,可能会有不一样的运行结果;一些在"正常模式"下可以运行的语句,在"严格模式"下将不能运行。掌握这些内容,有助于更细致深入地理解Javascript,让你变成一个更好的程序员。

wangdoc.com/javascript/…

  // 1. 在js文件下给整个文件开启"严格模式":
  "use strict";

  // 2. 给某个函数开启"严格模式":
  function foo() {
    "use strict";
  }

  // 注意: 严格模式要在文件或者函数的开头使用"use strict";来开启

  // 3. 如 class/module 默认是在严格模式运行的

  // 4. 常见的严格模式限制
  // (1) 无法意外创建全局变量
  // 在正常模式中,如果一个变量没有声明就赋值,默认是全局变量。严格模式禁止这种用法,全局变量必须显式声明。

  // v = 1; // 报错,v未声明

  // for (i = 0; i < 2; i++) {
  //   // 报错,i未声明
  // }

  // (2) 禁止this关键字指向全局对象
  // 正常模式下,函数内部的this可能会指向全局对象,严格模式禁止这种用法,避免无意间创造全局变量。
  // 正常模式
  function f() {
    console.log(this === window);
  }
  f(); // true

  // 严格模式
  function f() {
    "use strict";
    console.log(this === undefined);
  }
  f(); // true

  // (3) 对象不能有重名的属性
  // 正常模式下,如果对象有多个重名属性,最后赋值的那个属性会覆盖前面的值。严格模式下,这属于语法错误。
  var o = {
    p: 1,
    p: 2,
  }; // 语法错误

  // (4) 函数不能有重名的参数
  // 正常模式下,如果函数有多个重名的参数,可以用arguments[i]读取。严格模式下,这属于语法错误。
  function f(a, a, b) {
    // 语法错误
  }

9. Object.defineProperty()

juejin.cn/post/699507…

3. 继承与原型

1. 对象和函数的原型

  // 1. 什么是原型
  /*
  我们创建的每个构造函数都有一个 prototype(原型) 属性,该属性是一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
  当我们通过构造函数创建对象时,在这个对象中有一个指针,这个指针指向构造函数的prototype。我们将这个指向构造函数的prototype的指针称为原型。
  */

  // 2. 对象的原型
  var obj = {
    name: "zgc",
    age: 18,
  };
  console.log(obj);

  // 获取对象的原型
  console.log(obj.__proto__); // 隐式原型
  console.log(Object.getPrototypeOf(obj));
  console.log(Object.getPrototypeOf(obj) === obj.__proto__); // true
  // 对象原型的作用
  // 当在自身查找不到属性和方法时, 可以去原型查找

  // 3. 函数的原型
  function Foo() {}

  // 获取函数的原型
  console.log(Foo.prototype); // 显式原型, 对象没有prototype属性
  var f1 = new Foo();
  var f2 = new Foo();
  console.log(f1.__proto__ === Foo.prototype); // true
  console.log(Foo.prototype === f2.__proto__); // true
  console.log(f1.__proto__ === f2.__proto__); // true

  // 创建三个对象
  function Foo(name, age) {
    this.name = name;
    this.age = age;

    // 方式1: 这样每次都会创建一个新对象, 每个对象都会定义一遍函数, 造成浪费
    // this.bar = function () {
    //   console.log(this.name, "bar");
    // };
    // this.baz = function () {
    //   console.log(this.name, "baz");
    // };
  }
  // 方式2: 这将每一个方法定义在原型上面, 这样每个对象调用的都是同一个函数
  /*
   即当我们多个对象拥有共同属性时, 我们可以将它放到构造函数的显式原型对象上面,
   由构造函数创建出来的所有对象都会共享这些属性
  */
  Foo.prototype.bar = function () {
    console.log(this.name, "bar");
  };
  Foo.prototype.baz = function () {
    console.log(this.name, "baz");
  };
  var foo1 = new Foo("zgc", 18);
  var foo2 = new Foo("wf", 24);
  var foo3 = new Foo("cx", 20);

  // console.log(foo1.bar === foo2.bar); // flse
  console.log(foo1.bar === foo2.bar); // true
  foo3.baz(); // cx baz

  // 函数原型的作用
  // 在用new操作符创建对象时, 将自身的prototype属性赋给对象的隐式原型

2. 函数原型上的constructor属性

  // 显式原型上面的constructor属性指向了函数对象
  function Foo() {}

  console.log(Foo.prototype.constructor === Foo); // true
  console.log(Foo.name); // Foo
  console.log(Foo.prototype.constructor.name); // Foo
  console.log(Foo.name === Foo.prototype.constructor.name); // true

  var f = new Foo();
  console.log(f.__proto__.constructor === Foo.prototype.constructor); // true
  console.log(f.__proto__ === Foo.prototype); // true

3. 重写函数原型对象

 function Person() {}
  // console.log(Person.prototype);

  // 像原型对象上直接添加属性和方法
  Person.prototype.msg = "Hello World";
  Person.prototype.info = { name: "zgc", age: 18 };
  Person.prototype.run = function () {};
  // console.log(Person.prototype);

  // 如果我们需要在原型上添加过多的属性, 通常我们可以重写整个原型对象
  Person.prototype = {
    msg: "Hello Person",
    info: { name: "wf", age: 24 },
    run: function () {},
    // constructor: Person,
  };
  // constructor默认是无法枚举的,所以直接添加constructor属性不太适合
  Object.defineProperty(Person.prototype, "constructor", {
    value: Person,
    writable: true,
    configurable: true,
    enumerable: false,
  });
  var p1 = new Person();
  console.log(p1.msg); // Hello Person

4. 默认原型链和自定义原型链

 /*
   当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性;
   这个原型对象又会有自己的原型,这样层层上溯,就形成了一个类似链表的结构,这就是原型链;
   原型链的尽头一般来说都是Object.prototype所以这就是我们新建的对象为什么能够使用toString()等方法的原因。;
  */
  var info = {};
  //  等价于
  var info = new Object();

  console.log(info.__proto__ === Object.prototype); // true

  var obj = {
    name: "zgc",
    age: 18,
  };

  // 查找顺序
  // 1. 在obj上查找'
  // 2. 在obj的原型上查找(obj.__proto__)
  // 3. 在obj的原型的原型上查找上查找(obj.__proto__.__proto__)
  // 3. 直到某个原型指向null则意味着到原型链尽头,停止查找 返回undefined

  console.log(obj.msg); // undefined

  // 自定义原型链
  obj.__proto__ = {
    // msg: "a",
  };
  obj.__proto__.__proto__ = {
    msg: "b",
  };
  obj.__proto__.__proto__.__proto__ = {
    msg: "c",
  };

  console.log(obj.msg); // b
  console.log(
    obj.__proto__.__proto__.__proto__.__proto__ === Object.prototype
  ); // true
  console.log(Object.prototype.__proto__); // null

5. 继承

juejin.cn/post/697794…

6. 对象额外判断方法补充

 // 1. hasOwnProperty: 对象是否拥有某一个属于自己的属性(不是在原型上的属性)
  var obj = {
    name: "zgc",
    age: 18,
  };

  var info = Object.create(obj); // 创建一个新的对象info, info的隐式原型指向obj
  console.log(info.__proto__); // {name: 'zgc', age: 18}
  info.address = "中国";

  console.log(info.hasOwnProperty("name")); // false
  console.log(info.hasOwnProperty("address")); // true

  // 2. in/for in操作符: 判断某个属性是否在某个对象或者对象的原型上面
  console.log("name" in info); // true
  console.log("name" in obj); // true
  console.log("address" in info); // true
  console.log("address" in obj); // false

  // for in遍历对象不仅仅遍历自己对象的内容, 还有原型之上的内容
  for (var key in info) {
    console.log(key); // address name age
  }

  // 3. instanceof: 用于检测构造函数的prototype, 是否出现在某个实例对象的原型链上面
  // instanceof用于判断对象(实例)和类(构造函数)之间的关系
  function Student() {}
  function Person() {}
  function Foo() {}

  function createObject(o) {
    const F = function () {};
    F.prototype = o;
    return new F();
  }

  function inheritPrototype(subType, superType) {
    subType.prototype = createObject(superType.prototype); //核心代码
    Object.defineProperty(subType.prototype, "constructor", {
      value: subType,
      enumerable: false,
      writable: true,
      configurable: true,
    });
  }
  inheritPrototype(Student, Person);
  var stu = new Student();

  console.log(stu instanceof Student); // true
  console.log(stu instanceof Person); // true
  console.log(stu instanceof Foo); // false
  console.log(stu instanceof Object); // true
  console.log(stu instanceof Array); // false

  // 4. isPrototypeOf: 用于检测某个对象是否出现在某个实例对象的原型链上面
  // isPrototypeOf是用来判断对象和对象之间的关系的
  console.log(obj.isPrototypeOf(info)); // true, info的隐式原型指向obj
  console.log(Student.prototype.isPrototypeOf(stu)); // true
  console.log(Person.prototype.isPrototypeOf(stu)); // true

7. 原型继承关系图解

juejin.cn/post/707144…

b58117e53b0147e79115fad4fa0a99d8_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.webp

  • 对象.__proto__ === 其构造函数.prototype
  • Object.prototype 是所有对象的(直接或间接)原型
  • 任何函数.__proto__ === Function.prototype 任意函数有 Object / Array / Function...
  var obj = {};
  var arr = [];
  function foo() {}

  // 1. x的原型 === x._proto_
  console.log(obj.__proto__ === Object.prototype); // true
  console.log(arr.__proto__ === Array.prototype); // true
  console.log(foo.__proto__ === Function.prototype); // true

  // Object.prototype 的原型
  //Object.prototype是根对象,根对象的原型为null
  console.log(Object.prototype.__proto__ === null); // true

  // Function.prototype 的原型
  // 因为Function.prototype是一个对象,而Object.prototype 是所有对象的原型
  console.log(Function.prototype.__proto__ === Object.prototype); // true
  console.log(Array.prototype.__proto__ === Object.prototype); // true

  // 箭头函数f的原型
  // 所有函数的原型都是Function.prototype
  var f = () => {};
  console.log(f.__proto__ === Function.prototype); // true
  console.log(Object.__proto__ === Function.prototype); // true
  console.log(Function.__proto__ === Function.prototype); // true
  console.log(Array.prototype.toString.__proto__ === Function.prototype); // true

8. 函数对象原型和Function的关系

  function foo() {}

  console.log(foo.__proto__ === Function.prototype); // true

  // 函数对象调用的某些方法都是在函数原型上面的
  console.log(foo.apply === Function.prototype.apply); // true

  // 在Function.prototype中添加的属性和方法可以被所有函数获取
  Function.prototype.info = function () {
    console.log("info");
  };

  Function.prototype.msg = "Hello World";

  function bar() {}

  bar.info(); // info
  console.log(bar.msg); // Hello World

  // Array
  var arr = [1, 2, 3];
  var arr1 = Array.prototype.slice.call(arr, 0, 2);
  console.log(arr1); // [1, 2]

  var arr2 = [].slice.apply(arr, [0, 2]);
  console.log(arr2); // [1, 2]

4. ES6

1. class

class类的定义与使用

  • 每个类都可以有一个自己的构造函数, 这个函数的名称是固定的constructor
  • 当我们通过new操作符操作一个类时, 会调用这个类的构造函数constructor
  • 每个类只能有一个构造函数, 如果包含多个构造函数, 那么会抛出异常
  • 当我们通过new关键字操作类时, 调用这个类的构造函数constructor, 并执行如下操作:
    • 在内存中创建一个新对象(空对象)
    • 将这个类的显式原型(Person.prototype)赋值给对象的隐式原型(p.__proto__)
    • 构造函数内部的this会指向创建出来的对象
    • 执行构造函数内部的代码为这个新对象添加属性
    • 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。
  // 1. ES5定义类
  function Baz(name) {
    this.name = name;
  }
  Baz.prototype.run = function () {
    console.log("baz.run");
  }; // 实例方法
  Baz.study = function () {
    console.log("Baz.study");
  }; // 类方法

  var baz = new Baz("zgc");
  // 实例方法调用
  baz.run(); // baz.run
  // 类方法调用
  Baz.study(); // Baz.study
  console.log(baz);

  // 2. ES6定义类
  // 方式一:
  var Foo = class {}; // 比较少见
  // 方式二:
  class Bar {}

  var f1 = new Foo();
  var b1 = new Bar();
  console.log(f1, b1);

  // 3. 类中的构造方法, 实例方法, 静态方法(类方法)
  // 4. 静态方法通常用于定义直接使用类来执行的方法, 不需要有类的实例, 使用static关键字
  // 5. class类与构造函数的相同点
  class Person {
    // 1. 类中的构造函数
    // 当我们通过new关键字调用一个类时, 默认调用class中的constructor方法
    // Person内部的this指向new出来的实例
    constructor(name, age) {
      this.name = name;
      this.age = age;
    }

    // 2. 类中的实例方法
    // 这样定义的方法本质上是放在Person.prototype上面的
    // 无法通过类名之间调用
    run() {
      console.log(this.name, "person.run");
    }
    eat() {
      console.log(this.name, "person.eat");
    }

    // 3. 类中的静态方法
    // 使用static关键字
    // 在静态方法中是不能获取实例属性的状态(属性)的
    static study() {
      console.log(this, "Person.study");
    }
  }

  var p1 = new Person("zgc", 18);
  console.log(p1);
  p1.run(); // zgc person.run
  p1.eat(); // zgc person.eat
  Person.study(); // 这里的this指向Person类 Person.study

  // 而实例对象的原型指向Person.prototype, 实例方法定义在这里面
  console.log(p1.__proto__ === Person.prototype); // true
  // Person类的原型指向Function.prototype, 里面没有实例方法
  console.log(Person.__proto__ === Function.prototype); // true
  console.log(Person.run); // undefined

  console.log(baz.__proto__ === Baz.prototype); // true
  console.log(Baz.prototype.constructor); // 指向Baz()构造函数

  console.log(Person.prototype.constructor); // 指向Person类
  console.log(typeof Baz, typeof Person); // function function

  // 6. class类与构造函数的不同点
  // class类不能够作为普通函数调用
  // Baz()
  // Person() 报错

类的访问器方法

  class Person {
    constructor(name) {
      // 程序员之间的约定: 以_开头的属性和方法, 不要在外界访问
      this._name = name;
    }

    set name(value) {
      // 设置name
      this._name = value;
    }

    get name() {
      // 获取name
      return this._name;
    }
  }

  var p1 = new Person("zgc");
  p1.name = "wf";
  console.log(p1.name); // wf

  var p2 = new Person("cx");
  console.log(p2.name); // cx

  // 应用场景
  class Rectangle {
    constructor(x, y, width, height) {
      this.x = x;
      this.y = y;
      this.width = width;
      this.height = height;
    }

    get position() {
      return { x: this.x, y: this.y };
    }

    get size() {
      return { width: this.width, height: this.height };
    }
  }

  var r = new Rectangle(10, 10, 20, 50);
  console.log(r.position, r.size); // {x: 10, y: 10} {width: 20, height: 50}

类的继承

class Person {
    constructor(name, age) {
      this.name = name;
      this.age = age;
    }
    run() {
      console.log("person.run", this);
    }
  }

  class Student extends Person {
    constructor(name, age, score) {
      // 在子类的构造函数(constructor)中使用this或者返回默认对象之前, 必须先通过super调用父类构造函数
      super(name, age);
      this.score = score;
    }

    study() {
      console.log("student.study", this);
    }
  }

  var stu1 = new Student("zgc", 18, 98);
  console.log(stu1); // {name: 'zgc', age: 18, score: 98}
  stu1.study(); // student.study stu1
  stu1.run(); // person.run stu1

  class Teacher extends Person {
    constructor(name, age, subject) {
      super(name, age);
      this.subject = subject;
    }

    // 当子类与父类的方法同名时, 实例调用的是子类的方法
    run() {
      console.log("teacher.run", this);
    }

    sub() {
      console.log("teacher.sub", this);
    }
  }

  var tea1 = new Teacher("wf", 28, "数学");
  console.log(tea1); // {name: 'wf', age: 28, subject: '数学'}
  tea1.sub(); // teacher.sub tea1
  tea1.run(); // teacher.run tea1

super关键字

super关键字:

  • 在子类执行super.method(...)来调用一个父类的方法
  • 在子类构造方法执行super(...)来调用父类的constructor方法
  • 在子类的构造函数(constructor)中使用this或者返回默认对象之前, 必须先通过super调用父类构造函数
  • super的位置有三个: 子类构造方法, 实例方法, 静态方法
  class Person {
    constructor(name, age) {
      this.name = name;
      this.age = age;
    }
    run() {
      console.log("person.run", this);
    }

    eat() {
      console.log("person.eat", this);
    }

    introduce() {
      console.log("I am zgc");
    }

    static sleep() {
      console.log("Person static sleep", this === Student);
    }
  }

  class Student extends Person {
    constructor(name, age, score) {
      // 2. 在子类构造中执行super(...)来调用父类的constructor方法
      super(name, age);
      this.score = score;
    }

    // 子类对父类方法不满意, 重写父类方法
    eat() {
      console.log("stu.eat", this);
    }

    // 子类对父类方法部分满意, 拓展父类方法
    introduce() {
      // 1. 在子类执行super.method(...)来调用一个父类的方法
      super.introduce();
      console.log("I love you");
    }

    // 子类拓展父类静态方法
    static sleep() {
      super.sleep();
      console.log("Student static sleep", this === Student);
    }

    study() {
      console.log("stu.study", this);
    }
  }

  var stu1 = new Student("zgc", 18, 98);
  console.log(stu1);
  stu1.run(); // person.run stu1
  stu1.eat(); // stu.eat stu1

  stu1.introduce();
  // I am zgc
  //I love you

  // 子类同样会继承父类的静态方法
  Student.sleep();
  // Person static sleep true
  // Student static sleep true

继承内置类并进行扩展

  // 1. 创建一个新的类继承Array进行扩展
  class MyArray extends Array {
    // 当内部什么都不写时, MyArray 与 Array 功能一致

    // 拓展:
    // 获取数组最后一个元素

    lastItem() {
      return this[this.length - 1];
    }

    get firstItem() {
      return this[0];
    }
  }

  var arr = new MyArray(10, 20, 30, 40);
  console.log(arr); // [10, 20, 30, 40]

  console.log(arr.lastItem()); // 40
  console.log(arr.firstItem); // 10

  // 2. 直接对Array进行扩展
  Array.prototype.secondItem = function () {
    return this[1];
  };
  console.log(arr.secondItem()); // 20

类的混入 mixins

 // JS的类只支持单继承: 也就是只能有一个父类
  // 那么在开发时需要在一个类中添加更多的相似功能, 可以使用混入(mixin)

  // class A {
  //   run() {
  //     console.log("run", this);
  //   }
  // }
  function A(classBase) {
    return class extends classBase {
      run() {
        console.log("run", this === c1);
      }
    };
  }
  // class B {
  //   eat() {
  //     console.log("eat", this);
  //   }
  // }
  //
  function B(classBase) {
    return class extends classBase {
      eat() {
        console.log("eat", this === c1);
      }
    };
  }

  class C {
    constructor(name, age) {
      this.name = name;
      this.age = age;
    }
    study() {
      console.log("study", this === c1);
    }
  }

  // 方式1:
  // var CC = A(B(C));

  // 方式2:
  class CC extends A(B(C)) {}

  var c1 = new CC("zgc", 18);

  c1.run(); // run true
  c1.study(); // study true
  c1.eat(); // eat true

2. JavaScript多态

  // 继承是多态的前提
  class Shape {
    getArea() {}
  }

  class Rectangle extends Shape {
    constructor(width, height) {
      super();
      this.width = width;
      this.height = height;
    }

    getArea() {
      return this.width * this.height;
    }
  }

  class Circle extends Shape {
    constructor(radius) {
      super();
      this.radius = radius;
    }

    getArea() {
      return this.radius * this.radius * 3.14;
    }
  }

  var rect1 = new Rectangle(10, 20);
  var rect2 = new Rectangle(5, 15);

  var c1 = new Circle(10);
  var c2 = new Circle(1);

  // 多态: 同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。
  // 根据不同数据类型的实体提供了统一的接口, 或者使用一个单一的符号来表示多个不同的类型
  // 即不同的数据类型进行同一种操作, 表现出不同的行为, 就是多态的体现
  function getShapeArea(shape) {
    console.log(shape.getArea());
  }

  getShapeArea(rect1); // 200
  getShapeArea(rect2); // 75
  getShapeArea(c1); // 314
  getShapeArea(c2); // 3.14

  // JS中到处都是多态
  function sum(a, b) {
    return a + b;
  }

  sum(10, 20);
  sum("ab", "cd");

  var foo = 123;
  var foo = true;
  var foo = [];

3. 对象字面量的增强写法

// 1. 属性的增强
  // 当对象中的属性值使用变量, 且属性值变量与属性名相同时, 可以简写
  var name = "zgc";
  var age = 18;
  var obj = {
    name,
    age: age,
  };

  console.log(obj); // {name: 'zgc', age: 18}

  // 2. 方法的增强
  var info = {
    foo: function () {
      console.log(this);
    }, // 会绑定this

    rnning() {
      // 是上一种方式的简写
      console.log(this);
    }, // 会绑定this

    bar: () => {
      console.log(this);
    }, // 不会绑定this
  };

  info.bar(); // window
  info.foo(); // info
  info.rnning(); // info

  // 3. 计算属性名
  var key = "address";
  var age = "my" + "Age";
  var user = {
    name: "zgc",
    [key]: "广州",
    [age]: 18,
  };
  console.log(user); // {name: 'zgc', address: '广州', myAge: 18}

4. 数组和对象的解构

  • 数组中解构赋值是按照变量的顺序来进行赋值的
  • 对象是无序的,顺序是任意的, 变量必须与属性同名才能取到正确的值
  • 如果解构失败,变量的值为undefined
  // 1. 数组的解构
  var names = ["zgc", "wf", "cx", "wlc"];

  // 基本使用
  var [name1, name2, name3] = names;
  console.log(name1, name2, name3); // zgc wf cx

  // 剩余数组
  var [a, b, ...c] = names;
  console.log(a, b, c); // zgc wf ['cx', 'wlc']

  // 解构的默认值
  var users = ["zgc", "wf", undefined, undefined];
  var [x, y, z = "default", t] = users;
  console.log(x, y, z, t); // zgc wf default undefined

  // 2. 对象的解构
  const zhao = {
    name: "赵本山",
    age: 66,
    xiaopin: function () {
      console.log("我可以演小品");
    },
  };

  // 基本使用
  var { name, age, xiaopin } = zhao;
  console.log(name, age); // 赵本山 66
  xiaopin(); // 我可以演小品

  // 变量重命名
  var { name, age: myAge } = zhao;
  // console.log(age); // age is not defined
  console.log(name, myAge); // 赵本山 66

  // 默认值
  var { address } = zhao;
  console.log(address); // undefined

  var { address = "广州" } = zhao;
  console.log(address); // 广州

  // 剩余对象
  var info = {
    userName: "赵本山",
    age: 22,
    address: "北京",
  };
  var { userName, ...b } = info;
  console.log(userName, b); // 赵本山 {age: 22, address: '北京'}

  // 3. 多重解构
  var bar = {
    type: {
      title: "obj",
      des: "对象",
    },
    list: ["a", "b"],
  };

  var {
    type: { title, des: desc },
    list: [list1, list2],
  } = bar;

  console.log(title, desc, list1, list2); // obj 对象 a b

  // 4. 应用: 参数解构
  var position = {
    x: 10,
    y: 20,
  };
  function foo1(position) {
    console.log(position.x, position.y);
  }

  function foo2({ x, y }) {
    console.log(x, y);
  }

  foo1(position); // 10 20
  foo2(position); // 10 20

5. 手写apply&call&bind方法

  // call
  Function.prototype.myCall = function (context, ...args) {
    // console.log(this); // 指向调用的函数对象
    context = context === null || context === undefined ? window : Object(context);
    context.fn = this; // this是调用call的函数
    const result = context.fn(...args);
    delete context.fn; // 执行后删除新增属性
    return result;
  };

  // apply
  Function.prototype.myApply = function (context, args = []) {
    // console.log(this); // 指向调用的函数对象
    context = context === null || context === undefined ? window : Object(context);
    context.fn = this; // this是调用apply的函数
    const result = context.fn(...args);
    delete context.fn;
    return result;
  };

  // bind
  Function.prototype.myBind = function (context, ...args) {
    // console.log(this); // 指向调用的函数对象
    const _this = this;  
    return function Bind(...newArgs) {
      // 考虑是否此函数被继承
      if (this instanceof Bind) {
        // console.log(this); // this指向Bind实例对象
        return _this.myApply(this, [...args, ...newArgs]);
      }
      return _this.myApply(context, [...args, ...newArgs]);
    };
  };

  //测试用例
  var message = "我的名字是";
  var obj = {
    message: "My name is: ",
  };

  function getName(firstName, lastName) {
    console.log(this.message + " " + firstName + " " + lastName);
    return 100;
  }
  console.log(getName("zgc", "zgc")); // 我的名字是 zgc zgc 100
  console.log(getName.myCall(obj, "zgc", "wlc")); // My name is:  zgc wlc 100
  getName.myApply(obj, ["zgc", "cx"]); // My name is:  zgc cx

  let bind1 = getName.myBind(obj, "zgc", "wf");
  bind1(); // My name is:  zgc wf

  let bind2 = getName.myBind(obj, "zgc", "hl");
  console.log(new bind2()); // undefined zgc hl

6. let & const关键字

juejin.cn/post/698954…

const 关键字

const 关键字用来声明常量,const 声明有以下特点:

  1. 声明必须赋初始值;
  2. 不允许重复声明;
  3. 块级作用域(局部变量);
  4. 值不允许修改(对数组元素的修改和对对象内部的修改是可以的,数组和对象存的是引用地址);
  5. 不存在变量提升

let 关键字

let 关键字用来声明变量,使用 let 声明的变量有几个特点:

  1. 不允许重复声明;
  2. 块级作用域(当前所处代码块,既可以是全局或者整个函数块,也可以是 if、while等用{}限定的代码块);
  3. 不存在变量提升;( 在块级作用域中,变量需要先声明,然后再使用,在使用let命令声明变量之前,该变量都是不可用的,这被称为暂时性死区;)

var关键字

  1. 允许重复声明
  2. var 声明的变量的作用域只能是全局或者整个函数的。
  3. 存在变量提升
  4. 所有未声明直接赋值的变量都会自动挂在顶层对象下

let & const的基本使用

  // 1. let
  //(1) 声明变量
  let msg = "Hello World";
  msg = "my name is zgc";
  console.log(msg); // my name is zgc

  //  批量批量声明变量
  let b, c, d;

  // 批量声明变量并赋值
  // let f = 521, g = 'iloveyou', h = [];

  //(2) 变量不能重复声明
  // let star = '罗志祥';
  // let star = '小猪'; // Identifier 'star' has already been declared

  //(3) 块级作用域  全局, 函数, eval
  // if else while for

  // {
  //     let girl = '周扬青';
  // }
  // console.log(girl); // girl is not defined

  //(4) 不存在变量提升
  // console.log(song);  // Cannot access 'song' before initialization
  // let song = '恋爱达人';

  //(5) 不影响作用域链
  {
    let school = "尚硅谷";
    function fn() {
      console.log(school);
    }
    fn(); // 尚硅谷
  }

  // 2. const
  // (1) 声明常量
  const SCHOOL = "尚硅谷";

  // (2) 不允许重复声明
  // const foo = () => {};
  // const foo = () => {}; // Identifier 'foo' has already been declared

  //(3) 声明一定要赋初始值
  // const A; // Missing initializer in const declaration

  //(4) 一般常量(字符串, 数字)使用大写(潜规则, 但可以不遵守)
  // const NUM = 100;

  //(5) 常量的值不能修改
  // SCHOOL = 'ATGUIGU';  // Assignment to constant variable

  //(6) 块级作用域
  // {
  //     const PLAYER = 'UZI';
  // }
  // console.log(PLAYER); // PLAYER is not defined

  //(7) 对于数组和对象的元素修改(引用类型), 不算做对常量的修改, 不会报错
  // 数组和对象是引用类型, 对数组和对象内部元素的修改不会改变它们的内存地址
  const team = ["UZI", "MXLG", "Ming", "Letme"];
  team.push("Meiko");
  console.log(team); // ['UZI', 'MXLG', 'Ming', 'Letme', 'Meiko']

  //(8) 不存在变量提升
  // console.log(song);  // Cannot access 'song' before initialization
  // const song = '恋爱达人';

没有作用域提升和暂时性死区

  // let 和 const 没有作用域提升, var存在变量提升
  console.log(msg); // undefined
  var msg = "Hello World";

  // console.log(address); //  Cannot access 'address' before initialization
  // console.log(info); //  Cannot access 'info' before initialization
  let address = "北京";
  const info = {};

  // 暂时性死区
  // 在块级作用域中,变量需要先声明,然后再使用,在使用let/const命令声明变量之前,该变量都是不可用的,这被称为暂时性死区
  // 从块作用域的顶部一直到变量声明完成之前, 这个变量处在暂时性死区
  function foo() {
    // ----- bar的暂时性死区
    console.log("Hello World");
    console.log("你好 世界");
    // ------- bar的暂时性死区
    let bar = "bar";
    let baz = "baz";
  }

  // 暂时性死区和定义的位置没有关系, 和代码的执行顺序有关
  function bar() {
    console.log("user", user);
  }

  // console.log("user", user); // Cannot access 'user' before initialization
  let user = "zgc";
  bar(); // user zgc
  console.log("user", user); // user zgc

  // 暂时性死区形成之后, 在该区域内这个标识符不能访问

  let test1 = "哈哈哈";
  function baz1() {
    console.log(test1);
  }
  baz1(); // 哈哈哈

  let test2 = "哈哈哈";
  function baz2() {
    console.log(test2);
    let test2 = "嘿嘿嘿";
  }
  // baz2(); // Cannot access 'test2' before initialization

块级作用域

  // 1. var
  // var在全局声明的变量会直接挂载到window上
  var message = "message";
  console.log(window.message); // message

  // 所有未声明直接赋值的变量都会自动挂在顶层对象下
  user = "wf";
  console.log(window.user); // wf

  // var 声明的变量只有在函数与全局有作用域
  {
    var msg = "msg";
  }
  console.log(window.msg); // msg

  function foo() {
    if (true) {
      var userName = "zgc";
    }
    console.log(userName);
  }

  foo(); // zgc

  // 2. let & const
  // let & const 声明的变量存在块级作用域
  {
    let a = 1;
    const b = 2;
  }
  // console.log(a, b); //  a is not defined  b is not defined

  function bar() {
    if (true) {
      let userName = "zgc";
      const userAge = 18;
    }
    console.log(11, userName, userAge);
  }
  // bar(); // userName is not defined userAge is not defined

  // 3. let 应用

  let items = document.getElementsByClassName("item");

  // for (var j = 0; j < items.length; j++) {
  //   items[j].onclick = function () {
  //     console.log("j", j); // 3 3 3
  //   };
  // }
  // console.log(j); // 3

  /*
    用var声明变量永远打印出3, 因为外层for循环只是给每个item绑定了点击事件,
     然后点击事件是异步任务,点击时for已经执行完了, 最后 var = 3,var是全局作用域
     点击事件开始向外层作用域找,找不到,就是window.j,此时是3
  */

  for (let i = 0; i < items.length; i++) {
    items[i].onclick = function () {
      console.log("i", i); // 0 1 2
    };
  }
  // console.log(i); // i is not defined

  /*
    如果是let i,具有块级作用域,所以每一次循环都会生成一个新的块级作用域记录当前的i,
    每个块级作用域的i都是不同的, 且在全局没有声明的i变量(块级作用域)
  */

7. 模板字符串

  // ES6 引入新的声明字符串的方式模板字符串

  //1. 声明: ``
  let msg = `我也是一个字符串哦!`;
  console.log(msg, typeof msg); // 我也是一个字符串哦! string

  //2. 内容中可以直接出现换行符
  let str = `<ul>
                <li>沈腾</li>
                <li>玛丽</li>
                <li>魏翔</li>
                <li>艾伦</li>
            </ul>`;

  console.log(str);

  //3. 可以通过${变量}来拼接内容
  let lovest = "魏翔";
  let out = `${lovest}是我心目中最搞笑的演员!!`;
  console.log(out); // 魏翔是我心目中最搞笑的演员!!

  function foo() {
    return "foo";
  }

  // 4. 在${}中可以进行函数调用
  console.log(`my function is ${foo()}`); // my function is foo

  // 5. 标签模板字符串
  /*
   如果我们使用标签模板字符串, 并且在调用的时候插入其他的变量:
      模板字符串会被拆分成数组
      数组的第一个元素也是数组, 是被拆分的字符串组合(以${}为分隔符)
      数组后面的元素是一个个${}传入的内容
  */
  function bar(...args) {
    console.log("参数", args);
  }

  bar("zgc", 18, 1.88); // ['zgc', 18, 1.88]

  const name = "wf";
  const age = 22;
  const height = 1.88;
  bar`my name is ${name}, age is ${age}, height is ${height}`;

image.png

8. 默认参数

  // 1. 函数的默认参数
  // ES6 允许给函数参数赋值初始值

  //1. 形参初始值 具有默认值的参数, 一般位置要靠后(潜规则)
  // 注意: 默认参数会对undefined进行赋值, 但不会对null进行赋值
  function add(a, b, c = 10) {
    return a + b + c;
  }
  console.log(add(1, 2)); // 13
  console.log(add(1, 2, undefined)); // 13
  console.log(add(1, 2, null)); // 3

  //2. 与解构赋值结合
  const obj = { name: "zgc" };

  // 解构赋默认值值
  const { name = "wf", age = 18 } = obj;
  console.log(name, age); // zgc 18

  // 具有默认值的参数, 位置最好放在后面
  // function connect(info = {}) { // 给参数一个默认值 {}
  function connect({ port = 1101, host = "127.0.0.1" } = {}) { // 解构参数, 给解构属性一个默认值
    console.log(host, port);
  }
  var info = {
    host: "atguigu.com",
    port: 3306,
  };
  connect(info); // atguigu.com 3306
  connect(); // 127.0.0.1 1101

  // 3. 具有默认参数的形参, 是不会计算在length之内的
  // 并且之后所有的参数都不会计算在length之内
  function foo1(name, age, height) {}
  console.log(foo1.length); // 3

  function foo2(name, age, height = 1.88) {}
  console.log(foo2.length); // 2

  function foo3(name, height = 1.88, age) {}
  console.log(foo3.length); // 1

  // 4. 如果默认参数与剩余参数一起出现, 默认参数放到剩余参数的前面
  // 剩余参数本来也不会计算在length之内
  function foo4(name, height = 1.78, ...args) {}
  console.log(foo4.length); // 1

9. ...扩展运算符

  //1. 数组的合并
  const kuaizi = ["王太利", "肖央"];
  const fenghuang = ["曾毅", "玲花"];
  const newArr1 = kuaizi.concat(fenghuang);
  const newArr2 = [...kuaizi, ...fenghuang];
  console.log(newArr1, newArr2);

  //2. 数组的浅拷贝
  const sanzhihua = ["E", "G", "M"];
  const sanyecao = [...sanzhihua]; //  ['E','G','M']
  console.log(sanyecao);

  //3. 将伪数组转为真正的数组
  const divs = document.querySelectorAll("div");
  const divArr = [...divs];
  console.log(divArr);

  // 4. 函数参数(展开数组或者字符串)
  const names = ["zgc", "wf", "wlc", "cx"];
  const str = "Hello";
  function foo(name1, name2, ...args) {
    console.log(name1, name2, args);
  }

  foo(...names); // zgc wf ['wlc', 'cx']
  foo(...str); // H e ['l', 'l', 'o']

  console.log(...str); // H e l l o

  // 5. ES9之后可以在对象上使用扩展运算符
  var obj = {
    name: "zgc",
    age: 18,
  };

  // 对象合并
  const info = { ...obj, height: 1.88 };
  console.log(info); // {name: 'zgc', age: 18, height: 1.88}

  // 如果有同名属性 则后面的覆盖掉前面的
  const user = { ...obj, name: "wf" };
  console.log(user); // {name: 'wf', age: 18}

  // 对象的浅拷贝
  const newObj = { ...obj };
  console.log(newObj); // {name: 'zgc', age: 18}

10. 浅拷贝与深拷贝

  • 当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,都会导致另一个对象也发生变化;
  • 浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址, 拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响;
  • 深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,拷贝前后的两个对象互不影响;
  // 1. 引用赋值:
  var obj = {
    name: "zgc",
    age: 18,
  };

  var obj1 = obj;
  obj1.name = "wf";
  console.log(obj.name, obj1.name); // wf wf

  // 2. 浅拷贝
  var info = {
    name: "zgc",
    age: 18,
    detail: {
      height: 1.88,
    },
  };

  const info1 = { ...info };
  info1.name = "wlc";
  info.detail.height = 2.22;

  console.log(info.name, info1.name); // zgc wlc
  console.log(info.detail.height, info1.detail.height); // 2.22 2.22

  // 3. 深拷贝
  var user = {
    name: "zgc",
    age: 18,
    friend: {
      height: 1.88,
    },
  };

  const user1 = JSON.parse(JSON.stringify(user));
  user.name = "cx";
  user1.friend.height = 1.78;

  console.log(user.name, user1.name); // cx zgc
  console.log(user.friend.height, user1.friend.height); // 1.88 1.78

11. 数值的拓展

  //0. Number.EPSILON 是 JavaScript 表示的最小精度
  //EPSILON 属性的值接近于 2.2204460492503130808472633361816E-16
  function equal(a, b) {
    return Math.abs(a - b) < Number.EPSILON ? true : false;
  }
  // console.log(0.1 + 0.2 === 0.3);
  console.log(equal(0.1 + 0.2, 0.3));

  //1. 二进制/八进制/十进制/十六进制
  let b = 0b1010; // 二进制 ob
  let o = 0o777; // 八进制 0o
  let d = 100; // 十进制
  let x = 0xff; // 十六进制 0x
  console.log(b, o, d, x); // 10 511 100 255

  //2. Number.isFinite  检测一个数值是否为有限数
  console.log(Number.isFinite(100)); // true
  console.log(Number.isFinite(100 / 0)); // false
  console.log(Number.isFinite(Infinity)); // alse

  //3. Number.isNaN 检测一个数值是否为 NaN
  console.log(Number.isNaN(123)); // false

  //4. Number.parseInt Number.parseFloat字符串转整数
  console.log(Number.parseInt("5211314love")); // 5211314
  console.log(Number.parseFloat("3.1415926神奇")); // 3.1415926

  //5. Number.isInteger 判断一个数是否为整数
  console.log(Number.isInteger(5)); // true
  console.log(Number.isInteger(2.5)); // false

  //6. Math.trunc 将数字的小数部分抹掉
  console.log(Math.trunc(3.5)); // 3

  //7. Math.sign 判断一个数到底为正数 负数 还是零
  console.log(Math.sign(100)); // 1
  console.log(Math.sign(0)); // 0
  console.log(Math.sign(-20000)); // -1

  // 8. 数字过长时,可与使用_连接符
  const money = 1000000000;
  // 可以写成
  const money1 = 10_0000_0000;
  console.log(money === money1); // true

12. Symbol的基本使用

ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值。它是JavaScript 语言的第七种数据类 型,是一种类似于字符串的数据类型;

Symbol 特点:

  1. Symbol 的值是唯一的,用来解决命名冲突的问题;
  2. Symbol 值不能与其他数据进行运算;
  3. Symbol 定义的对象属性不能使用for…in循环遍历 ,但是可以使用Reflect.ownKeys 来获取对象的 所有键名;
  // 为什么需要Symbol?
  /*
    在ES6之前, 对象的属性名都是字符串形式, 那么很容易造成冲突;
    比如原来有一个对象, 我们希望在其中添加一个新的属性和值,
    但是我们不确定它原来内部有什么内容的情况下, 很容易造成冲突, 从而覆盖掉它内部的某个属性;
  */

  // Symbol就是为了解决上面的问题, 用来生成一个独一无二的值, 它是JavaScript 语言的第七种数据类型
  // Symbol值是通过Symbol函数来生成的, 生成后可以作为属性名
  // 在ES6中, 对象的属性名可以使用字符串, 也可以使用Symbol值

  // 1. 基本使用
  const s1 = Symbol();
  var obj = {
    name: "zgc",
    [s1]: "aaa",
  };

  const s2 = Symbol();
  obj[s2] = "bbb";

  console.log(obj);

  // Symbol函数执行后, 每次创建出来的值都是独一无二的
  console.log(s1 === s2); // false

  // 2. 获取Symbol对应的key
  // 这种方法获取不到Symbol对应的key
  // console.log(Object.keys(obj)); // ['name']

  console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(), Symbol()]

  const symbols = Object.getOwnPropertySymbols(obj);
  for (let key of symbols) {
    console.log(obj[key]); // aaa, bbb
  }

  // 3.  Symbol函数执行后, 每次创建出来的值都是独一无二的, 但我们可以通过 Symbol.for来创建相同的Symbol
  /*
  它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。
  如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,
  并将其注册到全局。

  */
  const s3 = Symbol.for();
  const s4 = Symbol.for();
  console.log(s3 === s4); // true

  // 4. 我们可以在创建Symbol值的时候传入一个描述desc
  const s5 = Symbol("我是S5");
  console.log(s5.description); // 我是S5
  const s6 = Symbol(s5.description);
  console.log(s5 === s6); // false

  // 相同的desc, 可以用Symbol.for创建出相同的Symbol值
  const s7 = Symbol.for("我是S7");
  console.log(s7.description); // 我是S7
  // console.log(Symbol.keyFor(s7)); // 我是S7

  const s8 = Symbol.for(s7.description);
  console.log(s7 === s8); // true

  const s9 = Symbol.for("我是S9");
  console.log(s8 === s9); // false

  /*
  Symbol.for()与Symbol()这两种写法,都会生成新的 Symbol。
  它们的区别是,前者会被登记在全局环境中供搜索,后者不会。
  Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,
  而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。
  比如,如果你调用Symbol.for("cat")30 次,每次都会返回同一个 Symbol 值,
  但是调用Symbol("cat")30 次,会返回 30 个不同的 Symbol 值。
  */

  // 5. 不能与其他数据进行运算
  // let result = s + 100;
  // let result = s > 100;
  // let result = s + s;

13. Set

ES6 提供了新的数据结构 Set(集合),它类似于数组,但成员的值都是唯一的,集合实现了 iterator 接口,所以可以使用『扩展运算符』和『for…of…』进行遍历,集合的属性和方法:

Set的属性和方法:

  1. size 返回集合的元素个数;
  2. add 增加一个新元素,返回当前集合;
  3. delete 删除元素,返回 boolean 值;
  4. has 检测集合中是否包含某个元素,返回 boolean 值;
  5. clear 清空集合,返回 undefined;
  // 一. Set的基本使用
  // 创建Set
  let s = new Set();
  console.log(s, typeof s);

  // 添加元素
  let s1 = new Set(["大哥", "二哥", "三哥", "四哥", "三哥"]);
  console.log(s1); // 自动去重 {'大哥', '二哥', '三哥', '四哥'}

  // 1. size 返回集合的元素个数;
  console.log(s1.size); // 4

  // 2. add 增加一个新元素,返回当前集合;
  let s2 = s1.add("大姐");
  console.log("s2", s2, s1);
  // {'大哥', '二哥', '三哥', '四哥', '大姐'} {'大哥', '二哥', '三哥', '四哥', '大姐'}

  // 3. delete 删除元素,返回 boolean 值;
  let result = s1.delete("三哥");
  console.log("result", result); // true
  console.log("s3", s1); // {'大哥', '二哥', '四哥', '大姐'}

  // 4. has 检测集合中是否包含某个元素,返回 boolean 值;
  let s4 = s1.has("二姐");
  console.log("s4", s4); // false

  // 5. clear 清空集合,返回 undefined;
  let s5 = s1.clear();
  console.log("s5", s5, s1); // undefined {}

  // 5.forEach 遍历
  let s6 = new Set(["大哥", "二哥", "三哥", "四哥", "三哥"]);
  s6.forEach((item, index) => console.log(item, index));
  // 大哥 大哥
  // 二哥 二哥
  // 三哥 三哥
  // 四哥 四哥

  // 6. for of
  for (let v of s6) {
    console.log(v); // 大哥, 二哥, 三哥, 四哥
  }

  // 二. Set应用
  let arr = [1, 2, 3, 4, 5, 4, 3, 2, 1];

  //1. 数组去重
  // let result1 = [...new Set(arr)];
  let result1 = Array.from(new Set(arr));
  console.log(result1);

  //2. 交集
  let arr2 = [4, 5, 6, 5, 6];
  // let intersection = [...new Set(arr)].filter((item) => {
  //   let s2 = new Set(arr2); // 4 5 6
  //   if (s2.has(item)) {
  //     return true;
  //   } else {
  //     return false;
  //   }
  // });

  let intersection = [...new Set(arr)].filter((item) =>
    new Set(arr2).has(item)
  );
  console.log(intersection);

  //3. 并集
  let union = [...new Set([...arr, ...arr2])];
  console.log(union);

  //4. 差集
  let diff = [...new Set(arr)].filter((item) => !new Set(arr2).has(item));
  console.log(diff);

14. Map

ES6 提供了 Map 数据结构, 它类似于对象,也是键值对的集合, 但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键, 也实现了iterator 接口,所以可以使用『扩展运算符』和『for…of…』进行遍历;

Map 的属性和方法:

  1. size 返回 Map 的元素个数;
  2. set 增加一个新元素,返回当前 Map;
  3. get 返回键名对象的键值;
  4. has 检测 Map 中是否包含某个元素,返回 boolean 值;
  5. clear 清空集合,返回 undefined;
  6. delete删除元素
  // 1. Map的基本使用

  // 创建一个空 map
  let m = new Map();

  // 1. size 返回 Map 的元素个数;
  console.log(m.size); // 0

  // 2. set 增加/修改一个新元素,返回当前 Map;
  // 添加
  m.set("皇帝", "大哥");
  m.set("丞相", "二哥");
  const obj = { name: "zgc" };
  m.set(obj, "wf");
  console.log(m); // {'皇帝' => '大哥', '丞相' => '二哥', {…} => 'wf'}

  // 修改
  console.log(m.set("皇帝", "父亲")); // {'皇帝' => '父亲', '丞相' => '二哥', {…} => 'wf'}

  // 3. get 返回键名对象的键值;
  console.log(m.get("皇帝")); // 父亲
  console.log(m.get(obj)); //  wf

  // 4. has 检测 Map 中是否包含某个元素,返回 boolean 值;
  console.log(m.has("皇帝"));

  // 5. delete, 删除元素,返回 boolean 值
  const result = m.delete(obj);
  console.log(result, m); // true  {'皇帝' => '父亲', '丞相' => '二哥'}

  // 5. clear 清空集合,返回 undefined;
  const result1 = m.clear();
  console.log(result1, m); // undefined {size: 0}

  // 6. forEach
  let m2 = new Map([
    ["皇帝", "大哥"],
    ["丞相", "二哥"],
    [obj, "wf"],
  ]);
  console.log("m2", m2);
  m2.forEach((item, index) => console.log(item, index));
  // 父亲 皇帝
  // 二哥 丞相
  // wf { name: "zgc" }

  // 7. for of

  for (let item of m2) {
    console.log(item);
  }
  // ['皇帝', '大哥']
  // ['丞相', '二哥']
  // [{name: 'zgc'}, 'wf']

  for (let item of m2) {
    const [key, value] = item;
    console.log(key, value);
  }
  // 皇帝 大哥
  // 丞相 二哥
  // {name: 'zgc'} 'wf'

5. ES7+新特性

1. Array.includes和指数运算符

 // includes
  const books = ["西游记", "红楼梦", "三国演义", "水浒传"];

  console.log(books.includes("西游记")); // true
  console.log(books.includes("金瓶梅")); // false

  //indexOf

  console.log(books.indexOf("红楼梦")); // 1
  console.log(books.indexOf("遮天")); // -1

  // 指数运算符
  console.log(2 ** 10); // 1024
  console.log(Math.pow(2, 10)); // 1024

2. 对象相关方法补充

  const school = {
    name: "尚硅谷",
    cities: ["北京", "上海", "深圳"],
    subject: ["前端", "Java", "大数据", "运维"],
  };

  //1. 获取对象所有的键
  console.log(Object.keys(school)); // ['name', 'cities', 'subject']

  //2. 获取对象所有的值
  console.log(Object.values(school));
  // ['尚硅谷', ["北京", "上海", "深圳"],["前端", "Java", "大数据", "运维"]]

  //3. entries
  const arr = Object.entries(school);
  console.log("arr", arr);
  // [['name', '尚硅谷'], ['cities', Array(3)], ['subject', Array(4)]

  // 遍历
  for (const item of arr) {
    const [key, value] = item;
    console.log(key, value);
  }

  // 也可以操作字符串/数组
  console.log(Object.entries(["abc", "cba"])); // [['0', 'abc'],['1', 'cba']]
  console.log(Object.entries("Hi")); // [['0', 'H'], ['1', 'i']]

  //4. 获取对象所有属性的描述对象
  console.log(Object.getOwnPropertyDescriptors(school));
  /*
  {
  cities: {value: Array(3), writable: true, enumerable: true, configurable: true}
  name: {value: '尚硅谷', writable: true, enumerable: true, configurable: true}
  subject: {value: Array(4), writable: true, enumerable: true, configurable: true}
  }
  */

  // ES6:
  // 1. Object.is 比较两个值是否严格相等,与『===』行为基本一致(+0 与 NaN);
  console.log(Object.is(120, 120)); // true

  // 注意下面的区别
  console.log(Object.is(NaN, NaN)); // true
  console.log(NaN === NaN); // false

  console.log(Object.is(-0, +0)); // false
  console.log(-0 === +0); // true

  // NaN与任何数值做===比较都是false,跟他自己也如此!

  // 2. Object.assign 对象的合并,将源对象的所有可枚举属性,复制到目标对象;
  const config1 = {
    host: "localhost",
    port: 3306,
    test: "test",
  };
  const config2 = {
    host: "http://zgc.com",
    port: 3003,
    test2: "test2",
  };
  // 如果前边有后边没有则保 ,如果前后都有,后面的会覆盖前面的
  const info = Object.assign(config1, config2);
  console.log(info);
  // {host: 'http://zgc.com', port: 3003, test: 'test', test2: 'test2'}

3. 字符串的填充方法

  // padStart padEnd
  var a = "9".padEnd(2, "4");
  var b = "7".padStart(3, "0");
  console.log(a, b); // 94 007

  // 对时间进行格式化
  var minute = "5";
  var second = "36";
  console.log(`${minute}:${second}`); //5:36

  var minute = "5".padStart(2, "0");
  var second = "36".padStart(2, "0");
  console.log(`${minute}:${second}`); //05:36

  // 敏感数据格式化:身份证/银行卡
  const cardNum = "370782200410160225";
  const lastNum = cardNum.slice(-4);
  const result = lastNum.padStart(cardNum.length, "*");
  console.log(result); // **************0225

4. flat & flatMap

 // Array.prototype.flat 与 flatMap

  // flat: 将多维数组转换成低维数组

  // 将二维数组转换成一维数组
  const arr = [1, 2, 3, [4, 5], 6, 7];
  console.log(arr.flat()); // [1, 2, 3, 4, 5, 6, 7]

  // 将三维数组转换成二维数组
  const arr2 = [1, 2, 3, [4, 5, [6, 7]], 8, 9];
  console.log(arr2.flat()); // [1, 2, 3, 4, 5, [6, 7], 8, 9]

  // 将三维数组转换成一维数组
  console.log(arr2.flat(2)); // [1, 2, 3, 4, 5, 6, 7, 8, 9]

  // 将n维数组转换成一维数组
  // arr.flat(n-1)

  // flatMap
  // 对数组先进行map操作, 在进行flat操作
  // flatMap中flat的深度为1
  const arr3 = [1, 2, 3, 4, 5];
  const result1 = arr3.map((item) => item * 10);
  console.log(result1); // [10, 20, 30, 40, 50]

  const result2 = arr3.map((item) => [item * 10]);
  console.log(result2); // [[10], [20], [30], [40], [50]]

  const result3 = arr3.flatMap((item) => [item * 10]);
  console.log(result3); // [10, 20, 30, 40, 50]

  // 应用: 以空格为标准切割字符串且放入数组
  // 1. for of
  const message = ["Hello World", "你好 zgc"];
  const strArr = [];
  for (let item of message) {
    // console.log(item); // Hello World
    const infos = item.split(" ");
    // console.log(infos); // ['Hello', 'World']
    for (let info of infos) {
      strArr.push(info);
    }
  }
  console.log(strArr); // ['Hello', 'World', '你好', 'zgc']

  // 2. map 与 flat
  const mapArr = message.map((item) => item.split(" "));
  console.log(mapArr); // [['Hello', 'World'], ['你好', 'zgc']]
  console.log(mapArr.flat()); // ['Hello', 'World', '你好', 'zgc']

  // 3. flatMap
  const flatArr = message.flatMap((item) => item.split(" "));
  console.log(flatArr); // ['Hello', 'World', '你好', 'zgc']

5. Object.fromEntries

//  Object.fromEntries(), 将Object.entries生成的数组转回对象
  const obj = {
    name: "zgc",
    age: 18,
    friend: ["wf", "cx"],
  };
  const result = Object.entries(obj);
  console.log(result); // [['name', 'zgc'], ['age', 18], ['friend', ['wf', 'cx']]]

  const reverse = Object.fromEntries(result);
  console.log(reverse); // {name: 'zgc', age: 18, friend: ["wf", "cx"]}

  // 应用
  const search = "?name=zgc&age=18&height=1.78";
  const params = new URLSearchParams(search);

  console.log(params.get("name")); // zgc
  console.log(params.get("age")); // 18

  for (const item of params.entries()) {
    console.log(item);
  }
  // ['name', 'zgc']
  // ['age', '18']
  // ['height', '1.78']

  const paramObj = Object.fromEntries(params.entries());
  console.log(paramObj); // {name: 'zgc', age: '18', height: '1.78'}

6. trimStart&trimEnd

  // trim(): 去除首尾空格
  // trimStart(): 去掉首部空格
  // trimEnd():去掉尾部空格
  const message = "  Hello World ";
  console.log(message); // '  Hello World '
  console.log(message.trim()); // 'Hello World'
  console.log(message.trimStart()); // 'Hello World '
  console.log(message.trimEnd()); // '  Hello World'

7. BigInt

  // BigInt: 大整型
  // 在早期的JS中, 我们不能正确的表示过大的数字
  // 大过MAX_SAFE_INTEGER的数值, 表示的可能是不正确的

  console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991

  let n = 100n;
  console.log(n, typeof n); // 100n 'bigint'
  // 函数:普通整型转大整型
  let m = 123;
  console.log(BigInt(m)); // 123n

  // 用于更大数值的运算
  let max = Number.MAX_SAFE_INTEGER;

  console.log(max); // 9007199254740991
  console.log(max + 1); // 9007199254740992
  console.log(max + 2); // 9007199254740992

  console.log(BigInt(max)); // 9007199254740991n
  console.log(BigInt(max) + BigInt(1)); //9007199254740992n
  console.log(BigInt(max) + BigInt(2)); // 9007199254740993n

8. 空值合并运算符

  // ?? 空值合并运算符
  // 只有当 ?? 前面表达式为 null 或者 undefined时才会执行后面语句

  console.log({} ?? "默认值"); // {}
  console.log("" ?? "默认值"); // ''
  console.log(false ?? "默认值"); // false
  console.log(0 ?? "默认值"); // 0
  console.log(undefined ?? "默认值"); // 默认值
  console.log(null ?? "默认值"); // 默认值

  // 注意 ?? 与 || 运算符的区别
  console.log({} || "默认值"); // {}
  console.log("" || "默认值"); // 默认值
  console.log(false || "默认值"); // 默认值
  console.log(0 || "默认值"); // 默认值
  console.log(undefined || "默认值"); // 默认值
  console.log(null || "默认值"); // 默认值

9. 可选链运算符

  // 可选链操作符 ?.

  // 1. 在 ES5 访问对象的深嵌套属性时obj.a.b,首先需要检查它的上一个属性是否存在,然后才能获取属性的值,否则就会报错
  let obj1 = {};
  console.log(obj1.a); //undefined
  // console.log(obj1.a.b); // => undefined.b 会报错

  // 2. ES5中借助 && 来保证程序的健壮性,但当嵌套的对象很深时,则要对每一层进行验证,这样不利于阅读,而且容易出现程序上的错误
  var obj2 = {};
  var b = obj2.a && obj2.a.b;
  console.log(b); // undefined

  // 3. 可选链操作符使用  ?.  来表示,可以判断操作符之前属性是否有效,从而链式读取对象的属性或返回 undefined 。
  const config = {
    db: {
      host: "192.168.1.100",
    },
    friend: {
      name: "zgc",
      run: function () {
        console.log("run");
      },
    },
  };

  // 传统写法
  // const dbHost = config && config.db && config.db.host;
  // 可以直接 config.db.host,如果数据都存在那和上面的没区别,但如果有一个数据不存在就报错

  // 可选链操作符对象写法
  console.log(config?.db?.host); // 192.168.1.100
  console.log(config.db.name); // undefined
  // console.log(config.user.name); // Cannot read properties of undefined (reading 'name')

  // 传统写法
  if (config && config.friend && config.friend.run) {
    config.friend.run(); // run
  }

  // 可选链操作符函数写法
  config?.friend?.run?.(); // run

  // 可选链访问数组元素
  var arr = [];
  console.log(arr?.[5]); // undefined

10. 逻辑赋值运算符

  // 赋值运算符
  let count = 100;
  count = count + 50;
  count += 100;

  // 逻辑赋值运算符
  function foo(msg) {
    //  ||
    // msg = msg || "默认值";
    // msg ||= "默认值";

    // ??
    // msg = msg ?? "默认值";
    msg ??= "默认值";
    console.log(msg);
  }

  foo("and"); // and
  foo(); // 默认值

  // &&
  var obj = {
    name: "zgc",
    foo: function () {},
  };

  // obj = obj && obj.foo()
  obj &&= obj.foo();

11. 字符串replaceAll

  const message = "my name is zgc, zgc age is 18.";

  // 只替换第一个
  const msg1 = message.replace("zgc", "wf");
  console.log(message, msg1);
  // my name is zgc, zgc age is 18.
  // my name is wf, zgc age is 18.

  // 全部替换
  const msg2 = message.replaceAll("zgc", "wf");
  console.log(message, msg2);
  //  my name is zgc, zgc age is 18.
  //  my name is wf, wf age is 18.

12. Object.hasOwn

  // obj.hasOwnProperty: 判断对象上是否拥有某属性或者方法
  const obj = {
    name: "zgc",
    age: 18,
    foo: function () {},
  };
  console.log(obj);
  obj.__proto__.address = "北京";
  obj.__proto__.bar = function () {};
  console.log(obj.age, obj.address); // 18 '北京'

  console.log(obj.hasOwnProperty("age")); // true
  console.log(obj.hasOwnProperty("address")); // false
  console.log(obj.hasOwnProperty("foo")); // true
  console.log(obj.hasOwnProperty("bar")); // false

  // Object.hasOwn(对象, 属性/方法名): 用来代替obj.hasOwnProperty
  console.log(Object.hasOwn(obj, "name")); // true
  console.log(Object.hasOwn(obj, "address")); // false
  console.log(Object.hasOwn(obj, "foo")); // true
  console.log(Object.hasOwn(obj, "bar")); // false

13. class新成员

  class Person {
    // 实例属性:
    // punlic 公共属性
    height = 1.88;

    // 私有的属性, 但这是属于程序员直接的约定, 其实还是可以被访问的
    _intro = "my name is why";

    // 私有的属性, Person类外界无法访问
    #intro = "my name is why";

    // 类属性(静态属性):
    // 无法通过实例访问
    static total = '70亿'

    // 私有的类属性
    static #maleTotal = '40亿'

    constructor(name, age) {
      // 对象的属性: 在constructor中通过this来设置的
      this.name = name;
      this.age = age;
      this.address = "北京";
    }

    // 静态代码块
    static {
      console.log('在第一次加载解析类的时候就会执行');
      console.log('仅执行一次');
    }
  }

  const p1 = new Person("zgc", 18);
  console.log(p1.address, p1.name, p1.age, p1.height, p1._intro); // 北京 zgc 18 1.88 my name is why
  // console.log(p1.#intro); // Property '#intro' is not accessible outside class 'Person' because it has a private identifier.

  console.log(p1.total); // undefined
  console.log(Person.total); // '70亿'
  // console.log(Person.#maleTotal); // Property '#maleTotal' is not accessible outside class 'Person' because it has a private identifier.