镇楼图

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://caniuse.com/

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.jspolyfill.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

posted on 2022-09-02 01:02  摸鱼鱼的尛善  阅读(46)  评论(0编辑  收藏  举报