likes
comments
collection
share

JavaScript 进阶 ES6

作者站长头像
站长
· 阅读数 5

JavaScript新特性

紧接上文,JS入门手册💯

这篇文章介绍了,JavaScript的基本语法,而随着时代发展,JS早已今非昔比,推荐一个大佬的文章:阮一峰老师

ECMAScript

ECMAScript(简称“ES”)是根据:ECMA-262 标准实现的通用脚本语言

ECMA-262 标准主要规定了这门语言的语法、类型、语句、关键字、保留字、操作符、对象等几个部分.

  • ESMA (European Computer Manufacturers Association) 是一个组织

    中文名称为:欧洲计算机制造商协会,这个组织的目标是评估、开发和认可电信和计算机标准.

  • Ecma 国际制定了许多标准,而 ECMA-262 只是其中的一个

    ECMA-262 定义了 JavaScript 的语法、语义、基本对象和操作,以及与浏览器环境和其他宿主环境的交互等方面的规则

    TC39(Technical Committee 39) 是推进 ECMAScript 发展的委员会,其会员都是公司 其中主要是浏览器厂商,有苹果、谷歌、微软、因特尔等)

ES6

是ECMA-262,在2015年发布的新版本ES此后每年进行更新~

ES6 的版本变动内容最多,具有里程碑意义 ,引入了许多新的语法特性、功能和改进,使得 JavaScript 编码更加现代化、清晰和高效。

ES6的兼容性:

我们都知道,JavaScript在不同的浏览器中具有不同的兼容性,因为ES每年都会更新,

所以,一些较旧的浏览器可能不完全支持所有的 ES6 特性,可以通过官网进行查询兼容环境配置:ES6兼容性

  • 现在大部分的浏览器都兼容ES6,也可以修改上述,URL来查询不同版本的ES环境兼容

let 关键字

let 是 ES6 引入的一个关键字,用于声明变量

相比于使用 var 声明变量,let 具有更好的作用域控制和块级作用域特性。

  • 全局对象属性

    使用 var 声明的变 量会成为全局对象的属性,也是根据环境而言的

    而使用 let 声明的变量不会。这意味着使用 let 声明的变量不会污染全局命名空间

  • { 块级作用域 }

    let 声明的变量具有块级作用域,意味着变量的作用域限制在声明它的代码块内

  • 不存在变量提升

    let 声明的变量不会发生变量提升,变量只有在声明之后才能被访问和使用,变量提升可以看一这篇文章👉

    临时死区: let 声明的变量在其声明之前不能被访问,这被称为临时死区会抛出错误

  • 不允许重复声明

    在同一个作用域内,不可以使用 let 重复声明同名的变量。这有助于避免出现命名冲突和不必要的错误。

  //重复声明:
  //在同一个作用域内,不可以使用 let重复声明同名的变量,这有助于避免出现命名冲突和不必要的错误
  // var a = "123";
  let a = "let123";       //let重复声明在这里就会报错预警了...(注释运行);
  // let a = "let321";       //let变量声明,会检查当前环境中是否存在同名变量,存在则不予运行;
  console.log(a);
  //{块级作用域}+变量提升+命名污染
  //使用 let声明的变量具有块级作用域,意味着变量的作用域限制在声明它的代码块内如: {花括号所包含的范围}
  let obj = "代码块外部obj";
  {
      //ES6允许你在代码块中使用 let 和 const 声明变量,
      //将变量的作用域限制在块级范围内,避免了传统的变量提升、重名污染问题.
      let obj = "{代码块内部obj} 二者不相互影响"
      let obj2 = "{代码块中的变量,外部也无法访问}"
      console.log(obj);
      console.log(obj2);
  }
  console.log(obj);
  // console.log(obj2);  //外部找不到obj2报错.

循环作用域:

经过,上述我已经了解了,let一种新的变量声明方式,更加像Java高级语言了,看的出来JS想要逆袭当大哥了🤖

这里举个例子更加深入的了解一下let的强大:循环中的作用域: 这也是let出现的原因:

ES5原始版本一直存在一个问题:

  • var 声明的变量具有函数级作用域,
  • 而在循环体内部声明的变量在整个函数范围内都是可见的,这可能导致一些意外的行为:
  //使用 var 声明的变量在循环体内部具有函数级作用域
  ///这意味着循环内部的变量会被提升到循环外部,从而导致循环迭代时可能出现意外的行为。
  for (var i = 0; i < 5; i++) {
      setTimeout(function() {
        console.log(i);           //输出:5 五次,因为循环结束时i的值为 5
      }, 100);
  }

ES6中的循环作用域:

  • ES6 中,使用 let 声明变量可以在每次循环迭代时创建一个新的块级作用域,避免了上述问题。
  //ES6 中使用 `let` 声明变量可以在每次循环迭代时创建一个新的块级作用域,避免了循环作用域问题。
  for (let i = 0; i < 5; i++) {
      setTimeout(function() {
        console.log(i);           //输出 0、1、2、3、4每次循环迭代都会创建一个新的块级作用域.
      }, 100);
  }

const 关键字

const 是 ES6 引入的一个关键字,用于声明常量

let 不同,使用 const 声明的变量必须在声明时初始化,而且其值在初始化后不能被修改 (常量)

  • 不可修改: const 声明的变量的值在初始化之后不能被修改,这意味着一旦赋值,就无法再修改它的值

  • 块级作用域: const 也具有块级作用域,类似于 let,只在声明它的代码块内有效

  • 声明初始化: 使用 const 声明的变量必须在声明时初始化,即赋予一个初始值

  • 对象和数组: 使用 const 声明的变量可以保存对象和数组等复杂数据类型,

    变量本身的引用不能被修改,即不能将 const 变量指向另一个对象或数组,但对象或数组的内容可以修改

常量命名规范: 通常将使用 const 声明的变量命名为大写字母和下划线的组合,以表示它是一个常量

  //结合了上面的let const就很容易了...
  //声明一个常量 PI 并初始化为 3.14159
  const PI = 3.14159;  
  // PI = 3.14;       //这行代码会报错,因为常量 PI 的值不能被修改;
  
  const person = {
    name: 'John',
    age: 30
  };
  person.age = 31;            // 对象的属性可以修改
  person.city = 'New York';   // 对象的属性可以新增
  // person = {};  // 这行代码会报错,因为常量 person 本身的引用不能被修改

解构赋值

解构赋值是一种在 JavaScript 中从数组|对象,中提取值并赋给变量的语法

使得操作复杂数据结构变得更加方便和可读,解构赋值适用于 数组、对象、函数参数...

通过使用花括号 []|{} 来匹配,[数组]|{对象} 的属性,并将匹配的下标,属性值赋给相应的变量,下标|同名属性存在默认值情况,则覆盖

数组解构赋值

你可以从数组中提取元素并赋给变量,基于它们在数组中的位置(下标)。

  • 语法: let [变量1,变量2,变量...] = [数组]

    变量1、变量2、与数组对用的下标进行匹配

  let numbers = [0,1,2,3];
  let [a,b,c,d=33,e=55,f] = [0,1,2,3];
  console.log(a);
  console.log(b);
  console.log(c);
  console.log("变量和数组下标都存在值,数组为准: "+d);              //3
  console.log("如果声明变量下标大于数组,变量默认值: "+e);           //55  
  console.log("如果声明变量下标大于数组,无变量默认值: "+f);         //undefined
  
  //解构赋值可以简写,直接使用数组|对象进行赋值;
  let [a1,b1,c1,d1=33,e1=55,f1] = numbers;    //与上述一样效果;

对象结构赋值

从对象中提取属性值并赋给变量,基于它们在对象中的属性名

  • 语法: let {匹配属性名1,匹配属性名2,匹配属性名...} = {属性1,属性2,属性...}
 let obj = {
     name : "张三",
     age : 18,
 }
 let { name,age = 19,gender,birthday=new Date() } = obj;
 console.log("无匹配属性默认: "+gender);                    //undefined
 console.log("无匹配可自定义: "+birthday);                  //默认标准时间
 console.log("匹配优先采用对象属性值: "+age);                 //匹配优先采用对象属性值: 18

函数参数解构赋值:

解构赋值: 允许你从传入的对象或数组中提取值,并将它们作为函数的参数,这使得传递和处理数据更加方便和灵活

对象解构作为函数参数:

  /**对象解构作为函数参数 */
  let person = { firstName: "John", lastName: "Doe", age: 30 };
  function printPersonInfo({firstName,lastName,age}) { console.log(`Name: ${firstName} ${lastName}, Age: ${age}`); }
  
  printPersonInfo(person);        //Name: John Doe, Age: 30
  • printPersonInfo() 函数接受一个对象参数,并通过解构赋值提取了 firstNamelastNameage 属性

    当传入 person 对象时,解构会将相应的属性值传递给函数。

数组解构作为函数参数:

  let point = [10, 20];
  function printCoordinates([x,y]) { console.log(`X: ${x}, Y: ${y}`); }
  printCoordinates(point);        //X: 10, Y: 20
  • printCoordinates() 函数接受一个数组参数

    并通过解构赋值提取了数组中的元素,当传入 point 数组时,解构会将数组元素的值传递给函数

设置默认值:

解构赋值的特性,可以给函数参数传递时候,设置参数默认值.

  let person2 = { firstName: "John", lastName: "Doe" };
  function printPersonInfo({firstName = "Unknown", lastName = "Unknown", age = "Unknown"}) { 
      console.log(`Name: ${firstName} ${lastName}, Age: ${age}`); 
  }
  printPersonInfo(person2);       //Name: John Doe, Age: Unknown
  • person 对象中缺失了 age 属性,函数会使用默认值 "Unknown"

使用解构赋值在函数参数中可以使函数调用更加清晰,并且允许你选择提取对象或数组的特定部分来处理.

模板字符串

模板字符串是 ES6 引入的一个特性,它提供了一种更便捷、可读性更高的方式来创建字符串。

模板字符串使用反引号包裹,允许在字符串中插入变量、表达式和多行文本,从而避免了传统字符串拼接的繁琐和不直观;

  const age = 30;
  const name = 'Alice';
  const message = `我叫${name} 今年${age}岁,
  明年${age+1}岁,后年${age+2},还有 ${12-(age%12)+'年'}就是`本命年``;
  
  console.log(message);       //输出: 我叫Alice 今年30岁,
                              //      明年31岁,后年32,还有 6年就是`本命年`

REST 参数使用

ES6中 REST 参数是一个强大的特性,用于处理不定数量的函数参数,为我们提供了一种简单而灵活的方式:

 /** REST参数的使用 */
 //REST 参数使用三个点 ... 作为前缀,放在函数参数列表的最后
 function show(a,b,...c){
     console.log(a);
     console.log(b);
     console.log(c+"\n"); 
 }
 ​
 show(1);                //未赋值参数默认: undefined
 show(1,2,3,4);          //REST参数 ...c: 将后面接受的参数以数组的行驶进行存储

REST 和 arguments对象的区别:

 /** REST参数和Arguments对象的区别: */
 function showa(a,b){
     console.log(a);
     console.log(b);
     console.log(arguments.length+"\n"); 
 }
 showa(1);                   //arguments长度1
 showa(1,2,3,4);             //arguments长度4    arguments对象相当于是一个参数列表的对象 可以获取函数所有的参数,以对象形式存储;

扩展运算符使用

ES6中的扩展运算符 也称为spread运算符,它可以让我们更方便地处理数组|对象

 /** ES6扩展运算符 */
 //『...』 扩展运算符能将『数组』转换为逗号分隔的『参数序列』
 const odeity = ["蒙德","璃月","稻妻","须弥","枫丹","纳塔"];
 console.log(odeity);    //数组形式输出
 console.log(...odeity); //x,x,x形式输出
 ​
 /** 扩展运算符常用于对数组的操作使用: */
 //数组合并
 const nums1 = [1,2,3];
 const nums2 = [4,5,6];
 console.log("两个数组元素拆出来并放在新的数组对象中: "+[...nums1,...nums2]);
 ​
 //数组克隆: 
 const str1 = ["E","G"];
 const str2 = [...str1];
 console.log("克隆新数组:"+str2+",需要注意基本数据类型copy元素值,引用数据类型copy元素地址");
 ​
 //伪数组对象--转数组
 //有部分的对象但是存储方式是数组形式的可以通过 ...obj 转换成数组,比如 arguments
 function show(){ 
     console.log(arguments); 
     console.log(...arguments); 
 }
 show("a","b","c","d","e","f","g",);     //对象也可以通过 扩展运算符 转化为多个变量

扩展运算符和 REST参数:

虽然都是...但是它和REST并不是一个东西别混淆

  • REST: 一般声明在函数的形参列表中,对多参数函数的一个数组化管理
  • 扩展运算符: 是对数组|对象进行格式化拆分,一般以实参形式使用

字面量{ 对象简写 }

ES6引入了一种字面量对象简写 ,使得创建和定义对象变得更加方便和清晰,个人觉得并不清晰,实际开发请根据项目规则使用

属性|函数简写:

  • 如果已存在(变量|函数,和字面量对象内部(属性|方法),同名且值相同则可以直接引用;

函数定义简写:

  • 字面量对象中:定义函数可以省略 function

    原始声明: var 字面量对象 = { 函数:function(){ ... } }

    对象简写: let 字面量对象 = { 函数(){ ... } }

计算属性名: 允许使用已存在变量值,作为字面量对象的属性名;动态属性名

拼接属性名: 允许使用已存在变量值,[多个变量+拼接] 为字面量对象的属性名;动态属性名

  //初始化一些变量|函数;
  let name = "wsm";
  let age = 18;
  let i = 1;
  function say(){ console.log(`我叫${name},今年${age}`); }
  
  //字面量创建对象:
  //属性|函数简写: 如果已存在(变量|函数),和字面量对象内部(属性|方法)同名且值相同则可以直接引用;
  //原始写法:     var obj = { name:name,age:age,say:say}
  let obj = { name,age,say }
  console.log(obj.name);  //wsm
  console.log(obj.age);   //18
  console.log(obj);       //{ name: 'wsm', age: 18, say: [Function: say] }
  obj.say();              //我叫wsm,今年18
  
  //函数定义简写: 字面量对象中定义函数可以省略 function;
  //原始写法: var obj2 = { newName:"540",name:name,newSay: function(){ console.log(`我叫${this.name},新名字是${this.newName}`) } }
  let obj2 = { newName:"540",name,newSay(){ console.log(`我叫${this.name},新名字是${this.newName}`) } }
  console.log(obj2);
  obj2.newSay();
  
  //计算属性名:  允许使用已存在变量值,作为字面量对象的属性名(动态属性名)
  //拼接属性名:  允许使用已存在变量值,多个变量[拼接]为字面量对象的属性名(动态属性名)
  let obj3 = { name:"540", [name+i]:"540i号" }
  console.log(obj3);      //{ name: '540', wsm1: '540i号' }   属性名是对象的值|多个对象值组合,可以用于动态属性名;

注意☢:

该简写方式仅仅,适用于 字面量形式

本人在学习时候困惑了很久,因为之前学习JS时候,JS声明对象的千奇百怪的写法:字面量new Object()

之后为了方便批量创建对象,使用了工厂函数() ,又为了区分对象的类型了解到了 new 构造函数()为了避免内存泄漏了解到了原型链接

  • 所以接触到这种写法时候,突然有一点尬了,所以建议和我一样刚上手的朋友,只需要关注这是字面量声明对象的一种:语法糖🍬 不必多想
  • 如果有朋友想了解: JS对象的前因后果,可以点击这里👍

箭头函数( )=>{ }

箭头函数允许以更简洁的语法来声明函数,特别是在编写简单的匿名函数时非常方便

  • 语法: (参数列表) => { 代码主体 }

    如果函数没有参数,你可以简单地使用空括号 () 表示

    如果函数只有一个参数,你可以省略参数周围的括号 x=>{ },多个参数,需要使用括号(括起来) (x,y)=>{ }

    如果函数体只有一条返回语句,你可以将函数体和返回语句合并到一行,省略大括号和 return 关键字:(a,b) => a * b;

无法作为构造函数: 箭头函数不能用于构造函数,创建对象实例,它没有自己的 prototype

没有 arguments 对象: 箭头函数也没有自己的 arguments 对象,但是可以使用传递给箭头函数的参数

箭头函数不会改变this的上下文: 箭头函数的一个重要特性是继承外部作用域的 this 值,这个特性对于某些情况反而有好处:

  • 这个特性使得在使用函数作为回调函数或者在嵌套函数内部时,不需要使用额外的方法来绑定 this,从而减少了代码的复杂性.
  • 关于回调函数... 后面会更新,没更新👟踢我....

基本使用:

 //定义原始函数、箭头函数
 function originalFun(){
     console.log("定义普通函数");
 }
 const arrowsFun = ()=>{ console.log("定义箭头函数"); }
 originalFun();
 arrowsFun();
 ​
 /////////////////////////////////////////////////////////////////////////////箭头函数高级使用:
 //只有一个形参的时候,可以省略小括号
 const arrowsParamFun = x =>{ return x }
 console.log("箭头函数返回值:"+arrowsParamFun("one"));
 ​
 //只有一行代码的时候,可以省略 return
 //只有一行代码的时候,我们可以省略大括号
 const arrowsParamFun2 = x =>  x;
 console.log("箭头函数返回值:"+arrowsParamFun2("one"));

Foreach数组遍历:

ForEach(fun): 是 JavaScript 中的一个数组方法,用于遍历数组的每个元素,且参数是一个回调函数

 /////////////////////////////////////////////////////////////////////////////Foreach遍历数组对象:
 //forEach() 是 JavaScript 中的一个数组方法,用于遍历数组的每个元素,并且可以使用箭头函数:
 let arrs = ['one', 'two', 'tree'];
 //普通函数使用
 arrs.forEach(function (item, index) {
     //执行过程中可以修改原数组
     arrs[index] = arrs[index] + "X";
     console.log(`第${index + 1}元素:${item}`);
 });
 //箭头函数使用
 arrs.forEach((item, index) => console.log(`第${index + 1}元素:${item}`));

回调函数接收两个参数:item 表示当前元素的值,index 表示当前元素的索引

  • forEach() 不会对空数组执行回调函数,可以在回调函数遍历过程中进行修改原数组操作

箭头函数可以直接返回一个对象⭐

 //这种写法对于刚接触的朋友会很炸裂,慢慢接受一下就好了🆗
 //我们都知道函数有返回值 return {可以是一个对象}👇👇👇
 ​
 const ObjFun = ()=>{ return {a:123}; };         //该函数返回一个对象,而 ()=>{} 仅有一行代码可以省略 return {  }
 const ObjFun2 = ()=> {a:123};
 ​
 console.log(ObjFun());
 console.log(ObjFun2());                         //返回undefined 因为: {a:123} JS无法判断{}是函数代码块|对象的作用域,所以无法判断为对象返回; 
 ​
 //但,难道: ()=>{ return {对象} } 就只能这样写了吗? 🚫还可以升级🆙
 const ObjFun3 = ()=> ({a:123});
 console.log(ObjFun3());                         //使用({对象})标识对象的作用域进行返回完成🎉
 ​
 //Demo: 箭头函数返回一个对象,name需要作为参数传递(最简化)
 //方案一:
 const objname = (pname)=> ({name:pname});
 console.log(objname("张三"));                   //🎉完成,箭头函数简介大大提高了开发效率!但还可以升级;
 ​
 //方案二: 结合上面学习的 `字面量对象`:同名参数对象之间引用,如果参数名和对象名相同则👇👇👇
 const objname2 = (name)=> ({name});
 console.log(objname2("张三三"));                  //太强了👍❗
 { a: 123 }
 undefined
 { a: 123 }
 { name: '张三' }
 { name: '张三三' }

没有 arguments 动态参数

箭头函数中没有 arguments 动态参数,但是可以通过 ... 动态获取实参,实际使用中:...需要放在最后,方便JS判断

 //使用箭头函数 ... 动态传递参数,并使用;
 const agrufun = (...params)=>{
     console.log();
     process.stdout.write("传入参数"+params.length+"个,分别是:");
     params.forEach(element => process.stdout.write(element+" ") );      //箭头函数通常作为回调函数使用;
 }
 ​
 agrufun();
 agrufun("123");
 agrufun("1","2","3");
  • process.stdout.write("node环境下输出内容不换行")

箭头函数不会改变this的上下文

箭头函数不会创建自己的this,它只会从自己的作用域链的上一层沿用this

在箭头函数出现之前,每一个新函数根据它是被如何调用而决定这个函数的this

 //在箭头函数出现之前,每一个新函数根据它是被如何调用的来定义这个函数的this值
 name = "全局变量";
 const funw = function(){ console.log(this.name); }
 const objw = { name:"objw变量",funw }
 ​
 funw();         //输出: 全局变量
 objw.funw();    //输出: objw变量        this.name指向引用对象objw;

箭头函数并不是为了取代【普通函数】而出现:

箭头函数更适用于作为:匿名函数回调函数,解决回调this默认win指向 那么什么是回调函数呢?

简单介绍:回调函数()

回调函数是一种函数,它作为参数传递给另一个函数

并在后者执行完成或达到特定条件时被调用执行,这允许我们在需要的时候执行一些操作,而不必等待同步代码块的执行完毕

大致语法:

  //假设存在:函数x、函数y|x是y的回调函数
  //就是:将函数x,作为参数传递函数y
  y(x,...){ 
    //执行函数,
    //在特定的位置执行x() 函数;
  }
  //如果x函数需要参数,可以定义在...也在y()函数中一起传递过来;

demo测试:

 //定义X函数: 用于加法运算;
 function x(a,b){
     console.log(a+b);
 }
 //定义y函数: 作为计算器验证判断参数是否是数值;
 function y(callback,a,b){
     console.log("y函数的代码: 判断a,b是否是数值...");
     callback(a,b);
 }
 ​
 x(1,1);             //普通调用x 但不安全;
 y(x,1,1);           //为了保证参数是数值,函数y进行验证正常callback回调x函数进行计算;
  • 上述,就是一个回调函数从小Deom,实际开发中也会存在更多更复杂的 callback回调函数
  • 当然JS本身也有很多默认的带回调函数的方法: setTimeoutsetInterval

回调函数存在丢失this:

首先我们得知道js中的引用类型传值是按引用传值的⚠️

 //定义全局属性|方法;
 a = "global属性";
 function callback(){ console.log(this.a) }
 ​
 //定义对象: 对象内部引用全局的方法;
 var obj = { a:"obj属性",callback:callback };
 //调用方法:
 obj.callback();               //obj属性
 callback();                   //global属性,以上还算正常;
 ​
 /**正片开始
  * 定义函数: 参数是一个回调函数; 
  */
 function func(callback){
     callback();
 }
 //调用函数传递回调函数是对象.函数 obj.callback
 func(obj.callback);          //global属性

why?为什么明明参数是 obj.callback 调用函数而 this指向的却是全局的???

  • 因为: 引用类型作为参数传递,传递的是地址 callback函数在 func函数内部,执行是没有任何对象引用的

    所以: 15行 callback 就相当于普通的一个函数调用,而执行时候并没有任何修饰,因此默认this就是全局的对象了

    JavaScript 进阶 ES6

  • 你去药店买药,维生素2块钱一瓶维生素药片,和保健品200块钱装着维生素药片

    func 内部实参函数的调用,并没有任何 对象.函数 进行调用

    fun(obj.callback)fun(callback) 本质上其实就是一个东西(同一个地址),前者并不会把obj带过去,仅仅是包装好看,引用的都是一个地址(药片)

箭头函数不会改变this上下文:

经过上述的分析,我们知道:函数作为回调函数调用

引用类型作为函数参数传递的是地址,函数内部的直接调用相当于没有任何对象引用,所以this本质还是全局对象

很多时候我们希望,回调函数可以使用函数内部的变量…. 🔜所以: 箭头函数,继承外部作用域的 this 外部作用域:{ 当前箭头所处的大括号 }

 <script>
     /**箭头函数不会创建自己的this它只会从自己的作用域链的上一层沿用this */
     //在箭头函数出现之前,每一个新函数根据它是被如何调用的来定义这个函数的this值
     name = "全局变量";
     const funw = function(){ console.log(this.name); }
     const objw = { name:"objw变量",funw }
 ​
     funw();         //全局变量
     objw.funw();    //objw变量: this指向引用的对象;
 ​
 //////////////////////////////////////////////////////////////////////////////////////
     //箭头函数不会创建自己的this,它只会从自己的作用域链的上一层沿用this
     JTname = "{全局箭头属性}"
     const fn = () => console.log(this.JTname);
     fn()
     
     {
         //作用域中调用箭头函数: 箭头函数优先使用作用域中的变量
         JTname = "{作用域箭头属性}";
         fn();
     }
 </script> 
 全局变量
 objw变量
 {全局箭头属性}
 {作用域箭头属性}

Node | 浏览器环境中的this:

🆗,上述代码我们测试了解了箭头函数中的,This属于函数所调用的作用域,但是Node环境中的this 还有不同: 箭头函数返回 undefined 因为:

  • 全局环境:如果一个函数在全局环境运行,this 就指向顶层对象global(在浏览器中为 window 对象)

    Node.js 中,通过 global 可以获取全局对象

    在严格模式和模块环境下,this 会返回 undefined

    在松散模式下,可以在函数中返回 this 来获取全局对象

    使用 node 命令执行文件时,默认会把文件当成一个模块,并将 this 修改成 {} this = module.exports = exports

  • 对象方法:如果一个函数作为某个对象的方法运行,this 就指向那个对象本身

  • 构造函数:如果一个函数作为构造函数,this 指向它的实例对象

Node环境中变量如何定义在全局global

  • 只有特别重要的变量才能声明到global,这是为了防止变量污染
  • 语法:global.变量名 = 变量值

⚠⚠⚠ 个人对node 学习使用并不多,分析不到位地方请指点学习

ES6 新集合👆

ES6 的集合是一种新的数据结构,它类似于数组,但是每个元素的值都是唯一的,没有重复的值简单介绍一下:

Set 集合

ES6 提供了新的数据结构 Set,它类似于数组,但是成员的值都是唯一的,没有重复的值

Set集合初始化:

  • new Set(); 空参构造器
  • new Set([数组对象]); Set可以接受一个*【数组】或 具有 iterable 接口的其他数据结构*,作为参数,并利用唯一性实现数组去重

Set 实例的属性和方法:

  • Set.prototype.size:返回Set实例的成员总数
  • Set.prototype.constructor:构造函数,默认就是Set函数

  • Set.prototype.clear():清除所有成员,没有返回值
  • Set.prototype.add(value):添加某个值,返回 Set 结构本身
  • Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员
  • Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功

  • Set.prototype.keys():返回键名的遍历器
  • Set.prototype.values():返回键值的遍历器
  • Set.prototype.entries():返回键值对的遍历器
  • Set.prototype.forEach():使用回调函数遍历每个成员,和Java Set无序不同,Set的遍历顺序就是插入顺序
 /** Set集合 
  *  Set集合类似于数组,但是成员的值都是唯一的 */
 {
     let objs = new Set();
     let nums = [1, 2, 3, 3, 2, 4];
     nums.forEach(o => objs.add(o));     //add()方法向 Set 结构加入成员
 ​
     console.log(objs);                  //Set(4) { 1, 2, 3, 4 }: 结果表明 Set 结构不会添加重复的值
     console.log("集合元素个数:" + objs.size);
 }
 ​
 /** Set常用方法: 
  *  Set可以接受一个【数组】或 具有 iterable 接口的其他数据结构作为参数 */
 {
     let objs = new Set([1, 1, 2, 3]);
 ​
     objs.clear();                   //清空Set集合元素 Set(0) {}  
     console.log(objs);
     console.log(objs.add(1));       //Set(1) { 1 }
     console.log(objs.add('1'));     //Set(2) { 1, '1' } 1和'1' 是两个不同的值
                                     //Set内部判断算法叫做“Same-value-zero equality” 类似于 === 恒等判断; 
     console.log(objs.has(1));       //true  元素存在
     console.log(objs.has(2));       //false 元素不存在
     console.log(objs.delete(1));    //true  删除成功
     console.log(objs.delete(2));    //false 元素不存在删除失败
 }
 ​
 /** Set的遍历: */
 {
     let objs = new Set([1, 1, 2, 3]);
     //由于 Set 结构没有键名,只有键值,所以keys方法和values方法的行为完全一致
     for (let item of objs.keys()) { console.log(item); }
     for (let item of objs.values()) { console.log(item); }
     //entries方法返回的遍历器同时包括键名和键值,每次输出一个数组,它的两个成员完全相等
     for (let item of objs.entries()) { console.log(item); }
     //Set 结构的实例与数组一样,也拥有forEach方法,用于对每个成员执行某种操作,没有返回值
     objs.forEach((value, key) => console.log(key + ' : ' + value))
 }

Set的使用小技巧|扩展: 主要利用Set唯一性,实现数组对象、等元素去重的操作

 /** Set使用小技巧: */
 {
     //通过结构赋值+Set 快速数组去重
     let nums = [1, 1, 2, 2, 3];
     nums = [...new Set(nums)];
     console.log(nums);          //[ 1, 2, 3 ]
 ​
     //Array.from(Set): 快速将Set数据结构转换成数组
     let items = Array.from(new Set([1, 1, 2, 2, 3]));
     console.log(items);         //[ 1, 2, 3 ]
 ​
     //快速去除字符串重复字符 并通过 [数组].join('') 拼接数组元素
     let strs = [...new Set('aABBBCC')].join('');
     console.log(strs);          //aABC
 ​
     //因为Set的唯一去重的特性可以快速实现: 并集(Union)、交集(Intersect)和差集(Difference)
     let a = new Set([1, 2, 3]);
     let b = new Set([4, 3, 2]);
 ​
     // 并集
     let union = new Set([...a, ...b]);
     // Set {1, 2, 3, 4}
 ​
     // 交集
     let intersect = new Set([...a].filter(x => b.has(x)));
     // set {2, 3}
 ​
     // (a 相对于 b 的)差集
     let difference = new Set([...a].filter(x => !b.has(x)));
     // Set {1}
 }

WeakSet

Weak (中译: 弱): WeakSet 结构与 Set 类似,也是不重复的值的集合,但与 Set 有两个区别:

  • WeakSet 的成员只能是对象和 Symbol 值,而不能是其他类型的值

  • WeakSet 中的对象都是弱引用,垃圾回收机制不考虑 WeakSet 对该对象的引用,

    根据可达性分享算法,如果没有任何对象引用该对象,则垃圾回收机制会自动回收该对象所占用的内存

由于上面这个特点,WeakSet 的成员是不适合引用的,因为它会随时消失

由于WeakSet有多少个成员,取决于垃圾回收是否运行,运行前后可能元素个数不一样,而垃圾回收机制不可预测的,因此ES6规定WeakSet不可遍历

Map 集合

JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键

ES6 提供了 Map 数据结构: 它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键

Map 集合初始化:

  • new Map(); 空参构造器
  • new Map([数组对象]); Map可以接受一个数组作为参数,该数组的成员是一个个表示键值对的数组

Map 实例的属性和方法:

  • Map.prototype.size:返回Set实例的成员总数

  • Map.prototype.set(key, value):方法设置键名key对应的键值为value,如果key已经有值,则键值会被更新,否则就新生成该键
  • Map.prototype.get(key):方法读取key对应的键值,如果找不到key,返回undefined
  • Map.prototype.has(key):方法返回一个布尔值,表示某个键是否在当前 Map 对象之中
  • Map.prototype.delete(key):方法删除某个键,返回true,失败返回false
  • Map.prototype.clear()clear()方法清除所有成员,没有返回值

  • Map.prototype.keys():返回键名的遍历器
  • Map.prototype.values():返回键值的遍历器
  • Map.prototype.forEach():遍历 Map 的所有成员
  • Map.prototype.entries():返回所有成员的遍历器
 //Map的使用:
 {
     let objm = new Map();
     //set().set(支持链式编程添加元素) K,V可以是任何数据结构
     objm.set("name", "wsm").set("age", 18);
     console.log(objm.get("name"));      //wsm
     console.log(objm.has("name"));      //true
     console.log(objm.size);             //2
     console.log(objm);                  //Map(2) { 'name' => 'wsm', 'age' => 18 }
 ​
     //删除指定key的元素,没有key则返回false
     console.log(objm.delete("name2"));   //false
     console.log(objm.delete("name"));    //true    
     console.log(objm.size);              //1
 ​
     objm.clear();                        //清空所有元素
     console.log(objm);                   //Map(0) {}
     
     /** Map可以使用数组(或任何具有 Iterator 接口且每个成员都是一个双元素的数组的数据结构) 作为参数 */
     const map1 = new Map([
         ['name', '张三'],
         ['title', 'Author']
     ]);
     console.log(map1);
 ​
     const set = new Set([
         ['foo', 1],
         ['bar', 2]
     ]);
     const m1 = new Map(set);
     const m2 = new Map(m1);
     console.log(m1);        //Map(2) { 'foo' => 1, 'bar' => 2 }
     console.log(m2);        //Map(2) { 'foo' => 1, 'bar' => 2 } 
 }
 ​
 //Map的循环遍历:
 {
     const map = new Map([['name', 'wsm'], ['age', 18],]);
     //返回键名的遍历器
     for (let key of map.keys()) { console.log(key); }
     //返回键值的遍历器
     for (let value of map.values()) { console.log(value); }
     //遍历 Map 的所有成员
     for (let [key, value] of map) { console.log(key, value); }
 ​
     //返回所有成员的遍历器 可以设置数组格式|[k,v] 格式
     for (let item of map.entries()) { console.log(item[0], item[1]); }
     for (let [key, value] of map.entries()) { console.log(key, value); }
 }

Map的使用小技巧|扩展:

 //Map小技巧:数据类型之间的转换
 {
     //Map转换数组
     const myMap = new Map().set('key', 'value').set({ 'key': 'key对象结构' }, 'value');
     console.log([...myMap]);                //[ [ 'key', 'value' ], [ { key: 'key对象结构' }, 'value' ] ]
 ​
     //Map转换对象
     //如果Map 所有的键都是字符串,它可以无损地转为对象
     //如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名
     const objmap = new Map().set('name', 'wsm').set('age', 18);
     function strMapToObj(strMap) {          //通过定义一个函数来实现;
         let obj = Object.create(null);
         for (let [k, v] of strMap) { obj[k] = v; }
         return obj;
     }
     console.log(strMapToObj(objmap));       //[Object: null prototype] { name: 'wsm', age: 18 }
 ​
     //对象转换Map直接通过Object.entries()
     let obj = { "a": 1, "b": 2 };
     console.log(new Map(Object.entries(obj)));
 ​
     //Map 转为 JSON
     //Map 的键名都是字符串,这时可以选择转为对象 JSON
     function strMapToJson(strMap) { return JSON.stringify(strMapToObj(strMap)); }   //使用递归遍历JSON.stringify();
     let JsonMap = new Map().set('yes', true).set('no', false);
     console.log(strMapToJson(JsonMap));
 ​
     //Map 的键名有非字符串,这时可以选择转为数组 JSON
     let arrayMap = new Map().set('key', 'value').set({ 'key': 'key对象结构' }, 'value');
     function mapToArrayJson(map) { return JSON.stringify([...map]); }
     console.log(mapToArrayJson(arrayMap));
 ​
     //JSON 转为 Map
     //JSON 转为 Map,正常情况下,所有键名都是字符串
     function objToStrMap(obj) {     //定义objToStrMap将对象转换为 Map 的函数
         let strMap = new Map();
         for (let k of Object.keys(obj)) { strMap.set(k, obj[k]); }
         return strMap;
     }
     function jsonToStrMap(jsonStr) { return objToStrMap(JSON.parse(jsonStr)); }
     console.log(jsonToStrMap('{"yes": true, "no": false}'));
 ​
     //特殊情况: 整个JSON就是一个数组,且每个数组成员本身,又是一个有两个成员的数组
     function jsonToMap(jsonStr) { return new Map(JSON.parse(jsonStr)); }
     console.log(jsonToMap('[[true,7],[{"foo":3},["abc"]]]'));
 }

WeakMap

WeakMap 和 Map的区别:

  • WeakMap的 Key 只能是对象和 Symbol 值
  • 没有遍历操作(即没有keys()values()entries()方法),也没有size属性
  • 无法清空,即不支持clear方法,WeakMap只有四个方法可用:get()set()has()delete()

因为弱引用,垃圾回收机制无法判断key是否存在,为了防止这种不确定性,则Weak没有便利、size、clear的操作

WeakRef 对象的弱引用

WeakSet 和 WeakMap 是基于弱引用的数据结构,例子很难演示,因为无法观察它里面的引用会自动消失,介绍 WeakRef

ES2021 提供了 WeakRef对象,用于直接创建对象的弱引用:

  • WeakSet 的存储的值都是弱引用,
  • WeakMap 是存储的 K 是弱引用,存入的值不会影响运行环境的垃圾回收
 /** WeakRef弱引用的引用场景: 解决内存泄漏 */
 //对象关联内存泄漏问题:
 {   //node --expose-gc   
     //global.gc();
     //假设有一个a b对象: a和b有关系,需要通过a找到b则:
     var a = {};
     var b = { 'obj':new Array(1000) };
     a.ab = b;       //直接讲b的引用到a的属性ab中
     //如果b对象使用完毕回收 b=null
     //因为b还被a.ab引用所以 b值虽然为null但是内存空间仍然无法回收;
     process.memoryUsage();      //查看环境内存使用情况
     b = null;                   //仅仅是将b变量引用的地址置空,但堆空间依然存在
     global.gc();                //手动调用垃圾回收
     console.log(a.ab);          //因为可达性分析算法堆依然有内存指向所以 Array(1000) 并不会被回收;
     process.memoryUsage();
 }
 ​
 //使用MeakMap解决内存泄漏
 {   //node --expose-gc
     //global.gc();
     //假设有一个a b对象: a和b有关系,需要通过a找到b则:
     var a = {};
     var b = { 'obj':new Array(1000) };
     a.ab = new WeakRef(b);      //a.ab 执行的一个WeakRef弱引用对象;            
     b = null;                   //b空引用                    
     global.gc();                //手动垃圾回收
     a.ab.deref();               //垃圾回收后值undefined
 }
  • WeakRef 实例对象有一个deref()方法,如果原始对象存在,该方法返回原始对象;
  • 如果原始对象已经被垃圾回收机制清除,该方法返回undefined

因为node 环境无法直接手动回收内存,需要通过:node --expose-gc 开启

关于内存和垃圾回收机制,不同的运行环境可能会有不同的效果,Demo案例不方便展示,还需要多多学习📕

JavaScript 进阶 ES6

JavaScript Class

Class类的概念: 在很多的高级编程语言中:C++、Java... 都有类的概念,类是对象的抽象,对象是类的实例

本质来说或,类是用于创建对象而存在的概念性语法… ES6之前JavaScirpt 也可以通过函数式来创建对象

  • 😶‍🌫️实在得吐槽一下,JavaScript早期创建对象的发展过程:声明式——函数式——原型链式...
  • 😵对于初学者的学习真的很晕,如果有人想要了解:请点击🔗

Class的基本使用

个人觉得JavaScript 早期发展非常冗杂 ES6之后开始从弱语言—慢慢发展—更加完善健壮💪

ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为创建对象的模板通过class关键字,可以定义类

  • ES6 的class可以看作只是一个语法糖🍬,它的绝大部分功能,ES5 都可以做到
  • 新的写法只是让对象原型的写法更加清晰、更像面向对象编程的语法:

ES5|ES6⬆️之前,通过定义构造函数方式创建对象: 这里简单介绍一下,详情🖱️🔗

  /** ES5|ES6⬆️之前,通过定义构造函数方式创建对象: 
   *  1.创建一个构造函数定义类的属性
   *  2.函数名.prototype定义类原型中类的方法
   *  3.new 构造函数(...)创建构造函数返回的类实例 */
  function Point(x, y) {
      this.x = x;
      this.y = y;
  }
  
  Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
  };
    
  var p = new Point(1, 2);
  console.log(p);                             //Point { x: 1, y: 2 }

ES6新增Class关键字创建对象:

  /** ES6新增Class关键字创建对象:
   *  1.使用Class关键字定义类
   *  2.constructor中this.属性定义类属性
   *  3.Class{}中直接 函数名(){ 定义类中的方法 } 
   *    函数不需要加function 开头,方法与方法之间不需要','逗号分隔加了会报错 */
  class Point {
      constructor(x, y) {
          this.x = x;
          this.y = y;
      }
  
      toString() { 
          return '(' + this.x + ', ' + this.y + ')'; 
      }
  }
  //  4.与ES6之前一样也是通过 new 关键字创建类的实例对象
  var p = new Point(1, 2);
  console.log(p);                             //Point { x: 1, y: 2 }

上述两种写法,最终结果完全一模一样: Class 本质是对构造函数的一层包装

  • Class定义的类名与构造函数的函数名有很多共同的属性:name、length

  • Class下定义的类与构造函数一样都具有原型链的特征:Prototype、__proto__、constructor

      /** Class 本质是对构造函数的一层包装 
      *  Class类名 与 构造函数名有相同的属性,有原型链的特性 
      *  因为有原型链的存在所以也可以像以前一样通过原型链定义方法,但不建议 */
      console.log(Point.prototype.constructor);                      //[class Point]
      console.log(Point.prototype === p.__proto__);                  //true
      console.log(Point.prototype.constructor === Point);            //true
       
      console.log(".name属性返回构造函数名"+Point.name);                //Point
      console.log(".length属性返回构造函数形参个数"+Point.length);       //2 对应类两个属性
    

当然,Class是封装构造函数,最终还是和构造函数有所不同🈲:

  • Class类,实例化对象必须通过 new 类名() 形式

    因为,ES6之前构造函数本身就是函数所以可以直接调用

  • 构造函数定义的函数可以枚举,而 Class类中定义的函数不可枚举便利

    类函数定义时候底层默认设置了:enumerable属性都是false,

    就是为了避免使用for…in |Object.keys() 把类的方法也遍历出来

    可以使用 Object.defineProperty() 方法,手动设置enumerable 属性为true

      /** Class最终还是和构造函数有所不同
      *  实例化对象必须通过 new 类名()
      *  Class类定义的函数不可枚举便利 */
      // let w =  Point(5,4);       报错: cannot be invoked without 'new'
      let w = new Point(5,4);
      for (const key in w) { console.log(key); }      //只有x、y
    

Class表达式

Class的一种语法糖🍬 有一点像Java语言中的 匿名类 好处是:快速定义,用完快速回收并不会占用太久内存 了解即可混个眼熟🤔

  {
      //方式一:类赋值myClass对于不常用的类 myClass=null 则可以快速回收my的类资源;
      const myClass = class my{  
          //省略构造器...
          fun(){ console.log("my只在Class{ 可以使用,代指当前类对象 }: "+my.name); }
       }
  
      // let myC = new my();      报错,对外展示的类名是对象名
      let myC = new myClass();
      myC.fun();
  
  
      //方式二: 匿名类,快速定义快速回收不会占用内存
      const myClass2 = new class { 
          constructor(name){ this.name=name }
          fun(){ console.log(this.name+"是一个理解执行的类,直接返回一个类对象"); } 
      }("wsm");
      myClass2.fun();
  }

Class注意事项

ES6 把整个语言升级到了严格模式

类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式,严格模式规范了之前横冲直撞的代码😠

JS中的严格模式是一种在严格条件下运行JS代码的方式,ES6之前在所有语句之前添加 use strict 开启

消除一些不合理、不严谨或不安全的语法和行为,提高编译器效率,增加运行速度

  • 不允许使用八进制字面量或转义字符
  • 不允许使用未声明的变量,否则会抛出错误
  • 不允许删除不可删除的属性,否则会抛出错误
  • 不允许使用eval、arguments、this等保留关键字作为变量名
  • 不允许给不可写、只读或不可扩展的属性赋值,否则会抛出错误
  • 不允许this关键字指向全局对象,限制了eval和arguments的使用
  • 不允许对象或函数中出现重复的属性名或参数名,否则会抛出错误

constructor构造器

constructor()方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法

  • constructor()方法默认返回实例对象,即this 也可以修改…
  • 一个类必须有一个constructor()方法,如果没有显式定义,则程序默认添加一个空的constructor()方法
  /** constructor构造器 */
  class MyClass{
      //无论是否手动定义,JavaScript引擎会默认添加一个空参构造器
      constructor(){
          console.log("执行构造器");
          return ["修改构造器返回对象实例this"]; 
      }    
  }
  let myc =  new MyClass();
  console.log(myc);                  //[ '修改构造器返回对象实例this' ]
  console.log(typeof myc);           //object类型
  console.log(myc instanceof Array); //true

JS修改构造器 return 取决于你返回的是什么类型的值

如果你返回的是一个基本类型的值,比如字符串、数字、布尔值等,那么这个值会被忽略,构造器会返回新创建的对象

如果你返回的是一个引用类型的值,比如对象、数组、函数等,那么这个值会替代新创建的对象,构造器会返回这个引用类型的值

Class、constructro 中的this

正常情况下,Class 和 constructro this 指向的都是类的实例对象

Class{ 中通过箭头函数定义 } 箭头函数this指向函数所在的{作用域} Class的作用域就是这个类实例对象所以不受影响

当然也存在特殊情况:

  • Class{ 中定义的函数 } 通过解构赋值引用地址到外面调用~
  • 函数调用时候,使用 bind... 等修改this的指向
  /** Class和constructor中的this */
  {
      class MyClass{
          //无论是否手动定义,JavaScript引擎会默认添加一个空参构造器
          constructor(name,age){
              this.name = name;
              this.age = age;
          }
          wb(){ console.log(this) }
          show(){ console.log(`我叫${this.name},今年${this.age}`); }
          showJT = ()=>{  console.log(`我叫${this.name},今年${this.age}`); }
      }
  
      let myc = new MyClass('wsm',19);
      myc.wb();
      myc.show();
      myc.showJT();
  
      //通过对象解构赋值 myc对象的show 方法解构出来在外面执行
      let {wb} = myc
      wb();               //undefined: 因为现在wb已经相当于没有任何引用的一个普通函数执行了
  }

由于ES6开始默认严格模式,所以这种情况大部分会报错,这里也只是为了了解展示案例

Class 属性|方法定义

ES新特性支持 除了constructor构造器中定义属性还可以在:类的顶层定义属性、通过表达式定义属性|函数名;

  • 优点: 所有实例对象自身的属性都定义在类的头部,看上去比较整齐,一眼就能看出这个类有哪些实例属性,一般用于声明具有初始|默认值的属性
 /** Class中的属性: 
 *   除了constructor构造器中定义属性ES新特性:还可以在类的顶层定义属性、属性名表达式定义 */
 {
     let word = "word";
     class MyClass {
         xname = 'w';
         ["hello" + word] = "初始化";
         constructor(name, age) {
             this.name = name;
             this.age = age;
         }
         //函数名也可以使用 [表达式声明]
     }
 ​
     let myc = new MyClass("wsm", 18);
     console.log(myc);               // MyClass { xname: 'w', helloword: '初始化', name: 'wsm', age: 18 }
 }

Class 私有属性|方法

私有属性和私有方法是指只能在类内部访问的属性和方法: JavaScript的私有|属性|方法|有多种声明方式:

早期JS私有属性|方法

  • 一、通过规则规定属性|方法名来实现规则私有,外部依然可以通过变量进行访问
  • 二、通过Symbol的唯一性,不可枚举性,实现伪私有化: 符合私有的定义,关于Symbol👇
 /** 早期JS私有属性|方法 */
 {
     class MyClass {
         _xname = '_私有属性';
         [Symbol] = "Symbol私有属性";
         constructor(name, age) {
             this.name = name;
             this.age = age;
         }
 ​
         _show() { return "_私有方法"; }
         [Symbol]() { console.log("Symbol私有方法"); }
     }
 ​
     let myc = new MyClass("wsm", 18);
     console.log(myc);
     console.log(myc._xname);
     console.log(myc._show());
     //Symbol 属性不具有枚举性且值唯一,类的外部并不能直接获取Symbol值...
 }
  • _私有化: 是一个规则私有化,通过改变属性|方法名实现伪私有化
  • Symbol私有化: 不具有枚举性且值唯一,类的外部并不能直接获取Symbol值:建议事先Class中定义好Symbol值方便类的内部调用私有属性|方法

ES6的#私有属性|方法

ES2022 正式为class添加了私有属性,方法是在属性名之前使用#表示

 /** ES6的#私有属性|方法 */
 {
     class MyClass {
         #xname = '#私有属性';
         #show() { return "_私有方法"; }
         pubShow() {
             console.log(this.#xname);
             return this.#show();
         }
     }
     let myc = new MyClass();
     console.log(myc);
     myc.pubShow();
     // console.log(myc.#xname);        //编辑器报错: 没有找到对应属性|方法
     // console.log(myc.#show());           
 }
  • 注意,从 Chrome 111 开始,开发者工具里面可以读写私有属性,不会报错,原因是 Chrome 团队认为这样方便调试

getter|setter方法:

私有属性和私有方法是指只能在类内部访问的属性和方法: 但是很多时候我们想要保证属性安全的同时依然想操作Class的属性;

与ES5 一样,在“类”的内部可以使用getset关键字 ,对Class某个属性设置存值函数和取值函数,拦截该属性的存取行为,可以在属性存取时添加额外操作;

  • get: 函数名无所谓,建议getXxx开头方便区分,内部 return 私用|共有属性 调用: 对象.getXxx 获取类的属性
  • set: 函数名无所谓,建议setXxx开头方便区分,内部 this.xxx = 参数赋值 调用: 对象.setXxx = value 对类的属性重新赋值
 /** ES6的#私有属性|方法: getter\setter */
 {
     class MyClass {
         #name = '#私有属性';
         get getName() { return this.#name }
         set setName(value) { this.#name = value }
     }
     let myc = new MyClass();
     console.log(myc);           //MyClass {}
     console.log(myc.getName);   //#私有属性
     myc.setName = "#set修改私有属性"; console.log(myc.getName); //#set修改私有属性
 }

Class 静态属性|方法

学习过Java语言应该不会陌生:Static

  • 类相当于实例的原型,所有在类中定义的方法,都会被实例继承:
  • 如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法|属性”
 {
     class MyClass {
         static sname = 'static属性';
         static show() {
             //"静态方法中的this指向的是MyClass: this===MyClass: "
             console.log(this === MyClass);
             return "static 方法";
         }
     }
     //静态属性属于类 不需要实例化对象直接通过 类名.属性|方法名
     console.log(MyClass);               //[class MyClass] { sname: 'static属性' }
     console.log(MyClass.sname);         //static属性
     console.log(MyClass.show());        //true static 方法
 }
  • 在 属性|方法声明前加上 static关键字: 就是静态的属性|方法,在Class加载最先执行

  • static 属性|方法,是属于类的属性|方法,且,会被子类继承

    类本身存在的 name 就是继承而来的静态属性, Class 内部重新定义 static name静态属性会覆盖修改原先的值

静态代码块:static { }

静态属性的一个问题是,如果它有初始化逻辑:这两种方法都不是很理想,前者是将类的内部逻辑写到了外部,后者则是每次新建实例都会运行一次

ES2022 引入了静态块(static block),允许在类的内部设置一个代码块,在类生成时运行且只运行一次,主要作用是对静态属性进行初始化

  • 一个类中允许有多个静态块,每个静态块中只能访问之前声明的静态属性
  • 静态块内部可以使用类名或this(指代当前类)
  • 静态块的内部不能有return语句
 /** 静态代码块 static { } 需要定义在类的内部 */
 {
     class MyClass {
         //...省略其他代码...
         static sname = 'static属性';
         static {
             try {
                 console.log("static{ } 在类加载时最先且只执行一次,可以在程序加载时候动态设置初始值");
                 this.sname = "static{初始值}"
             } catch {
                 console.log("static 初始化失败");
             }
         }
     }
     console.log(MyClass.sname);     // ... static{初始值}
 }

Class 类的继承

面向对象三大特性之一:继承 子类 继承 父类的属性方法,使子类具有父类的特征,实现代码复用且更符合现实

  • 语法: class A { ... }calss B extends A { ... } 通过extends关键字实现继承
  • Class 可以通过extends关键字实现继承,子类继承父类的:属性|方法静态属性|方法 但,不能继承私有属性|方法
 /** 类的继承extends 
  *  Class 可以通过`extends`关键字实现继承:子类继承父类的属性|方法、静态属性方法 但 不能继承私有属性|方法 */
 {
     //父类
     class A {
         #prva = '私有属性A'
         static sta = '静态属性A';
         constructor(a) { this.a = a; }
 ​
         #prvfun() { return "私有方法A" }
         static stfun() { return "静态方法A" }
         publicfunction() { return "普通共有方法A" }
         publicfunctionS() { return "普通共有方法AA" }
     }
 ​
     //子类:子类除了可以继承父类的属性方法....还可以有自己的属性方法\\
     class B extends A {
         constructor(a, b) {
             //子类构造函数中只有调用super()之后,才可以使用this关键字,否则会报错
             //子类实例的构建必须先完成父类的继承,只有super()方法才能让子类实例继承父类
             super(a);
             this.b = b;
         }
         publicfunctionS() { return "普通共有方法BB" }
     }
 ​
     let objB = new B('a', 'b');
     console.log(B);             //子类继承了父类的static静态属性方法
     console.log(B.sta);
     console.log(B.stfun());
 ​
     console.log(objB.a);        //继承了父类的属性|方法,对于同名方法子类重写的父类的方法
     console.log(objB.b);
     console.log(objB.publicfunction());
     console.log(objB.publicfunctionS());
 }
 [class B extends A]
 静态属性A
 静态方法A
 ​
 a
 b
 普通共有方法A
 普通共有方法BB

super() 函数

ES6 的继承必须先调用super()方法,因为这一步会生成一个继承父类的this对象,没有这一步就无法继承父类

 /** super() 函数使用: */
 {
     //父类
     class A {
         constructor(a) {
             this.a = a;
             console.log("A构造器执行");
         }
     }
 ​
     //子类:子类除了可以继承父类的属性方法....还可以有自己的属性方法\\
     class B extends A {
         constructor(a, b) {
             super(a);       //新建子类实例时,父类的构造函数必定会先运行一次
             this.b = b;
             console.log("B构造器执行");
         }
     }
 ​
     let objB = new B('a', 'b');
     //A构造器执行
     //B构造器执行
 }
  • 这意味着新建子类实例时,父类的构造函数必定会先运行一次
  • 只有调用super()之后,才可以使用this关键字,否则会报错 子类实例的构建,必须先完成父类的继承,只有super()方法才能让子类实例继承父类

super 函数中的this

super()的作用是形成子类的this对象,把父类的实例属性和方法放到这个this对象上面

子类在调用super()之前,是没有this对象的, 任何对this的操作都要放在super()的后面

 /** super虽然代表了父类的构造函数,但是内部调用和返回的是子类的this  */
 {
     //父类
     class A {
         xname = "Aclass";
         constructor() {
             console.log(new.target.name);   //new.targe 返回当前类名
             console.log(this.xname);        
         }
     }
 ​
     //子类
     class B extends A {
         xname = "Bclass";
         constructor() { super(); }
     }
     let objA = new A(); // A Aclass
     let objB = new B(); // B Aclass ?为什么返回的xname是Aclass? super中的thsi是父类?
 }
  • new.targe JavaScript 中的 new.target 是一个特殊的属性

    如果一个普通的函数调用,那么 new.target 的值就是 undefined

    如果一个构造函数或构造方法是通过 new 运算符被调用的,那么 new.target 的值就是指向这个函数或构造方法的引用:即:当前class对象

所以:上述Demo子类consturctor 调用 super 返回 B super中的this是子类

但,为什么this.xname 返回的是 Aclass,因为: super()执行时,Bxname属性还没有绑定到thisthis.xname拿到的是A类的xname属性

原型链:

学习过JS对象都知道:原型链 可以根据对象寻找它的基类——形成一个继承关系,class依然存在原型链

  • Object.getPrototypeof(obj): 根据子类返回父类对象
  • 子类的__proto__属性,表示构造函数的继承,总是指向父类
  • 子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性 …更多操作忽略…
 console.log(B.__proto__ === A);                     //true
 console.log(Object.getPrototypeOf(B) === A);        //true
 console.log(B.prototype.__proto__ === A.prototype); //true

Symbol 新数据类型

这个知识对于新手很不友好,所以不理解很正常,可以在后面工作过程中慢慢感受 我也是看了好多大佬的讲解,这里写的不好多多包含

吐槽网上很多人一堆视频、文章硬讲实在看不懂尚硅谷讲的也看不懂,稀里糊涂的建议举一点实际开发的例子…

如果实在看不懂直接跳过,影响不大,蹲一个评论区大佬


ES6 引入了一种新的原始数据类型Symbol,通常用于表示独一无二的值,它属于 JavaScript 语言的原生数据类型之一

Symbol的创建:

方式一: Symbol(); Symbol是JavaScirpt的基本数据类型,所以并不能使用new

方式二: Symbol('xxx'); 声明Symbol时候制定一个描述,但同名描述的结果并不是相同的

方式三: Symbol.for('xxx'); for不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,不存在才会新建

 //方式一
 //Symbol是JavaScirpt的基本数据类型,所以并不能使用`new`
 {
     let sy1 = Symbol();
     console.log('数据类型: ' + typeof sy1);                                         //数据类型:symbol
 }
 ​
 //方式二: 函数接受一个字符串参数,表示对Symbol实例的描述
 {
     let sy1 = Symbol('wsm');
     let sy2 = Symbol('wsm');
     console.log('Symbol(相同描述信息的值返回结果并不相同): ' + (sy1 === sy2));          //false
 }
 ​
 //方式三: 有时候为了重新使用同一个Symbol值,Symbol.for()
 //函数接受字符串参数,搜索内存堆中有没有以该参数作为名称的Symbol值,没有则创建|有则直接引用;
 {
     let sy1 = Symbol.for('wsm');
     let sy2 = Symbol.for('wsm');
     console.log('Symbol.for(搜索创建指向同一块内存空间结果): ' + (sy1 === sy2));        //true
     console.log('Symbol.keyFor返回一个已登记的 Symbol 类型值的key:' + Symbol.keyFor(sy1));    //wsm
 }
 ​
 /**注意: */
 //Symbol 值不能与其他数据进行运算会报错,但是Symbol值可以显式转为字符串
 //Symbol 值也可以转为布尔值,但是不能转为数值
 let sym = Symbol('sym');
 // console.log("Symbol拼接" + sym);                         注释报错影响运行;
 console.log("Symbol拼接" + String(sym));
 console.log("Symbol拼接" + sym.toString());
 console.log("Symbol可以转换成Boolean: " + Boolean(sym));
 // console.log("但不可以转换成Number: " + Number(sym));       注释报错影响运行;
  • Symbol 值不能与其他数据进行运算会报错,但是Symbol值可以显式转为字符串
  • Symbol 值也可以转为布尔值,但是不能转为数值

Symbol作为对象属性名:

JavaScript 属于弱语言可以随时在对象中添加新的属性

  • ES6之前的对象属性名都是字符串,这很容易造成属性名的冲突⚡
  • 如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突,于是ES6 引入Symbol
 //因为 JS可以通过 对象.属性名 形式给对象添加新属性
 //所以 当我们需要给对象新增一个临时属性|方法,很有可能新属性|方法同名覆盖了对象原有值;
 {
     let a = { name: "wsm", age: 18 }
     a.name = "误操作覆盖的值";
     console.log(a);
 }

ES6 新增的Symbol 具有唯一性,可以解决命名冲突覆盖的问题

 //使用 Symbol唯一性特点解决命名冲突
 //注意 使用Symbol作为属性名不能使用 .点运算符
 //因为 .点运算符后面总是字符串,并不会读取Symbol作为标识名所指代的那个值
 {
     let a = { name: "wsm", age: 18 }
     a[Symbol('sym')] = "[新增的属性名值0]";
     a[Symbol('sym')] = "[新增的属性名值1]";
     a[Symbol.for('sym')] = "[.for新增的属性名值0]";
     a[Symbol.for('sym')] = "[.for新增的属性名值1]";     //Symbol.for 的值是同一个内存空间所以后面的值会覆盖前面;
     console.log(a);
 }
 ​
 //方式二
 //Symbol可以在声明时候定义在对象内部但是,但是貌似就不方便访问了...
 {
     let sym = Symbol('sym');
     let obj = {
         name: 'wsm',
         sym: '.sym的值0',               //这里的sym并不是声明的Symbol类型,而是单纯的字符串;
         sym: '.sym的值1',               //对象定义相同的属性名会覆盖之前的值;
         [Symbol()]: '[symbol的值0]',
         [Symbol()]: '[symbol的值1]',
     }
     console.log(obj);
 ​
     //方式三: 
     //Object.defineProperty(对象,属性名,{配置信息}) 
     //Object 的静态方法可以直接在一个对象上定义一个新属性,通过配置设置属性值、可见...
     Object.defineProperty(obj, sym, { value: '[symbol的值2]', enumerable: true });
     console.log(obj);
     console.log("对象.xxx拼的是字符串sym: " + obj.sym);
     console.log("对象[xxx]中获取的是Symbol值: " + obj[sym]);
 }

😢 好鸡肋啊: 因为,以前学习Java 并不能直接在对象类外面——>对象添加新的属性,所以这种情况真的很蓝绷😢!

对于,Symbol我的观点一直都是没啥卵用,所以很难理解,希望路过大佬点评一下

Symbol 属性名的遍历:

需要注意⚡:

Symbol 值作为属性名,遍历对象时候,该属性不会出现在for...infor...of循环中

也不会被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回

它并不是私有属性Object.getOwnPropertySymbols()方法,可以获取指定对象的 [ 所有Symbol属性名 ]

 /** Symbol 属性名的遍历: */
 {
     let obj = {
         name: 'wsm',
         sym: '.sym的值0',
         [Symbol()]: '[symbol的值0]',
         [Symbol()]: '[symbol的值1]',
     }
     //`for...in`、`for...of`遍历对象时候 Symbol属性名不会出现在循环中
     for (const key in obj) { console.log("属性名: " + key + "\t属性值: " + obj[key]); }
 ​
     console.log("JSON.stringify 将一个 JavaScript 对象或值转换为 JSON 字符串" + JSON.stringify(obj));
     console.log("Object.keys 静态方法返回一个由给定对象自身的可枚举的字符串键属性名组成的数组:" + Object.keys(obj));
     console.log("Object.getOwnPropertyNames 静态方法返回一个自身对象所有属性包含不可枚举属性,但不包含Symbol的数组:" + Object.keys(obj));
     console.log("Object.getOwnPropertySymbols 静态方法返回一个给定对象自有所有 Symbol属性的数组" + Object.getOwnPropertySymbols(obj));
 }
  • 由于以 Symbol 值作为键名,不会被常规方法遍历得到
  • 我们可以利用这个特性,为对象定义一些 非私有的、但又希望只用于内部的方法

Symbol作为枚举固定参数使用:

枚举,不同的编程语言中好像概念都不同,这也是我晕的一个点😵 Java 中的枚举🔗

  • Java 中枚举是一种特殊的数据类型,它可以包含多个枚举常量,每个枚举常量都是该枚举类型的一个实例,通常用于表示类型、规格的概念
  • JavaScript 中,可枚举性是指对象属性是否可以被 for...in 循环遍历到,决定的是属性是否可以遍历

而,Symbol具有枚举的特性:

定义一组常量,保证这组常量的值都是不相等的

Symbol每一个符号都是唯一的,这意味着您必须始终使用枚举本身来比较枚举

还可以解决:魔法字符串的情况:在代码之中多次出现、与代码形成强耦合某一个具体字符串|数值,本身并没有意义的属性,定义成枚举解决魔法字符串

 //对于这样一组属性值不需要任何作用,但是需要属性名进行一定规范的属性可以使用Symbol当作其值确保唯一
 const LOG = {};
 LOG.LEVELS = {
     DEBUG: Symbol('debug'),
     INFO: Symbol('info'),
     WARN: Symbol('warn'),
 };
 console.log(LOG.LEVELS.DEBUG, 'debug message');
 console.log(LOG.LEVELS.INFO, 'info message');
 ​
 ​
 //Symbol每一个符号都是唯一的,这意味着您必须始终使用枚举本身来比较枚举
 function getError(levers) {
     switch (levers) {
         case LOG.LEVELS.DEBUG:
             return console.log("debug");
         case LOG.LEVELS.INFO:
             return console.log("info");
         case LOG.LEVELS.WARN:
             return console.log("warn");
         default:
             return console.log("throw new Error");
     }
 }
 //只能使用LOG.LEVELS 才能正确返回值
 getError("debug");                 //throw new Error
 getError(LOG.LEVELS.WARN);         //warn

JavaScript 内置Symbol值:

除了定义自己使用的 Symbol 值以外,ES6 还提供了 很多内置的 Symbol 值,指向语言内部使用的方法:

内置Symbol值,其实就是Symobl的属性 Symbol 属于JavaScript的一个 内置对象:

JS底层的很多方法都是通过Symbol 进行配置的: instance of

 /** Symbol的内置属性: */
 /** JS底层很多的实现类都是通过Symbol属性进行规定的: instanceof 
     instance of JS的内置属性用来判断对象的类型; */
 {   console.log([] instanceof Array); }
 /** 而对于对象类型 */
 {
     class Wsm{ constructor(name){ this.name=name } }
     let w1 = new Wsm("wsm");
     console.log(w1 instanceof Wsm);
 }
 /** 其实底层的 instanceof 就相当于调用了对象的 `Symbol.hasInstance属性`而我们也可以同Symbol来自定义函数执行 */
 {
     class Wsm{ 
         constructor(name){ this.name=name }
         static [Symbol.hasInstance](param){
             console.log("自定义instanceof");
             return param.constructor === this;
         }
     }
     let w1 = new Wsm("wsm"); 
     console.log(w1 instanceof Wsm);     //自定义instanceof  true
 }
  • 我们可以到每当我们使用 xxx instanceof Wsm 就相当于调用:Wsm[Symbol.hasInstance](){ } 函数
  • 注意: 底层很多地方都使用这个,所以一般不建议随意修改了解即可

Iterator 接口遍历器

ES6之后的JavaScirpt 真的是越来越像Java了,不够依然和Java有所不同,就比如Symbol:ES6通过Symbol 来实现伪接口编程

Iterator遍历器:就是这样一种机制,它是一种接口,为各种不同的数据结构提供统一的访问机制

ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性

一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”

原生具备 Iterator 接口的数据结构如下:

Array、Map、Set、String、TypedArray、函数的 arguments 对象、NodeList 对象

  • 迭代器可以用于for…of 循环,遍历实现 Symbol.iterator属性的数据结构,例如数组、字符串、Map、Set等

  • 迭代器对象有一个next方法,每次调用返回一个 { "value" : [obj],"done": [true|false] } 对象

    value是当前元素的值,done是一个布尔值,表示是否遍历结束

 /** Iterator迭代器的使用: */
 {
     //for...of:
     let arr = ['a', 'b', 'c'];
     for (const iteam of arr) { console.log(iteam); }
 ​
     //[Symbol.iterator].next() 函数:
     let iter = arr[Symbol.iterator]();
     console.log(iter.next()); // { value: 'a', done: false }
     console.log(iter.next()); // { value: 'b', done: false }
     console.log(iter.next()); // { value: 'c', done: false }
     console.log(iter.next()); // { value: undefined, done: true }
 }
 //有一些场合会默认调用 Iterator 接口(即Symbol.iterator方法
 //对数组和 Set 结构进行解构赋值时,会默认调用Symbol.iterator方法
 //扩展运算符(...)也会调用默认的 Iterator 接口、Array.from()、Map()、Promise....

自定义对象实现Iterator 接口:

[Symbol.iterator]方法应该返回一个next函数对象

next函数会在每次迭代时被调用,next函数应该返回包含valuedone属性的对象以下是一个简单的例子:

 {
     class MyIterable {
         constructor(name, age, interests) {
           this.name = name;
           this.age = age;
           this.interests = interests;
         }
       
         [Symbol.iterator]() {
           // 保存类实例的引用
           let index = 0;
           const self = this;  
           return {
             next: function () {
               if (index < Object.keys(self).length) {
                 //获取属性并返回属性名和对应的值 done为false
                 const key = Object.keys(self)[index++];
                 return { value: { key, value: self[key] }, done: false };
               } else {
                 index = 0; // 重置索引
                 return { done: true }; 
             }
             }
           };
         } 
     }
       //使用自定义类和迭代器: 我们可以对不同数据类型有了自己的控制
       const myObj = new MyIterable("John", 30, ["Reading", "Traveling"]);
       for (const { key, value } of myObj) { console.log(`${key}: ${value}`); }
       let iter = myObj[Symbol.iterator]();
       console.log(iter.next()); // { value: 'a', done: false }
       console.log(iter.next()); // { value: 'b', done: false }
       console.log(iter.next()); // { value: 'c', done: false }
       console.log(iter.next()); // { value: undefined, done: true }
 }

终于搞定了,至于ES6缺少的部分后面更新🆙

JavaScript 进阶 ES6