侧边栏壁纸
博主头像
lac博主等级

行动起来,活在当下

  • 累计撰写 66 篇文章
  • 累计创建 12 个标签
  • 累计收到 1 条评论

目 录CONTENT

文章目录

js红书读书笔记——第三章:基础语法(一)

Hude
2023-10-18 / 0 评论 / 1 点赞 / 249 阅读 / 28366 字

语言基础

语法:即描述一门语言的方法论

  • 区分大小写:ECMAScript中一切都区分大小写,包括关键字,Typeof是有效的变量或函数名

  • 标识符:所谓标识符就是变量、函数、函数参数、属性的名称。规则:

    • 第一个字符必须是字母、下划线或美元符

    • 其他剩下的字符可以是字母、数字、下划线、美元符

  • 命名惯例:建议使用小驼峰式命名法,首字母小写,其余单词首字母大写,这样使用的原因是ES内部函数和对象的名称也是使用这种方式,保持一致性

注释:单行:// 多行:/* */

严格模式:使用"use strict"切换,现代所有浏览器都支持,使用严格模式的目的是不破坏ES3的语法

语句:以分号为结束,加分号有利于优化解释器执行效率,代码清晰。多条语句组成代码块,一般以{}包裹,if等语句必须有{},即使内部只有一条语句

关键字和保留字(ES6)

关键字:按照规定,关键字不能被用做标识符和属性名:

Weixin Image_20231013172110.png

保留字:是指在语言中暂未特定用途,但是保留为将来做关键字用的。

始终保留:

enum

严格模式下保留:

implements package public

interface protected static

let private

模块代码中保留:

await

保留字和关键字的区别:关键字不能作为标识符和属性名,保留字可以作为属性名但是不能作为标识符,但是应避免保留字作为属性名

变量

ECMAScrpit变量是松散类型的,可以用于保存任何类型的数据。每个变量只不过是一个用于保存任意值的命名占位符,有三个关键字可以声明变量:var(任意版本)、let和const(包含ES6之后)

声明提升

一段代码在真正执行前,会有一个专门用来声明变量的过程,俗语成这一过程为预解析/预处理。无论是var还是let/const,都是在这一过程被提前声明好的。俗语常把这一表现称为hoist,区别在于var声明后会被初始化为undefined,而let和const不会

var关键字:函数作用域

1.声明作用域:var关键字声明的变量是存在作用域限制的,如果在函数中声明,该变量的作用域不会超过这个函数,函数执行完毕后,变量也会被销毁

不过,如果不使用var,而是直接变量名=值的方式直接赋值变量,那么该变量就会变成全局变量。技术上,这种方式“声明”的变量实际上是一个全局对象的属性,可以使用delete删除。而变量是不能被删除的。严格模式下,给未声明的变量赋值,会抛出错误ReferenceError

在严格模式下,不能定义名为 eval 和 arguments 的变量,否则会导致语法错误

var声明提升:

通过var声明的变量,如果在声明之前访问它们,这看似不太合理的行为,实际上是可以存在的,比如:

function foo(){
	console.log(val);// undefined
	var val = 'message';
}

它等同于如下代码:

function foo(){
	var val; // undefined
	console.log(val);// undefined
	var val = 'message';
}

这就是所谓的提升(hoist),也就是把所有的变量声明都拉到作用域的顶部,同时var也可以声明同名变量:

function foo(){
	var val = 'a';
	var val = 'b';
	var val = 'c';
	console.log(val);// 'c'
}
// 实际上等同于
function foo(){
	var val;
	val = 'a';
	val = 'b';
	val = 'c';
	console.log('c');
}

let关键字:块作用域

let关键字和var关键字很类似,但是又有着明显的区别:let声明的范围是块作用域,var声明的范围是函数作用域

// 块作用域下,var声明可以在外部使用到
if(true){
	var val1 = 1;
}
console.log(val1); // 1

if(true){
	let val2 = 2;
}
console.log(val2);// val2 is not defined

块作用域是函数作用域的子集,因此适用于var的作用域的限制同样适用于let

let不允许冗余赋值

var val1 = 1;
var val1 = 2;
console.log(val1); // 2

let val2 = 3;
let val2 = 4;
console.log(val2); // SyntaxError

不过JavaScript引擎会记录变量声明的标识符和块作用域,因此,嵌套使用相同的标识符不会报错

let val = 1;
{
	let val = 2;
	console.log(val); // 2
}

冗余赋值报错不会因混用var和let声明而受到影响,还是会报错,这两个关键字并不是声明不同的变量,它们只是指出变量在相关作用域如何存在

let val1 = 1;
var val1 = 2; // SyntaxError

var val2 = 3;
let val2 = 4; // SyntaxError

1.时间死区

var关别键字和let关键字最大的区别之一就是let关键字不具有逻辑上声明提升的特性,注意,在技术实现上, let和const依旧会发生提升,只不过存在暂时性死区,let和const声明的变量不会初始化为undefined,在暂时性死区中访问变量会抛出引用异常

在js规范中,创建一个变量的流程大致会经过以下几个内部方法:CreateMutablBinding()声明变量,InitializeBinding()初始化变量,SetMutableBinding()变量赋值,getBindingValue()引用一个变量。在执行CreateMutableBinding()后没有执行InitializaBinding()初始化变量就执行SetMutableBinding()

console.log(val1); // undefined
var val1 = 'val1';

console.log(val2); // ReferenceError
let val2 = 'val2';

注意,在解析代码时,JavaScript引擎也会注意到块后面的let声明,只不过在let声明之前,不能以任何方式来引用未声明的变量,在let声明执行之前的瞬间,被称为”暂时性死区“(temporal dead zone),在此阶段引用任何后面声明的变量都会引发ReferenceError

暂时性死区依赖于词法环境的构建,在js中,每个上下文都有一个与之关联的词法环境,用于管理变量和函数的词法绑定。

词法环境由两部分组成:环境记录和外部词法环境的引用

具体来说,let和const声明会在词法环境的环境记录中创建相应的绑定,这些绑定包括变量名和一个特殊的值,称为uninitialized标志表示变量已经声明但尚未初始化。在初始化之前,该变量处于 TDZ 中

2.全局声明

与var不同的另外一个表现是,使用var在全局作用域声明的变量变成全局对象的属性,而let声明的变量则不会

var val1 = 1;
console.log(window.val1);// 1
console.log(this.val1);// 1

let val2 = 2;
console.log(window.val2); // undefined
console.log(this.val2); // undefined

3.条件声明

不太理解原文,先跳过,之后回来探索

4.for循环中使用let声明

在for循环主体中使用let声明,每一次循环javaScript引擎都会声明一个新的迭代变量,因此每一次循环都会使用不同的变量实体,而var则不会这样,它保存的是导致迭代退出的那个值

for(let i = 0; i < 5; i++){
	setTimeout(()=>{console.log(i)},1000)
}
// 1s后同时输出0,1,2,3,4

for(var i = 0; i < 5; i++){
	setTimeout(()=>{console.log(i)},1000)
}
// 1s后同时输出5,5,5,5,5,5

这种每次迭代都独立声明一次迭代变量实例的行为适用于所有风格的for循环

const声明

const直译为常量,因此它所声明的数据具有常量的特性,即必须初始化,且无法进行修改,但仅仅只表示对变量赋予常量特性,不在实现层面描述其为常量声明,其次他的行为和let很相似

const con = 1;
con = 2; // Assignment to constant variable.

// 与let具有形似的行为
// 不能声明同名变量
const con2 = 2;
const con2 = 3;// SyntaxError

// 作用域范围为块级作用域
const con2 = 1;
{
    const con2 = 3;
	console.log(con2); // 3
}

注意:

  • const 声明的限制只适用于它指向的变量的引用,也就是说,如果变量的引用是对象,修改对象属性并不违反const的限制

  • const不能用于声明for循环的迭代变量,因为迭代变量会自增。但是const声明for in和for of等循环的迭代变量是非常有意义的

变量声明最佳实践:不使用var,const优先,let次之

避免使用var,优先使用const,const可以让变量保持不变,也可以让静态代码分析工具提前发现不合法操作,如果确定变量将来会发生变化才使用let

数据类型

6种基本类型(原始类型):Undefined、Null、Boolean、Number、String、Symbol

1种复杂类型:Object

typeof操作符

由于ECMAScript的类型系统是松散型的,因此需要使用typeof来判断任意变量的类型。对一个值使用typeof时会返回以下字符串之一:

"undefined" // 值未定义
"bigint" // 大整数
"number" // 数值
"string" // 字符串
"booblean" // 布尔值
"function" // 函数
"object" // 对象或者null
"smybol" // 符号

注意:严格来说,ECMAScript中认为函数也是一个对象,但是函数有自己特殊的属性,因此需要使用function类型将它和对象区分开来

Undefined类型

Undefined类型只有一个值,特殊值"undefined",它表示一个变量(使用var或let声明)已声明,但未初始化时的值。

let val; // undefined

console.log(val); // undefined

console.log(typeof val); // 'undefined'

let ud = undefined;
console.log(ud === undefined); // true

undefined可以显式的被初始化,但是没有必要,因为默认情况下,任何没有初始化的变量,它的值都是'undefined'

字面量undefined主要用于比较,在ES3之后才出现的,目的是为了正式区分空对象指针(null)和未初始化变量的区别

注意:包含undefined的值和未定义的变量是有区别的

var val1;

console.log(val1); // undefined
console.log(val2); // 报错

对于未声明的变量,只能执行一个有用的操作:typeof

对于未声明的变量,还有一个没有实际作用的操作:delete,他不会报错,但他也没什么用

typeof val3 // val3从未声明过,输出undefined
let ud;
typeof ud; // 声明但是未初始化:undefined
typeof udd; // 未声明:undefined

上述代码中,严格来说ud和udd存在根本上的差异,但是逻辑上来说,这是正确的,因为它们都进行无法实际操作

注意:应该尽量避免声明变量但不初始化,因为这样,使用typeof就可以明确知道一个值是未声明,而不是无法确定其真实情况

undefined是一个假值,所以检测它是一定要明确是字面量undefined,因为可能会受到其他假值的影响

Null类型

Null类型同样只有一个值,即null,null表示一个空对象指针,所以通过typeof检测时会返回object,具体的,js是通过值的符号位,即前三位来判断类型的,object的符号位是000,null值在内存上的体现是全0,自然就会返回object

在创建一个变量指向对象的引用时,初始化时如果没有明确定义,应该给它初始化为null,这样就可以知道这个变量是否在后来被重新赋予一个对象的引用

undefined是由null派生而来的,因此ECMA-262将它们定义为表面上相等,此时的==操作符不会发生隐式类型转换,这里是规范中特别定义的相等

// 等于操作符==
null == undefined; //true

null === undefined; // false,同时比较了值类型

注意:如前所述,不必显示的将变量设置为undefined,虽然null和undefined有相似之处,但是null的语义表达为空对象指针,所以当一个变量要保存对象,但是当时又没有对象实体时,应该显示的设置为null

null也是一个假值

Boolean类型

有两个字面量:true和false,注意字面量是区分大小写的

在ECMAScript中,所有其他类型都可以转换为布尔类型,使用Boolean()函数转换

转换类型如下

WXWorkCapture_16976155802200.png

在if等流控制语句中,会自动转换其值为布尔值

const value = 'Message';
if(value){
	console.log('yes');
}// 最终输出yes,因为value被自动的转换为对应的布尔值

Number类型

Number类型使用IEEE754标准表示数值和双精度浮点值

Number类型的字面量基本格式是十进制,即直接使用十进制数即可

const num = 10;

同时整数字面量格式也可以用八进制或十六进制表示

使用八进制时,整数前面必须携带一个0,后面则是0~7,如果超过这个范围,会自动忽略前缀0,输出为十进制数值

const num8_1 = 070; // 56
const num8_1 = 082; // 十进制82

注意:八进制字面量在严格模式下是无效的。会导致JavaScript引擎抛出语法错误,但是在ES6中,可以使用0o来表示八进制,这个表示方式不会在严格模式下抛出错误,严格模式下只是不能识别前缀0

使用十六进制时,前缀为0x(不区分大小写),后面格式为0~9,A-F,注意A-F不区分大小写

const num16_1 = 0xFF;
const num16_2 = 0xff;
// 两种表示方式相同

注意:使用八进制和十六进制创建的字符在数学计算中都被视为十进制

1.浮点值

定义浮点值,数值中必须包含小数点

注意:小数点后没有数值的话,会被自动转化为整数字面量,小数点前面也可以没有数值,同0.x表示相同,但是不建议这么表示

const num_float = .1 // 等同于0.1
const num_int = 1. // 等于1

上面说到小数点后没有数值的情况会自动转化为整数,不但如此,如果小数点后有且仅有0的小数也会转换为整数,这是因为存储小数是一个昂贵的操作,它需要的存储空间是整数的两倍,所以ECMAScript总是想方设法把小数转换为整数

对于非常大或非常小的数,ECMAScript支持使用科学计数法表示它,格式为一个数值后跟着一个大写或小写e,e再加上一个要乘10的多少次幂,其逻辑和数学中的科学计数法类似

let num_big = 1.2e13 // 等于12000000000000

默认情况下,ECMAScript会把小数点后至少包含6个0️⃣的浮点值转换为科学计数法表示

console.log(0.00000000123); // 打印1.23e-9

浮点值的精确度最高可达17位小数,超过这个范围的会自动进1表示,在算术运算中小数远不及整数准确,例如,0.1+0.2 != 0.3,而是0.300 000 000 000 000 04,导致这种情况出现的原因并非是ECMAScript,而是IEEE754规范的64位双精度浮点数表示中,其结构为:首位为符号位,之后11位整数位,53位小数点位,通常小数点运算都会转换为这个格式的二进制数再运算,因此会存在微小的舍入误差

console.log(1.9999999999999999) // 输出2,实践证明当小数点精确到16位会进行进1舍入

2.值的范围

最小值:5e-324,用Number.MIN_VALUE表示;

最大值:1.7976931348623157e+308,用Number.MAX_VALUE表示;

超出值表示:Infinity(正无穷)、-Infinity(负无穷)

如果计算返回Infinity或-Infinity,则这个值就不能再进一步用于任何计算,这是因为Infinity没有可用于计算的表示格式

如果要确定值是不是有限大,介于JavaScript能表示的值范围,可以使用isInfinite()方法

isFinite(Number.MAX_VALUE * 2); // false

使用Number.POSITIVE_INFINITY可以表示Infinity;Number.NEGATIVE_INFINITY可以表示-Infinity

3.NaN

意思是Not a Number,不是一个数值,用来表示本来要返回数值的操作失败了,注意,这不是一个异常

ECMAScript中用0做除数时,是会返回逻辑上的0,但是如果0、+0和-0相除时,会返回NaN

console.log(0/1); // 0 不报错,输出0
console.log(0/0): // NaN

如果被除数是0,则会返回正无穷(Infinity)和负无穷(-Infinity)

NaN独特的属性:

1)任何涉及NaN的操作都会返回NaN

console.log(NaN == NaN); // false

2) isNaN()方法

上面的比较操作结果明显是个问题,因此JavaScript提供了isNaN()方法,用于检测传入值是不是数值,并且它会尝试把传入值转换为数值格式,如true,不过任何不能转换为数值的传入值都会让它返回true

4.数值转换

JavaScript提供三种方式用于数值转换:Number()、parseInt()和parseFloat(),Number是转型函数,可以用于任何类型的值,后两者主要是用于字符串转数值

parseInt('2023年10月'); // 2023
parseFloat('1.2aa'); // 1.2
Number(true); // 1

Number()转换规则:

1.对于布尔类型,ture=>1;false => 0;

2.对于Null类型,返回0;

3.对于数值,直接返回数值

4.对于Undefined类型,返回NaN

5.对于字符串,有以下规则:

  • 空串返回0

  • 整数字符串,无论是否带符号(+、-)都会返回对应的数值,自动忽略前缀0

  • 浮点数同整数字符串相同

  • 前缀是0x的十六进制数或0o的八进制数,会返回对应的十进制数值

  • 除了以上情况,其他字符串都返回NaN

6.对象: 先调用valueOf()方法,按照上述规则转换,如果转换结果是NaN,则调用toString()方法,再按照字符串规则转换

如果是字符串转数值,且需要取得整数时优先考虑parseInt(),这是因为Number()转型函数不会识别非纯数字字符串,而parseInt()对于字符串的识别能力比较强。规则如下:

  • 忽略前缀空格,如果首字符不是数值或者+、-符号,直接返回NaN

  • 数值截止到非数值字符,意味着.也不会识别

  • 空串返回NaN,这一点和Number()差别很大

  • 首字符是数值的情况,支持八进制和十六进制解释

  • 字符串模式下,只支持解释十六进制为十进制,不支持解释八进制

  • 支持传入第二个参数指定底数,指定底数时可以忽略八进制和十六进制前缀

parseInt()处理字符串中的数值更符合逻辑

parseFloat()处理字符串的方式同parseInt()类似,规则如下:

  • 忽略前缀空格和前缀0,首字符不是数值或+、-符号返回NaN

  • 末尾截止到非数值字符或第二个小数点

  • 只支持十进制,不能传入第二个参数指定底数

String类型

String类型是用来表示零个或多个16位Unicode字符序列,表示方法有三种:

const str1 = `str`;
const str2 = 'str';
const str3 = ""

三种方法表示的意义都是相同的,但是开头和结尾必须统一

const str = 'aaa`; // 语法错误

1.字符字面量

JavaScript字符串类型有一些用于表示非打印字符和其他操作的字符字面量

WXWorkCapture_16977905291688.png

WXWorkCapture_16977905579177.png这些字符字面量可以存在在字符串的任意位置,并且会被识别为单个字符

2.字符串的特点

不可修改性,字符串或者说JavaScript原始类型的值都是不可变,一旦创建就不能修改,修改操作只是创建了其副本,修改过程会先创建一个足以容纳新值的内存空间,然后把修改值填入空间,再销毁原来的字符串空间。这也是早期浏览器拼接字符串很慢的原因

3.转换为字符串

方法一:通过几乎所有值都有的toString()方法,字符串本身也有该方法,只不过返回的是其副本,注意,undefined和null没有toString()方法

同时,toString()方法在由数值转换为字符串时,也通过传入底数,指定其表示格式

const num = 123;
num.toString(16); // 7b

方法二:String()方法可以传入一个参数,然后返回其字符串形式。规则如下:

  • 如果值有toString()方法,则调用该方法(不传此参数)并返回结果

  • 如果值是null,则返回"null"

  • 如果值是undefined,则返回"undefined"

方法三:通过用+操作符加上一个空串也可以转换为其字符串形式

4.模板字面量

模板字面量是ES6新增的定义字符串的方法,模板字面量比常规使用单双引号定义字符串的能力更强、功能更丰富

模板字面能识别空格和换行,在定义HTML模板时特别有用

特别理解,上面提到的字符串类型会有一些字符字面量来表示换行等操作符,其实模板字面量本质上和使用了这些操作符的字符没有区别,只是简化了使用操作符字符字面量,比如:

const temp1 = `a
b`;
const temp2 = 'a\nb'
console.log(temp1 === temp2); // ture;

5.字符串插值

模板字符串支持字符串插值,技术上来讲,模板字符串不是字符串,而是一种特殊的JavaScript句法表达式,只不过求值之后得到的是字符串

模板字面量在定义时立即求值并转换为字符串示例,任何插入的变量也会从它们最近的作用域中取值

插值方法:在${}嵌套一个JavaScript表达式

let str1 = 'java';
let str2 = 'script';
let oldWay = 'best language is '+str1+str2;
let newWay = `best language is ${str1}${str2}`

所有插入值都会使用toString()强制转型为字符串,而且任何JavaScript表达式都可以用于插值

console.log(`${(()=>this)()}`) // '[object Window]'

6.模板字面量标签函数

模板字面量支持定义标签函数,他可以自定义模板字面量的插值行为

标签函数本身也是一个常规函数,通过前缀到模板字面量之前来应用自定义行为

const simpleTag = function (strings, ...interpolates) {
  console.log(strings);// [ 'a is ', ',b is ', '' ]返回以插值为分隔的模板字面量字符数组
  console.log(interpolates);// 插值列表
  return "abc";
};

const a = 1;
const b = 2;
const raw = `a is ${a},b is ${b}`;
const tagValue = simpleTag`a is ${a},b is ${b}`;
console.log(raw); // a is 1,b is 2
console.log(tagValue);// abc 模板字面量行为被改变

由于参数列表是固定的,因此可以使用剩余操作符...(rest operator)将它们收集到一个数组中

对于传入标签函数的插值如果为n个,那么第一个参数所包含的字符串个数始终为n+1,所以通过这个特性,可以拼接成默认字符串

const simpleTag = function (strings, ...interpolates) {
  return (
    strings[0] +
    interpolates
      .map((item, index) => {
        return `${item}${strings[index + 1]}`;
      })
      .join("")
  );
};

const a = 1;
const b = 2;
const tagValue = simpleTag`a is ${a},b is ${b}`;
console.log(tagValue);// a is 1,b is 2

7.原始字符串

上面提到模板字面量可以直接获取原始的字面量内容(换行符等),而不是转换后的字符,如果想保持字符串转换后的表示,可以使用String.raw标签函数,这里可以理解为String.raw标签函数保持了原始字符串

console.log(`a\nb`);
//a
//b
console.log(String.raw`a\nb`);
// a\nb

// 对实际的换行符来说是不行的
// 它们不会被转换成转义序列的形式
console.log(String.raw`a
b`);
// a
// b

另外,可以通过标签函数的第一个参数,也就是字符串数组的raw属性来获取原始字符串,这个属性是一个存放原始字符串的数组

const printRaw = function (strings) {
  console.log({ strings });
  // 转义后的字符序列
  for (const str of strings) {
    console.log(str);
  }

  // 原始字符序列
  for (const str of strings.raw) {
    console.log(str);
  }
};
printRaw`\u00A9`;

Symbol类型

  • 符号是原始值

  • 符号的实例是唯一的,不可变的

  • 用途是确保对象属性使用唯一标识符,避免发生属性冲突的危险

1.基本使用:使用Symbol()方法创建,可以传入一个字符串作为符号描述,但这个参数与符号定义或标识符无关。没有构造函数,不能通过new来使用构造函数,避免创建符号包装对象,但是可以通过Object(Symbol)来使用包装对象

const foo = Symbol('foo');
const obj = {}

理解:这里描述的对象属性的唯一性指的是由于Symbol对象不具有字面量,从逻辑上来说无法从值的层面来创建相同的值,因此保证了对象属性的唯一性,从内存上来看,即使是相同的符号描述在内存中的体现也是不同的符号

const foo = Symbol('foo');
const bar = Symbol('foo');
// foo和bar虽然有相同的符号描述,但是他们指向的是内存中不同的符号
const obj = {}
obj[foo] = 1;
obj[bar] = 2;

const keyFoo = 'key';
const keyBar = 'key';

const obj2 = {}

obj[keyFoo] = 1;
obj[keyBar] = 2;
console.log(obj2[keyFoo]); // 2,由于键名相同,无法确保唯一性,无法保证变量指向的键

2.全局符号注册表

全局符号注册:Symbol.for();注册的全局符号与Symbol注册的符号不同,传入值即是键名也是描述,Symbol.for()对每个字符串键都执行幂等操作,只能传入字符串,非字符串会隐式转换

Symbol.for('foo') === Symbol.for('foo');// true
Symbol('foo') === Symbol('foo'); // false,此时传入值仅为描述

全局符号查看:Symbol.keyFor();查找全局符号注册表中是否包含符号

每一次调用都会检查全局注册表,没有就创建,有就重用,同非全局符号拥有相同描述时,也不会相同,是不同的符号

3.使用符号作为属性

凡是可以使用字符串或数字作为属性的地方,都可以使用符号

注意,Symbol 键,不会出现在循环或对象属性获取方法的结果中。这是 Symbol 的一种特性,可以用来创建私有属性或防止属性被意外覆盖。

对象字面量属性:

const s1 = Symbol('s1');

const o = {[s1]:'s1 value'};// 创建时使用

o[s1] = 's1 new value';// 属性操作符使用

// {Symbol(s1): 's1 new value'}

Object.defineProperty();

const s2 = Symbol('s2');

Object.defineProperty(o,s2,{value:'s2 value'});

{Symbol(s1): 's1 new value', Symbol(s2): 's2 value'}

Object.defineProperties();

const o  = {};

const s1 = Symbol('s1');

const s2 = Symbol('s2');

Object.defineProperties(o,{
    [s1]:{value:'s1 value'},
    [s2]:{value:'s2 value'}
})
{Symbol(s1): 's1 value', Symbol(s2): 's2 value'}

Object.getOwnPropertyNames()和Object.getOwnPropertySymbols()返回对象的属性值,不同的事前者返回的是常规属性数组,后者返回的是Symbol数组,两者的返回值互斥

Object.getOwnPropertyNames(o); // ['s3']
Object.getOwnPropertySymbols(o) // (2) [Symbol(s1), Symbol(s2)]

因为符号属性是对内存中符号的一个引用,因此直接创建并用作属性的符号不会丢失。但是这样没有显式的保存这些属性的引用,那么必须遍历所有符号属性才能获取相应的属性键

const o = {
    [Symbol('s1')]:'s1 value',
    [Symbol('s2')]:'s2 value',
}
undefined
Object.getOwnPropertySymbols(o).find((symbol)=>symbol.toString().match(/s2/))
Symbol(s2)
const s2 = Object.getOwnPropertySymbols(o).find((symbol)=>symbol.toString().match(/s2/))
undefined
s2
Symbol(s2)

4.常用内置符号的使用

ES6新增Symbol原始值的同时,还更新了一些内置的符号,这些符号通过js工厂函数字符串属性存在,用于暴露语言的内部行为。在构建类库时使用较多

class Bar {}
class Baz extends Bar {
  // @@hasInstance;
  static [Symbol.hasInstance]() {
    return false;
  }
}
const b = new Baz();
console.log(b instanceof Baz);// false;

5.@@hasInstace

一个方法,该方法决定一个构造器对象函数是否认可一个对象是它的实例,由instanceof操作符使用,instanceof操作符可以用来确定一个对象实例上是否有原型,用途:用于检测继承关系

const Baz = function () {};
const baz = new Baz();
console.log(baz instanceof Baz);// true

class Bar {}
const bar = new Bar();
console.log(bar instanceof Bar);// true

ES6中,instanceof使用Symbol.hasInstanceof函数来确定关系,调用以Symbol.hasInstanceof为键名的函数会得到相同的结果

const Baz = function () {};
const baz = new Baz();
console.log(Baz[Symbol.hasInstance](baz));

这个属性定义在Function类上,因此所有的函数和类都可以调用

instanceof操作符会在原型链上寻找这个属性,就像寻找其他属性一样,因此可以在继承的类上通过静态的方法重新定义这个函数

class Baz {
  static [Symbol.hasInstance]() {
    return false;
  }
}
const baz = new Baz();
console.log(baz instanceof Baz);// false instanceof行为被重新定义

@@iterator

一个方法,该方法返回对象默认的迭代器。由for-of使用。

for-of这样的语言结构会利用这个函数执行迭代操作。循环时,它们会调用以@@iterator为键的函数,并默认这个函数会返回一个迭代器API的对象。很多时候,返回的对象是实现迭代器API的生成器函数

const arr = [1, 2, 3, 4, 5];
arr[Symbol.iterator] = function* () {
  for (let i = 0; i < arr.length; i++) {
    yield arr[i] * 2;
  }
};
for (key of arr) {
  console.log(key);
}
// 2
// 4
// 6
// 8
// 10
// 可以修改for of的行为

@@species

一个函数值,该函数作为创建派生对象的构造函数。用于对内置类型实例方法的返回值暴露实例化派生对象的方法。

用于对内置实例方法的返回值暴露实例化派生对象的方法,用 Symbol.species 定义静态的获取器(getter)方法,可以覆盖新创建实例的原型定义

class Bar extends Array {}
class Baz extends Array {
  static get [Symbol.species]() {
    return Array;
  }
}
let bar = new Bar();
let baz = new Baz();

console.log(bar instanceof Bar);
console.log(bar instanceof Array);
bar = bar.concat("bar");
console.log(bar instanceof Bar);
console.log(bar instanceof Array);

console.log(baz instanceof Baz);
console.log(baz instanceof Array);
baz = baz.concat("baz");
console.log(baz instanceof Array);
console.log(baz instanceof Baz);

补充:@@species改变了派生对象的构造函数,举例来说,A继承B,创建A的实例a,当使用@@species改变构造函数时,使用a instanceof A不会返回true,而是false,这应用于例如继承了Array对象的派生类使用filter和map等方法时。希望它返回的只是Array而不是Array对象的派生类

@@toPrimitive

一个方法,该方法将对象转换为响应的原始值,由 ToPrimitive 抽象操作使用。很多内置操作都会尝试强制将对象转换为原始值,包括字符串、数值和未指定的原始类型

class Bar {}
const bar = new Bar();
console.log(3 - bar);// NaN
console.log("str" + bar);// str[object Object]
console.log(String(bar));// [object Object]

通过在这个实例的Symbol.toPrimitive属性上定义一个函数可以修改默认行为

class Baz {
  constructor() {
    this[Symbol.toPrimitive] = function (hint) {
      switch (hint) {
        case "number":
          return 1;
        case "string":
          return "str";
        default:
          return "defalut";
      }
    };
  }
}
const baz = new Baz();
console.log(3 - baz);// 2
console.log("str" + baz);// strdefalut
console.log(String(baz));// str

@@toStringTag

一个字符串,该字符串用于创建对象的默认字符串描述。由内置方法Object.prototype.toString()使用。内置类型已经指定了这个值,但是自定义类型需要自定义

// @@toStringTag
// 内置类型已指定
const set = new Set();
console.log(set.toString()); // [object Set]
console.log(set[Symbol.toStringTag]); // Set

// 自定义类型未指定
class Bar {}
const bar = new Bar();
console.log(bar.toString()); // [object Object]
console.log(bar[Symbol.toStringTag]); // undefined

// 自定义
class Baz {
  constructor() {
    this[Symbol.toStringTag] = "Baz";
  }
}
const baz = new Baz();
console.log(baz.toString()); // [object Baz]
console.log(baz[Symbol.toStringTag]); // Baz

Object类型

ECMAScript中对象其实就是一组数据和功能的集合。

如果不需要给构造函数传参时,可以这么做(但是不推荐):

const obj = new Object;

ECMAScript中Object也是派生其他对象的基类

每个Object实例都有以下属性和方法:

  • constructor:用于创建对象的函数

  • hasOwnProperty(propertyName):判断当前对象实例上(不是原型)是否存在指定属性。传入参数为字符串或符号

  • isPrototypeOf(object):用于判断当前对象object是否是另一个对象的原型

  • propertyIsEnumerable(propertyName):判断给定属性是否可以使用

  • toLocaleString():返回对象的字符串表示形式,该字符反应对象所在的本地化环境

  • toString():返回对象的字符串表示形式

  • valueOf():返回对象的字符串、数值、布尔值表示,通常与toString()返回值相同

1

评论区