ECMAScript
ECMAScript(简称“ES”)是根据:ECMA-262 标准实现的通用脚本语言
ECMA-262 标准主要规定了这门语言的语法、类型、语句、关键字、保留字、操作符、对象等几个部分.
ESMA (European Computer Manufacturers Association) 是一个组织
中文名称为:欧洲计算机制造商协会
,这个组织的目标是评估、开发和认可电信和计算机标准.
Ecma 国际制定了许多标准,而 ECMA-262 只是其中的一个
ECMA-262 定义了 JavaScript 的语法、语义、基本对象和操作,以及与浏览器环境和其他宿主环境的交互等方面的规则。
TC39(Technical Committee 39) 是推进 ECMAScript 发展的委员会,其会员都是公司 其中主要是浏览器厂商,有苹果、谷歌、微软、因特尔等)
是ECMA-262,在2015年发布的新版本,ES此后每年进行更新~
ES6 的版本变动内容最多,具有里程碑意义 ,引入了许多新的语法特性、功能和改进,使得 JavaScript 编码更加现代化、清晰和高效。
ES6的兼容性:
我们都知道,JavaScript在不同的浏览器中具有不同的兼容性,因为ES每年都会更新,
所以,一些较旧的浏览器可能不完全支持所有的 ES6 特性,可以通过官网进行查询兼容环境配置:ES6兼容性
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中的循环作用域:
let
声明变量可以在每次循环迭代时创建一个新的块级作用域,避免了上述问题。 //ES6 中使用 `let` 声明变量可以在每次循环迭代时创建一个新的块级作用域,避免了循环作用域问题。
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i); //输出 0、1、2、3、4每次循环迭代都会创建一个新的块级作用域.
}, 100);
}
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()
函数接受一个对象参数,并通过解构赋值提取了 firstName
、lastName
和 age
属性
当传入 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年就是`本命年`
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并不是一个东西别混淆
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 构造函数()
,为了避免内存泄漏了解到了**原型链接
**
箭头函数允许以更简洁的语法来声明函数,特别是在编写简单的匿名函数时非常方便
(参数列表) => { 代码主体 }
如果函数没有参数,你可以简单地使用空括号 ()
表示
如果函数只有一个参数,你可以省略参数周围的括号 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(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 动态参数
,但是可以通过 ...
动态获取实参,实际使用中:**...
**需要放在最后,方便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值
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函数进行计算;
setTimeout
、 setInterval
…回调函数存在丢失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就是全局的对象了 fun(obj.callback)
和 fun(callback)
本质上其实就是一个东西(同一个地址),前者并不会把obj带过去,仅仅是包装好看,引用的都是一个地址(药片)经过上述的分析,我们知道:函数作为回调函数调用
引用类型作为函数参数传递的是地址,函数内部的直接调用相当于没有任何对象引用,所以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变量
{全局箭头属性}
{作用域箭头属性}
🆗,上述代码我们测试了解了箭头函数中的,This属于函数所调用的作用域,但是Node环境中的this 还有不同: 箭头函数返回 undefined 因为:
this
就指向顶层对象global(在浏览器中为 window
对象)
在 Node.js 中,通过 global
可以获取全局对象
在严格模式和模块环境下,this
会返回 undefined
在松散模式下,可以在函数中返回 this
来获取全局对象
使用 node
命令执行文件时,默认会把文件当成一个模块,并将 this
修改成 {}
即 this = module.exports = exports
this
就指向那个对象本身this
指向它的实例对象Node环境中变量如何定义在全局global
global.变量名 = 变量值
⚠⚠⚠ 个人对node 学习使用并不多,分析不到位地方请指点学习
ES6 的集合是一种新的数据结构,它类似于数组,但是每个元素的值都是唯一的,没有重复的值简单介绍一下:
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}
}
Weak (中译: 弱): WeakSet 结构与 Set 类似,也是不重复的值的集合,但与 Set 有两个区别:
由于上面这个特点,WeakSet 的成员是不适合引用的,因为它会随时消失
由于WeakSet有多少个成员,取决于垃圾回收是否运行,运行前后可能元素个数不一样,而垃圾回收机制不可预测的,因此ES6规定WeakSet不可遍历
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 和 Map的区别:
keys()
、values()
和entries()
方法),也没有size
属性clear
方法,WeakMap
只有四个方法可用:get()
、set()
、has()
、delete()
因为弱引用,垃圾回收机制无法判断key是否存在,为了防止这种不确定性,则Weak没有便利、size、clear的操作
WeakSet 和 WeakMap 是基于弱引用的数据结构,例子很难演示,因为无法观察它里面的引用会自动消失,介绍 WeakRef
ES2021 提供了 WeakRef
对象,用于直接创建对象的弱引用:
/** 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
}
deref()
方法,如果原始对象存在,该方法返回原始对象;undefined
因为node 环境无法直接手动回收内存,需要通过:**node --expose-gc
** 开启
关于内存和垃圾回收机制,不同的运行环境可能会有不同的效果,Demo案例不方便展示,还需要多多学习📕
Class类的概念: 在很多的高级编程语言中:C++、Java...
都有类的概念,类是对象的抽象,对象是类的实例
本质来说或,类是用于创建对象而存在的概念性语法… ES6之前JavaScirpt 也可以通过函数式来创建对象
过程:声明式——函数式——原型链式
...个人觉得JavaScript 早期发展非常冗杂 ES6之后开始从弱语言—慢慢发展—更加完善健壮💪
ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为创建对象的模板通过class
关键字,可以定义类
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的一种语法糖🍬 有一点像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();
}
ES6 把整个语言升级到了严格模式
类和模块的内部,默认就是严格模式,所以不需要使用use strict
指定运行模式,严格模式规范了之前横冲直撞的代码😠
JS中的严格模式是一种在严格条件下运行JS代码的方式,ES6之前在所有语句之前添加 use strict
开启
消除一些不合理、不严谨或不安全的语法和行为,提高编译器效率,增加运行速度
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{ 中通过箭头函数定义 } 箭头函数this指向函数所在的{作用域}
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开始默认严格模式,所以这种情况大部分会报错,这里也只是为了了解展示案例
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 }
}
私有属性和私有方法是指只能在类内部访问的属性和方法: JavaScript的私有|属性|方法|有多种声明方式:
/** 早期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值...
}
建议事先Class中定义好Symbol值方便类的内部调用私有属性|方法
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());
}
私有属性和私有方法是指只能在类内部访问的属性和方法: 但是很多时候我们想要保证属性安全的同时依然想操作Class的属性;
与ES5 一样,在“类”的内部可以使用**get
**和**set
**关键字 ,对Class某个属性设置存值函数和取值函数,拦截该属性的存取行为,可以在属性存取时添加额外操作;
return 私用|共有属性
调用: 对象.getXxx
获取类的属性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修改私有属性
}
学习过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加载最先执行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 A { ... }
、calss B extends A { ... }
通过extends关键字实现继承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
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是父类?
}
所以:上述Demo子类consturctor 调用 super 返回 B
**:** super中的this是子类
但,为什么**this.xname
** 返回的是 Aclass,因为: super()
执行时,B
的xname
属性还没有绑定到this
,this.xname
拿到的是A
类的xname
属性
学习过JS对象都知道:原型链 可以根据对象寻找它的基类——形成一个继承关系,class依然存在原型链
__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
这个知识对于新手很不友好,所以不理解很正常,可以在后面工作过程中慢慢感受 我也是看了好多大佬的讲解,这里写的不好多多包含
吐槽网上很多人一堆视频、文章硬讲实在看不懂尚硅谷讲的也看不懂,稀里糊涂的建议举一点实际开发的例子…
如果实在看不懂直接跳过,影响不大,蹲一个评论区大佬
ES6 引入了一种新的原始数据类型**Symbol
**,通常用于表示独一无二的值,它属于 JavaScript 语言的原生数据类型之一
方式一: 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)); 注释报错影响运行;
JavaScript 属于弱语言可以随时在对象中添加新的属性
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 值作为属性名,遍历对象时候,该属性不会出现在for...in
、for...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));
}
枚举,不同的编程语言中好像概念都不同,这也是我晕的一个点😵 Java 中的枚举🔗
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
除了定义自己使用的 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](){ }
函数了解即可
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
函数应该返回包含value
和done
属性的对象以下是一个简单的例子:
{
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缺少的部分后面更新🆙 好好学习,天天向上!