镇楼图
Pixiv:DSマイル
〇、介绍
简介
浏览器中被嵌入了JS引擎(或称JS虚拟机)。在Chrome、Edge中为V8,在Firefox中为SpiderMonkey,Chakra则是用于IE,只要具备JS引擎即可执行JS脚本。简单来说引擎就是先解析脚本,再编译为机器语言,最后执行实现功能。
为了保证安全性,JS的文件操作功能比较受限,且因为同源策略导致了常见的跨域问题。
JS的语法可能并不能保证需求,便出现了CoffeeScript、TypeScript、Dart、Rescript、Kotlin等JS之上的语言
JS遵循ECMA-262规范,最新的规范草案在https://tc39.es/ecma262/,而最新的即将纳入规范的功能则在https://github.com/tc39/proposals。开发一般会参考MDN
但是不同浏览器对其兼容性不同,为了解决兼容性问题,可以查看下面两个网站来制定策略
■https://kangax.github.io/compat-table/es6/
Hello World
在浏览器中是使用script标签的,alert表示提醒框的形式输出
<script>
alert("Hello World");
</script>
而如果要引入外部脚本,则采用这种形式
<script src="url/1.js"></script>
<!-- 外部脚本内不应添加任何JS代码,否则会忽略 -->
一个语句的分号可以选择加或不加,不加分号的情况JS会自动判断来加分号,但这种可能会导致一些错误,建议语句加上分号
alert("Hello")
//会导致错误
[1, 2].forEach(alert);
注释有两种方式
//单行注释
/*
多行注释
*/
JS在版本发展中有许多新的变化,但这种变化默认情况下并不生效,原因是为了保证兼容性,需要使用use strict
来激活,可以在JS代码开始时声明或某个函数开始时声明。但是在class、module中是自动启用的不需要添加
function find(){
"use strict";
//启动严格模式,也可用'use strict'
}
一、基础语法
变量
变量声明有var、let、const三种类型,但var并不建议使用,它有诸多不便,这里不再说明
而是采用let和const定义,const即声明常量
let a = 5;
let b;
const c;//错误,常量必须赋值
const d = 50;
d = 30;//错误,d为常量无法修改
let e, f = 500, g = 5000;
let a;//错误,let以及const无法重复声明
变量名和其他大多数语言相同,首字符非数字,只允许字母、数字、_和$,区分大小写,无法使用关键字。在未使用严格模式的情况下,可以不声明,但并不推荐
a = 50;//正确
"use strict";
a = 50;//错误,未定义
数据类型
(1)Number类型
JS采用了双精度浮点数来表示数字
let a = 1;
let b = 1.23;
let c = .45;//省略整数位则为0,0.45
但是JS的Number类型不能有效地存储\([2^{53}-1,-2^{53}-1]\)以外的数字否则会出现非常明显的精度问题,因此JS支持了另外类型BigInt用于表示任意长度的整数,只需要在数字背后加n
即表示BigInt
let a = 12345678987654321n;//加n才是BigInt类型
(2)String类型
最基本的String就是单引号和双引号
let a = '123';
let b = "123";
但JS还有另外一种字符串,它可以格式化的输入输出
let c = 123;
let d = 456;
let e = `${c} + ${d} = ${c+d}`;//即${表达式}的形式去嵌入
(3)Boolean类型
即true和false
let a = true;
let b = false;
(4)null
null是特殊的空值,表示不存在值
let a = null;//未确定值时可以暂定为null
(5)undefined
undefined在值上是等价于null的,只有变量声明且未赋值时会自动赋予undefined,但建议任何变量都赋予值哪怕是null
let a;
alert(a);//值为undefined
其他类型后续会进行介绍
typeof运算符
由于JS是弱类型的语言,类型校验非常重要,如果不注重有时可能产生严重bug
console.log(typeof 0n);//BigInt
console.log(typeof(null));
//Object,为了兼容早期版本而出现的错误
let a;
console.log(typeof a);//undefined
console.log(typeof(alert));//function
简单输入输出
(1)prompt (title, [default])
现实带有title信息的输入框,第二个参数用于指定输入框的默认值
let res = prompt("输入信息", 15);
alert(res);
//若按Esc则会返回null
(2)confirm (question)
带有question文本信息的选择框,可选择确定或取消,根据选择返回true或false
let isBoss = confirm("Are you the boss?");
alert( isBoss );
类型转换
由于JS是弱类型语言,它会在适当的时候对类型进行自动转换,这种特性有时候比较坑,需要做类型校验。除了自动转换也提供了手动转换
let a = String(.123);//转为字符串0.123
let b = Number("123");//转为数字123,null,空字符串会转成0,而undefined会转成NaN
//字符串转成数字的规则比较复杂,这里不多说明
let c = Boolean(1);//true
let d = Boolean(0);//false,0、NaN、null、undefined、空字符串都会转成false
运算符
//数值
let a = 5;
console.log(a++ +2);
console.log(++a+2);
console.log(a-- -2);
console.log(--a-2);//自增自减详细参考C语言
console.log(1 + 2.5);
console.log(1 - 2.5);
console.log(1 * 2.5);
console.log(1 / 2);//由于Number是浮点数,不存在整除一说
console.log(3 % 2.2);//支持小数求余
console.log(3.6 ** 2.2);//幂乘
//字符串
console.log(3 + 5 + "2.2");//82.2,它遵循从左到右的原则,如果发现字符串要拼接则会进行转换
console.log("12" + 5 + 3);//1253并非128
但运算符会有自动类型转换,需要注意某些情况,特别容易引发bug
console.log(3 - "2");//Number类型,1
赋值运算符
let a = 5;
a += 3;
a -= 2;
a /= 2;
a %= 7;
a **= 0.5;
let b,c;
b = c = a * 3//链式赋值,从右向左赋值
位运算符
console.log(0b110 & 0b011);//位与
console.log(0b110 | 0b011);//位或
console.log(0b110 ^ 0b011);//位异或
console.log(~5);//非
console.log(0b110 << 2);//左移
console.log(0b110 >> 2);//右移,根据最高位填充左侧
console.log(-64 >>>1);//无符号右移,统一填充0
逗号运算符
一般不常用,它只会返回最后的值
let a = (1+2, 3+4);//返回7
比较运算符
■==:等于
■!=:也可写作<>,表示不等于
■>=:大于等于
■<=:小于等于
■>:大于
■<:小于
■===:等于
■!==:不等于
字符串比较详细参考其他语言(原理都是比较编码顺序),而Boolean类型则会将true转成1,false转成0。但null和undefined可能会比较特殊
由于JS是弱类型的,会先将类型自动转成同一类型(这可能很坑);因此有另外两个运算符是严格相等、严格不等,在比较前优先比较类型,如果类型不同,则直接返回false,类型相同才继续比较。下面有些奇怪例子,显然是有比较类型的更加好,但对于不严格比较需要注意null和undefined的参与
console.log(0 == false);//true
console.log("" == false);//true,Boolean型会转成Number型
console.log(0 === false);//false,由于类型不同为false
console.log(null == undefined);//true
console.log(null === undefined);//false
console.log(null > 0);//false
console.log(null == 0);//false
console.log(null >= 0);//true
console.log(undefined > 0);//false
console.log(undefined < 0);//false
console.log(undefined == 0);//false
逻辑运算符
逻辑运算符 | 功能 |
---|---|
a || b | 短路或 |
a && b | 短路与 |
!a | 逻辑非 |
除了逻辑上的运算功能外,其返回值并非true或false而是返回第一个真值,这时候可以有一些特殊用法
let a = "",
b = "ak47",
c = "";
console.log(a || b || c);//ak47
//如果能保证只有唯一真值可作为选择输出
//同理&&也可以作为唯一假值的输出
//若输出直到结束均未返回,则会输出最后一值
let a = "",
b = "",
c = null;
console.log(a || b || c);//null
优先级上非高于短路与高于短路或,短路特性参考C语言
空值合并运算符??
这是最新的特性,可能有兼容性问题。主要是为了防止一些特殊情况的空值
a ?? b;
a ?? b ?? c;
//已定义是指不为null,undefined
//其运算逻辑是返回第一个已定义的值
//一般情况是a为变量,b为默认值避免因为空值导致的bug
其出现是为了解决||的情况,||也会将false、0、null、NaN和空字符串作为考虑范围内,而??是更加约束的情况,优先级上与||相同
let height = 0;
alert(height || 100); // 100
alert(height ?? 100); // 0
出于安全性,??禁止和&&、||一起使用
let x = 1 && 2 ?? 3;//错误
let y = (1 && 2) ?? 3;//可以使用括号来解决
分支
它和C语言是一样的,这里不过多介绍
//基本的if-else
if(/*exp*/){
//...
}else if(/*exp*/){
//...
}else{
//...
}
//三元运算符
(/*条件*/) ? /*满足条件的值*/ : /*不满足条件的值*/;
let accessAllowed = (age >= 18) ? true : false;
//判断是否成年,若大于年龄达到18则为true
switch(x) {
case 'value1': // if (x === 'value1')
...
[break];
case 'value2': // if (x === 'value2')
...
[break];
default:
...
[break];
}
//具体语法参考C语言
//在JS中case的值是要求严格相等的,并不会做类型转换
循环
while (/*条件*/) {
// 循环体
}
do {
// 循环体
} while (/*条件*/);
for (/*初始条件*/; /*条件*/; /*步长*/) {
// 循环体
}
其余还有break(包括标签的语法)、continue这里不再说明。但break/continue是禁止与三元运算符? :
一起使用的,此外break的标签语法只能在代码块内,如果需要跳转至任意处可加{}解决。continue只能在循环内部中使用
label1: {
// ...
break label1; // 有效
// ...
}
break label2; // 无效
label2: for (...)
函数
function 函数名(/*参数列表*/){
//...
//return x;可添加return值若没有默认返回undefined
//return;也代表返回undefined
}
函数名(/*参数列表*/);//调用
function gcd(a, b) {
let r = a % b;
if (r > b / 2)r = (b-r);
return (r) ? gcd(b, r) : b;
}
alert(gcd(123,456));
从变量范围上来说,内部可访问外部,外部无法访问内部。若变量名相同则会优先访问同级别的再访问外部
let userName = 'John';
function showMessage() {
let userName = "Bob"; // 声明局部变量
let message = 'Hello, ' + userName;
alert(message);
}
// 函数内部的userName与函数外部的并不相同
showMessage();
alert( userName ); // John,未被更改,函数没有访问外部变量。
参数可以提供默认值,适用于未输入参数的情况,但如果未指定默认值,则默认值为undefined
function A(){return 5;}
function gcd(a = 7, b = A()) {
//默认值也可以为表达式
let r = a % b;
if (r > b / 2)r = (b-r);
return (r) ? gcd(b, r) : b;
}
alert(gcd(12));
alert(gcd());
函数表达式
函数是一种特殊的值,用于特殊值的情况下并不需要函数名
let a = function(){
//...
}
//创建函数保存至a变量中
a();//调用
function sayHi() {
alert( "Hello" );
}
let func = sayHi;
func();
sayHi();
回调函数:是指参数为函数的情况,这种情况可能会非常容易发生
比如要完成某个行动的函数,但是行动有策略,于是策略为函数类型的参数
function action(strategy){
//...
strategy(/*参数*/);
//strategy函数作为action的参数
}
函数和变量一样具有块级作用域,并不能在外部调用。如果需要在外部调用,则在外部定义变量,内部将函数赋值给外部的变量
let age = prompt("What is your age?", 18);
if (age < 18) {
function welcome() {
alert("Hello!");
}
} else {
function welcome() {
alert("Greetings!");
}
}
welcome(); //错误,未定义
箭头函数
箭头函数特别常见
let func = (/*参数列表*/) => /*表达式*/;
let sum1 = (a, b) => a + b;
let inc = n => n++;//如果参数只有一个可以省略括号
() => alert("Hello World!");//空参数的形式
let sum2 = (a, b) => {
let result = a + b;
return result;
};
支持旧代码
JS有很多新特性并不一定能支持,这时候需要另外两个工具转译器(Transpilers)和垫片(Polyfills),这些内容这里就不具体说明了
(1)Transpilers会分析代码,如果有发现旧版本不支持的情况,则会将新特性的代码等价地转换为旧形式的代码,Babel是比较著名的转译器(它被用在了webpack中)
(2)Polyfills是针对函数或类的情况,因为某些新特性只是针对于函数这种,只需要填补即可。core.js和polyfill.io支持这种服务
调试
(1)断点,在开发者工具中Sources内的代码文件可以通过左键行号设置断点右键设置条件断点
(2)debugger,可以在代码中使用debugger来暂停,但是debugger只有开发者工具打开才能生效,浏览器会忽略
在控制台右侧提供了调试的指令,从左到右依次为
■继续执行(F8)
■运行下一条指令(F9)
■跨步运行下一条指令,不会进入到函数内(F10)
■步入,类似于F9但在异步函数调用时有区别,它不会忽略异步行为如setTimeout
(F11)
■步出,当进入函数内时可以直接调用至函数结尾(Shift+F11)
■启用或禁用所有断点
■若启用,如果调用过程中发生bug则会暂停调用
let name = "John";
debugger;
let phrase = `Hello, ${name}!`;
debugger;
console.log(phrase);
右侧同样有其他信息
■Watch:显示任意表达式的当前值
■Call Stack:显示函数的调用栈
■Scope:显示当前变量的作用域,如果是函数内变量则显示Local
,如果是全局变量则显示Global
,此外还有this
二、对象
创建
对象具有属性列表,用{...}
表示,其中一个属性就是一个key: value的键值对,其中key是属性名,value是其值。一般情况下key只有说明语义的功能,比如属性名攻击力、防御力、角色名等
■创建空对象
let obj1 = new Object();//函数创建
let obj2 = {};//属性列表创建
■创建对象
let player = {
name: "John",
atk: 100,
def: 25.5,
level: 1
//键值对填充即可增加属性
};
■属性的访问
属性有两种访问方式,可以参考C语言是相类似的
console.log(player.name);
console.log(player["name"]);
//第二种访问方式必须采用字符串的形式
let date = {
s1: {
hour: 12,
min: 24,
sec: 36
},//对象下也可以嵌套对象
};
console.log(date.s1.min);
console.log(date["s1"]["min"]);
console.log(date.s1["min"]);
//对于复杂的对象继续访问即可
//也可以混合使用不同的访问方式
■属性的添加/删除
player.weapon = "AK47";
//要添加属性和访问语法是一样的,只需要保证属性名不在对象里即可
//否则就是访问对象的属性来修改值
delete player.weapon;
//采用delete运算符删除属性
■多词属性
有的时候一个属性名可能并不能表达语义,而需要多个词语来描述,采用字符串即可
let player2 = {
"like fruit": "apple",
"like vegetable": "pepper",
}
console.log(player2["likes fruit"]);
//但此时只能使用这种访问方式,其他添加,删除也一样
player2["like music"] = "INSANE";
delete player2["like fruit"];
console.log(player2);
简写情况
■变量赋予属性名
属性名可以通过变量来赋予
let key = "str1";
let obj3 = {};
obj3[key] = "1";
//必须采用这种访问形式,obj3.key则当作了key为属性名
console.log(obj3);
key = "str2";
console.log(obj3);
//变量值并不会改变属性名
//可以理解相互独立并不会随之变化
//js不只是字符串,甚至其他类型的值都可以充当属性名
//但是Object类型只会当作[object Object]
//因此下面注释的一行是无效的
let k1 = 123;
let k2 = true;
let k3 = {};
let k4 = {
"1 2 3": 123,
"4 5 6": 456
};
let obj4 = {};
obj4[k1] = 1;
obj4[k2] = 2;
//obj4[k3] = 3;
obj4[k4] = 4;
console.log(obj4);
//此外在访问上也具有灵活性
//比如给定几个选项,玩家选择后会赋予到变量内
//此时访问就很容易
let s = select(/***/);//用户选择
console.log(obj4[s]);//根据用户选择来输出
■变量赋予属性值
属性值也可以通过变量来赋予
let k = 1;
let obj5 = {
attr: k,
};
如果属性值的变量名与属性名相同则可以进一步简写
let atk = 100;
let player3 = {
atk,
//属性名为atk且属性值为100
//或许你已经注意到命名规则让这种简写无法应用于多词属性
}
属性名的命名规则
属性名的本质是字符串类型,其他类型也会转换成字符串类型,因此它的命名不符合变量的命名规则,哪怕是let
,if
等关键字都是可以的,但是有一个特殊的属性__proto__
是对象固有的属性不能当作新的属性
let obj5 = {
let: 5;
};
in操作符
in操作符用来判断属性是否在其中并返回Boolean类型的值
let obj6 = {
a: 1,
c: 3,
};
console.log("a" in obj6);//true
console.log("b" in obj6);//false
在大部分情况下,如果直接索引不存在的属性会返回undefined,但是存在例外,如果属性值恰好为undefined就不能说明是否存在
let obj7 = {
a: undefined,
b: 1,
};
console.log(obj7["c"]);//undefined,即不存在属性
console.log(obj7["a"]);//undefined,但确是存在的
//因此建议采用in操作符而不是根据索引值是否为undefined
for in循环
对于Object类型,如果用以前的循环获取其属性很难去迭代(需要用其他方法间接迭代),因此采用新的的循环方式来获取属性
let player3 = {
name: "Jhon",
atk: 100,
def: 20
};
console.log("玩家的属性为");
for(let key in player3){
console.log(`${key}:${player3[key]}\n`);
//key获取属性名
//player3[key]获取属性值
}
但是遍历对象的顺序并不一定按照创建顺序从上到下来
首先会排列整数属性,然后是非整数属性。其中整数属性是升序排序,非整数属性则会按照创建顺序
因此建议除非有特殊需求,属性名应避免整数属性
let obj8 = {
"15": 15,//改成非整数属性很容易+15或_15等都是可以的
true: true,
2: 2,
"abc": "abc",
7: 7,
}
for (let key in obj8) {
console.log(obj8[key]);
//实际顺序为2,7,15,true,abc
}
对象的COPY
对象是所有基础类型中唯一的引用类型。其他基础类型在赋值上都是值进行COPY,而对象存储的是内存地址,因此COPY后实质上还是原来的
let a = 2;
let b = a;
//a和b是两个变量
let a = {
a: 1,
b: 2,
};
let b = a;
//此时a和b是完全一样的
delete b.a;
console.log(a);
console.log(b);
//对b的操作实质也是对a的操作
此外由于是引用类型,在比较是否相等实际上是比较内存地址是否相等。比较时会将对象转成原始值,然后比较,这个原始值就代表了内存地址
let c = {};
let d = {};
let e = d;
console.log(c === d);//false,内存地址不同
console.log(d === e);//true,内存地址相同
如果要实现COPY操作且对象是完全不同的则需要你自己去编写函数来实现
let copyObject = (obj) => {
if(typeof obj !== "object"){
return -1;
}else{
let newobj = {};
for(let key in obj){
newobj[key] = obj[key];
}
return newobj;
}
}
除了自己实现外JS也提供了Object的方法
Object.assign(dest,[src1,src2,...]);
//dest为被COPY的对象,不一定要为空对象
//后面的src是要COPY的对象
//可以有多个src即将所有的src的属性COPY到新的对象中
//如果在COPY过程中存在属性名相同的情况则后面的会覆盖前面的
let user = {
name: "John",
age: 30,
};
let clone = Object.assign({}, user);
深层COPY
因为对象内属性值可以为对象值,此时就会有另外的问题,对于这种情况之前博主所写的函数就不能用了,他会导致属性值出现引用相同的情况,比如
let a = {
a: 1,
b: 2,
c: 3,
d: {
a: 10,
b: 11,
c: 12,
}
}
let b = copyObject(a);
console.log(a.d === b.d);//true,并不能解决嵌套的情况
刚才提到的方法以及JS内置的Objcet.assign方法均不能解决这种问题。如果手写函数还需要对属性值作类型校验,如果是对象需要执行当前函数,改动如下:
let copyObject = (obj) => {
if(typeof obj !== "object"){
return -1;
}else{
let newobj = {};
for(let key in obj){
if(typeof obj[key] == "object"){
//对值类型校验
let temp = copyObject(obj[key]);
newobj[key] = temp;
}else{
newobj[key] = obj[key];
}
}
return newobj;
}
}
内置的方法涉及到很后面才说的JSON,它的方法为JSON.parse(JSON.stringify(obj))
■const声明的对象可以被修改
const只针对当前变量,其下的属性都是可以被修改的
const obj9 = {
a: 1;
};
obj9["a"] = 2;
obj9["b"] = 3;
delete obj["a"];
//可以修改
对象方法
对象除了存储数据外也可以存储函数
let player4 = {
name: "John",
}
player4.walk = ()=>{
console.log("walk");
};
player4.walk();
player4["walk"]();
//同样也有两种方式访问
let run = ()=>{
console.log("run");
}
player4.run = run;
//也可以先定义函数再添加
let player5 = {
name: "John",
walk: function(){
console.log("walk");
},//也可以直接嵌入进去
run(){
console.log("run");
},//也可以简写,建议采取这种形式
};
this
在普通函数的情况下,this所指代的对象会在运行中计算出,在函数内部this由调用者决定,比如下面的是由player6决定的
let player6 = {
name: "John",
walk(){
console.log(`${name} is walking`);
},//并不能直接像Java的class一样访问
run(){
console.log(`${this.name} is running`);
},//也可以直接player6.name访问
//但如果player6复制给了player7那么就会出现问题
};
player6.walk();
player6.run();
如果调用者是全局的,则在严格模式下为undefined,未启用的情况下为Window对象
let walk = ()=>{
console.log(this);
};
walk();//Window
//this的指向取决于调用者
箭头函数内的this
在箭头函数中this具有封闭语法环境,如果箭头函数内使用了this,可以将整个箭头函数当作this来判断this的具体指向
let player7 = {
name: "John",
walk: () =>{
console.log(this);
},
run(){//实际上是run: function()的缩写
console.log(this);
},
example: this,
};
player7.walk();
console.log(player7["example"]);
//Window,因为封闭,可以当作只有walk: this,
player7.run();
//player7
在某些时候箭头函数的使用能方便不少,如下例通过箭头函数防止this指向不该指向的地方
let player8 = {
name: "John",
walk(){
setTimeout(()=>{console.log(`等一会,${this.name}`);},1200);
},
};
player8.walk();
//setTimeout是指延迟1200ms后执行函数
//如果第一个参数采用普通函数
//则普通函数判断调用者是setTimeout
//会导致this指向了全局对象
//使用箭头函数则相当于walk(){setTimeout(this,1200);}
//console.log(this)并不代表this在console.log函数内
//同理this也不在setTimeout函数内
//此时调用者为player8
对象构造器
上述对象都是具体的结构,如果需要构造则需要用到函数、this以及new
构造器不能使用箭头函数,因为它构造的原理用到了this,而箭头函数的封闭性导致了无法实现。简单来说就是函数使用this,此时this会指向函数本身从而赋予属性,再通过new操作符创建对象就实现了。
function Player(name,atk,def) {
//this赋予函数属性
this.name = name;
this.atk = atk;
this["def"] = def;
}
let player10 = new Player("John",100,20);
console.log(player10);
//new创建对象
关于new它本质上是创建一个空对象分配给this,然后返回this。通过这样的搭配可实现可复用的对象类型,或者某种意义上就是类。从new的原理上来说任何函数都可以是构造器,只要通过this赋予属性返回this即可
//相当于
function Player(name,atk,def) {
//this = {};
this.name = name;
this.atk = atk;
this["def"] = def;
//return this;
}
构造器的其他内容
■new.target提升编写体验
可以使用new.target属性来检查是否被new调用了
function Player(name,atk,def) {
if(!new.target){
//如果没有用new则返回带new的
return new Player(name,atk,def);
}else{
this.name = name;
this.atk = atk;
this["def"] = def;
}
}
let player10 = Player("John",100,20);
//可以采用这种方法让用户少写new
//有的时候就体验而言可能非常重要
■构造器的return
因为构造器本质上就是利用new来返回this,因此一般情况而言不能写入return,但是某些特殊情况下可以去return,比如当用户没有输入时去return一个有默认属性值的对象。
■省略的情况
如果new的函数没有参数则可以忽略括号
function A(){
this.a = "a";
}
let a = new A;
可选链?.
这是比较新的特性,可能有兼容性问题。有一类问题是在访问时,一般情况即使对象没有属性也是可以访问的,但如果对象为null或undefined则没有属性无法访问出现bug
let obj10 = {
a: 1,
};
console.log(obj10.a.b);//虽然不存在但不至于bug
console.log(obj10.b.c);//b为undefined出现bug
document.getElementById("...").attr;
//很典型的就是DOM操作的代码写在头部然后报错
//但至少应该返回一个异常值不至于报错
这种情况可能经常发生,为了避免这种情况,如果手动实现的话需要类型校验来返回导致代码比较复杂,可选链能很好地解决这种问题。手动实现这里不再说明,直接说可选链是如何实现的
■运算逻辑:如果可选链前的值为undefined或null(称为不存在),则会停止运算返回undefined
■短路:由于其运算逻辑当发现不存在时,不管后面的运算有多复杂都会停止运算
■bug:如果可选链前的变量是未声明的,则会报错
■适用范围,作为一种特殊的语法,只要是.?()或.?[]也可以
let userAdmin = {
admin() {
alert("I am admin");
}
};
let userGuest = {};
userAdmin.admin?.();
userGuest.admin?.();//不存在方法但不会报错
■可选链仅用于保证读取的安全性,而写入不建议使用可选链
Symbol类型
对象的属性名只接受字符串类型和Symbol类型,大部分情况字符串类型都能解决,Symbol是为了解决另外一个问题。如果属性名相同的情况下,字符串类型显然是无法满足需求的,因此引入了Symbol解决这种需求
Symbol类型的创建很像对象,但是参数只能为一个String类型的描述
let id1 = Symbol();
let id2 = Symbol("描述,必须是String类型");
let id3 = Symbol("name");
let id4 = Symbol("name");
console.log(id3 === id4);//false
■Symbol无法自动转成String类型
Symbol是例外,无法自动转成String类型,因此需要用内置的toString方法来手动转换后使用
let id = Symbol("id");
alert(id); // 类型错误
alert(id.toString());//正确
■Symbol.description
可以使用description来返回描述
let id = Symbol("id");
alert(id.description);
■解决重复属性名的问题
Symbol简单来说就是多嵌套一层从而可以使用相同的属性名
const name1 = Symbol("name");
const name2 = Symbol("name");
const player11 = {
[name1]: "John",
[name2]: "Golden King",
//属性名都是Symbol("name")
//但是通过变量名的不同区分了
};
console.log(player1[name1]);
console.log(player1[name2]);
■会被for in循环跳过
const name1 = Symbol("name");
const name2 = Symbol("name");
const player11 = {
[name1]: "John",
[name2]: "Golden King",
atk: 100,
def: 20,
};
for(let key in player1){
console.log(player1[key]);
//会忽略[name1]和[name2]即访问上会忽略
}
let clone = Object.assign({},player11);
//但是复制并不会忽略
■全局Symbol
如果Symbol虽然能保证描述是相同的,但每次创建都是完全不同的Symbol,如果要相同,则应当使用Symbol.for(key)方法获取相同的Symbol
for(key)的方法原理是在全局Symbol注册表中寻找是否有key的,如果没用则创建新的,如果有则返回保证相同。而普通的Symbol则无法做到这一点
let id5 = Symbol.for("name");
let id6 = Symbol.for("name");
console.log(id5 == id6);//true,相同的Symbol
■Symbol.keyFor(Sym)
针对于全局Symbol来说,它还可以使用keyFor的方法来获取描述,但是仅限于全局Symbol
let id7 = Symbol.for("name");
console.log(Symbol.keyFor(id7));//name
let id8 = Symbol("name");
console.log(Symbol.keyFor(id8));
//非全局Symbol,无法找到只会返回undefined
■重写语法规则
相比于上面避免同名属性名只是其中一个用法,另外一个用法则是重写一些语法规则,它主要可以重写对象、迭代器、正则相关的语法。这里不会多说,下面只谈谈如何用Symbol重写对象的语法
对象原始值
其他类型有相应的转换规则,Object也不例外,它可以转换成String类型或Object类型。采用toString或valueOf方法实现转换。String类型的原始值相当于简写形式告诉你这是什么类型,Object类型则会详细到方方面面有什么属性、方法之类的
let player12 = {
a: 1,
b: 2,
c: 3,
};
console.log(player12.toString());//[object Object]
console.log(player12.valueOf());//object,可详细查看
但上面都是手动进行转换的,然而作为弱类型语言它会根据实际情况来自动转换。在自动转换的情况下,会根据hint值来执行相应方法,hint为特殊值只有"number"、"string"和"default",会根据不同预期情况来执行
Symbol有一属性toPrimitive可以重写原始值,它的参数为hint。
JS首先会看Symbol.toPrimitive是否存在,如果不存在也就是开发者并没有干预自动转换的方法。如果hint为number即object在一个数学运算的环境下,它会先尝试调用valueOf方法;如果hint为string即在object在一个字符串运算的环境下,它会先尝试调用toString方法;default则是无法判别的情况,它和number情况相同
let player13 = {
a: 1,
b: 2,
c: 3,
};
console.log(player13 + "123456");
//[object Object]123456,字符串运算环境
console.log(+player13 * 5);
//NaN,数学运算环境,但是valueOf方法得到是非数字
这时候Symbol修改内部语法的作用就显现出来了,通过Symbol改写Object类型自动转换的情况
let player14 = {
a: 1,
b: 2,
c: 3,
[Symbol.toPrimitive](hint) {
if(hint == "number"){
return 15;
}else if(hint == "string"){
return "player14";
}else{//即hint为default的情况
return "player14";
}
},
};
console.log(player14 + "123456");
//player14123456
console.log(+player14 * 5);
//75
■Symbol.toStringTag修改标签
当对象类型采用toString方法时,它总会显示[object Object]
,这个标签的第一个object表示类型是object这个肯定是不能修改了,然而第二个是一个描述性的词语,比如String类型如果用对象包装则会显示[object String]
。
Symbol.toStringTag可以自定义这样的标签
let player15 = {
a: 1,
b: 2,
c: 3,
get [Symbol.toStringTag](){
//get是关键字,暂时不用纠结有什么用
return "Player";
},
};
console.log(player15.toString());
//[object Player]
参考资料
[1] 《JavaScrpit DOM 编程艺术》
[2] MDN
[3] 现代JS教程
[4] 黑马程序员 JS pink