You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
letleo=Symbol('hello');alert(leo);// Uncaught TypeError: Cannot convert a Symbol value to a stringString(leo);// "Symbol(hello)"leo.toString();// "Symbol(hello)"
letid=Symbol("id");letuser={name: "Leo",age: 30,[id]: 123};for(letkeyinuser)console.log(key);// name, age (no symbols)// 使用 Symbol 任务直接访问console.log("Direct: "+user[id]);
前言
在《初中级前端 JavaScript 自测清单 - 1》部分中,和大家简单过了一遍 JavaScript 基础知识,没看过的朋友可以回顾一下😁
本系列文章是我在我们团队内部的“现代 JavaScript 突击队”,第一期学习内容为《现代 JavaScript 教程》系列的第二部分输出内容,希望这份自测清单,能够帮助大家巩固知识,温故知新。
本部分内容,以 JavaScript 对象为主,大致包括以下内容:
一、对象
JavaScript 有八种数据额类型,有七种原始类型,它们值只包含一种类型(字符串,数字或其他),而对象是用来保存键值对和更复杂实体。
我们可以通过使用带有可选属性列表的花括号
**{...}**
来创建对象,一个属性就是一个键值对{"key" : "value"}
,其中键(key
)是一个字符串(或称属性名),值(value
)可以是任何类型。1. 创建对象
我们可以使用 2 种方式来创建一个新对象:
2. 对象文本和属性
创建对象时,可以初始化对象的一些属性:
然后可以对该对象进行属性对增删改查操作:
3. 方括号的使用
当然对象的键(
key
)也可以是多词属性,但必须加引号,使用的时候,必须使用方括号([]
)读取:我们也可以在方括号中使用变量,来获取属性值:
4. 计算属性
创建对象时,可以在对象字面量中使用方括号,即 计算属性 :
当然,计算属性也可以是表达式:
5. 属性名简写
实际开发中,可以将相同的属性名和属性值简写成更短的语法:
也可以混用:
6. 对象属性存在性检测
6.1 使用 in 关键字
该方法可以判断对象的自有属性和继承来的属性是否存在。
6.2使用对象的 hasOwnProperty() 方法。
该方法只能判断自有属性是否存在,对于继承属性会返回
false
。6.3 用 undefined 判断
该方法可以判断对象的自有属性和继承属性。
该方法存在一个问题,如果属性的值就是
undefined
的话,该方法不能返回想要的结果:6.4 在条件语句中直接判断
7. 对象循环遍历
当我们需要遍历对象中每一个属性,可以使用
for...in
语句来实现7.1 for...in 循环
for...in
语句以任意顺序遍历一个对象的除Symbol
以外的可枚举属性。注意 :
for...in
不应该应用在一个数组,其中索引顺序很重要。7.2 ES7 新增方法
ES7中新增加的
Object.values()
和Object.entries()
与之前的Object.keys()
类似,返回数组类型。1. Object.keys()
返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历属性的健名。
2. Object.values()
返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历属性的键值。
如果参数不是对象,则返回空数组:
3. Object.entries()
返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历属性的键值对数组。
手动实现
Object.entries()
方法:4. Object.getOwnPropertyNames(Obj)
该方法返回一个数组,它包含了对象
Obj
所有拥有的属性(无论是否可枚举)的名称。二、对象拷贝
参考文章《搞不懂JS中赋值·浅拷贝·深拷贝的请看这里》
1. 赋值操作
首先回顾下基本数据类型和引用数据类型:
概念:基本类型值在内存中占据固定大小,保存在
栈内存
中(不包含闭包
中的变量)。常见包括:undefined,null,Boolean,String,Number,Symbol
概念:引用类型的值是对象,保存在
堆内存
中。而栈内存存储的是对象的变量标识符以及对象在堆内存中的存储地址(引用),引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。常见包括:Object,Array,Date,Function,RegExp等
1.1 基本数据类型赋值
在栈内存中的数据发生数据变化的时候,系统会自动为新的变量分配一个新的之值在栈内存中,两个变量相互独立,互不影响的。
1.2 引用数据类型赋值
在 JavaScript 中,变量不存储对象本身,而是存储其“内存中的地址”,换句话说就是存储对其的“引用”。
如下面
leo
变量只是保存对user
对象对应引用:其他变量也可以引用
user
对象:但是由于变量保存的是引用,所以当我们修改变量
leo
\leo1
\leo2
这些值时,也会改动到引用对象user
,但当user
修改,则其他引用该对象的变量,值都会发生变化:这个过程中涉及变量地址指针指向问题,这里暂时不展开讨论,有兴趣的朋友可以网上查阅相关资料。
2. 对象比较
当两个变量引用同一个对象时,它们无论是
==
还是===
都会返回true
。但如果两个变量是空对象
{}
,则不相等:3. 浅拷贝
3.1 概念
概念:新的对象复制已有对象中非对象属性的值和对象属性的引用。也可以理解为:一个新的对象直接拷贝已存在的对象的对象属性的引用,即浅拷贝。
浅拷贝只对第一层属性进行了拷贝,当第一层的属性值是基本数据类型时,新的对象和原对象互不影响,但是如果第一层的属性值是复杂数据类型,那么新对象和原对象的属性值其指向的是同一块内存地址。
通过示例代码演示没有使用浅拷贝场景:
从上面示例代码可以看出:
由于对象被直接拷贝,相当于拷贝 引用数据类型 ,所以在新对象修改任何值时,都会改动到源数据。
接下来实现浅拷贝,对比以下。
3.2 实现浅拷贝
1. Object.assign()
语法:
Object.assign(target, ...sources)
ES6中拷贝对象的方法,接受的第一个参数是拷贝的目标target,剩下的参数是拷贝的源对象sources(可以是多个)。
详细介绍,可以阅读文档《MDN Object.assign》。
从打印结果可以看出,浅拷贝只是在根属性(对象的第一层级)创建了一个新的对象,但是对于属性的值是对象的话只会拷贝一份相同的内存地址。
Object.assign()
使用注意:Symbol
值的属性,可以被Object.assign拷贝;undefined
和null
无法转成对象,它们不能作为Object.assign
参数,但是可以作为源对象。2. Array.prototype.slice()
语法:
arr.slice([begin[, end]])
slice()
方法返回一个新的数组对象,这一对象是一个由begin
和end
决定的原数组的浅拷贝(包括begin
,不包括end
)。原始数组不会被改变。详细介绍,可以阅读文档《MDN Array slice》。
3. Array.prototype.concat()
语法:
var new_array = old_array.concat(value1[, value2[, ...[, valueN]]])
concat()
方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。详细介绍,可以阅读文档《MDN Array concat》。
Array.prototype.concat
也是一个浅拷贝,只是在根属性(对象的第一层级)创建了一个新的对象,但是对于属性的值是对象的话只会拷贝一份相同的内存地址。4. 拓展运算符(...)
语法:
var cloneObj = { ...obj };
扩展运算符也是浅拷贝,对于值是对象的属性无法完全拷贝成2个不同对象,但是如果属性都是基本类型的值的话,使用扩展运算符也是优势方便的地方。
3.3 手写浅拷贝
实现原理:新的对象复制已有对象中非对象属性的值和对象属性的引用,也就是说对象属性并不复制到内存。
for...in语句以任意顺序遍历一个对象自有的、继承的、
可枚举的
、非Symbol的属性。对于每个不同的属性,语句都会被执行。该函数返回值为布尔值,所有继承了 Object 的对象都会继承到
hasOwnProperty
方法,和in
运算符不同,该函数会忽略掉那些从原型链上继承到的属性和自身属性。语法:
obj.hasOwnProperty(prop)
prop
是要检测的属性字符串名称或者Symbol
。4. 深拷贝
4.1 概念
复制变量值,对于引用数据,则递归至基本类型后,再复制。深拷贝后的对象与原来的对象完全隔离,互不影响,对一个对象的修改并不会影响另一个对象。
4.2 实现深拷贝
1. JSON.parse(JSON.stringify())
其原理是把一个对象序列化成为一个JSON字符串,将对象的内容转换成字符串的形式再保存在磁盘上,再用
JSON.parse()
反序列化将JSON字符串变成一个新的对象。JSON.stringify()
使用注意:undefined
,symbol
则经过JSON.stringify()
`序列化后的JSON字符串中这个键值对会消失;Date
引用类型会变成字符串;RegExp
引用类型会变成空对象;NaN
、Infinity
和-Infinity
,则序列化的结果会变成null
;obj[key] = obj
)。2. 第三方库
4.3 手写深拷贝
核心思想是递归,遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。 实现代码:
该方法缺陷: 遇到循环引用,会陷入一个循环的递归过程,从而导致爆栈。
其他写法,可以阅读《如何写出一个惊艳面试官的深拷贝?》 。
5. 小结
浅拷贝:将对象的每个属性进行依次复制,但是当对象的属性值是引用类型时,实质复制的是其引用,当引用指向的值改变时也会跟着变化。
深拷贝:复制变量值,对于引用数据,则递归至基本类型后,再复制。深拷贝后的对象与原来的对象完全隔离,互不影响,对一个对象的修改并不会影响另一个对象。
深拷贝和浅拷贝是针对复杂数据类型来说的,浅拷贝只拷贝一层,而深拷贝是层层拷贝。
三、垃圾回收机制(GC)
垃圾回收(Garbage Collection,缩写为GC)是一种自动的存储器管理机制。当某个程序占用的一部分内存空间不再被这个程序访问时,这个程序会借助垃圾回收算法向操作系统归还这部分内存空间。垃圾回收器可以减轻程序员的负担,也减少程序中的错误。垃圾回收最早起源于LISP语言。
目前许多语言如Smalltalk、Java、C#和D语言都支持垃圾回收器,我们熟知的 JavaScript 具有自动垃圾回收机制。
在 JavaScript 中,原始类型的数据被分配到栈空间中,引用类型的数据会被分配到堆空间中。
1. 栈空间中的垃圾回收
当函数
showName
调用完成后,通过下移 ESP(Extended Stack Pointer)指针,来销毁showName
函数,之后调用其他函数时,将覆盖掉旧内存,存放另一个函数的执行上下文,实现垃圾回收。图片来自《浏览器工作原理与实践》
2. 堆空间中的垃圾回收
堆中数据垃圾回收策略的基础是:代际假说(The Generational Hypothesis)。即:
这两个特点不仅仅适用于 JavaScript,同样适用于大多数的动态语言,如 Java、Python 等。
V8 引擎将堆空间分为新生代(存放生存时间短的对象)和老生代(存放生存时间长的对象)两个区域,并使用不同的垃圾回收器。
不管是哪种垃圾回收器,都使用相同垃圾回收流程:标记活动对象和非活动对象,回收非活动对象的内存,最后内存整理。
**
1.1 副垃圾回收器
使用 Scavenge 算法处理,将新生代空间对半分为两个区域,一个对象区域,一个空闲区域。
图片来自《浏览器工作原理与实践》
执行流程:
当然,这也存在一些问题:若复制操作的数据较大则影响清理效率。
JavaScript 引擎的解决方式是:将新生代区域设置得比较小,并采用对象晋升策略(经过两次回收仍存活的对象,会被移动到老生区),避免因为新生代区域较小引起存活对象装满整个区域的问题。
1.2 主垃圾回收器
分为:标记 - 清除(Mark-Sweep)算法,和标记 - 整理(Mark-Compact)算法。
a)标记 - 清除(Mark-Sweep)算法
过程:
图片来自《浏览器工作原理与实践》
b)标记 - 整理(Mark-Compact)算法
过程:
图片来自《浏览器工作原理与实践》
3. 拓展阅读
1.《图解Java 垃圾回收机制》
2.《MDN 内存管理》
四、对象方法和 this
1. 对象方法
具体介绍可阅读 《MDN 方法的定义》 。
将作为对象属性的方法称为“对象方法”,如下面
user
对象的say
方法:也可以使用更加简洁的方法:
当然对象方法的名称,还支持计算的属性名称作为方法名:
另外需要注意的是:所有方法定义不是构造函数,如果您尝试实例化它们,将抛出
TypeError
。2. this
2.1 this 简介
当对象方法需要使用对象中的属性,可以使用
this
关键字:当代码
user.say()
执行过程中,this
指的是user
对象。当然也可以直接使用变量名user
来引用say()
方法:但是这样并不安全,因为
user
对象可能赋值给另外一个变量,并且将其他值赋值给user
对象,就可能导致报错:但将
user.name
改成this.name
代码便正常运行。2.2 this 取值
this
的值是在 代码运行时计算出来 的,它的值取决于代码上下文:规则:如果
obj.fun()
被调用,则this
在fun
函数调用期间是obj
,所以上面的this
先是user
,然后是admin
。但是在全局环境中,无论是否开启严格模式,
this
都指向全局对象2.3 箭头函数没有自己的 this
箭头函数比较特别,没有自己的
this
,如果有引用this
的话,则指向外部正常函数,下面例子中,this
指向user.say()
方法:2.4 call / apply / bind
详细可以阅读《js基础-关于call,apply,bind的一切》 。
当我们想把
this
值绑定到另一个环境中,就可以使用call
/apply
/bind
方法实现:注意:这里的
var name = 'pingan';
需要使用var
来声明,使用let
的话,window
上将没有name
变量。三者语法如下:
五、构造函数和 new 运算符
1. 构造函数
构造函数的作用在于 实现可重用的对象创建代码 。
通常,对于构造函数有两个约定:
new
运算符执行。new
运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。语法如下:
参数如下:
constructor
一个指定对象实例的类型的类或函数。arguments
一个用于被constructor
调用的参数列表。2. 简单示例
举个简单示例:
3. new 运算符操作过程
当一个函数被使用
new
运算符执行时,它按照以下步骤:this
。this
,为其添加新的属性。this
的值。以前面
User
方法为例:当我们执行
new User('leo')
时,发生以下事情:User.prototype
的新对象被创建;User
,并将this
绑定到新创建的对象;new
表达式的结果。如果构造函数没有显式返回一个对象,则使用步骤1创建的对象。需要注意:
new User
等同于new User()
,只是没有指定参数列表,即User
不带参数的情况;new
运算符运行。4. 构造函数中的方法
在构造函数中,也可以将方法绑定到
this
上:六、可选链 "?."
详细介绍可以查看 《MDN 可选链操作符》 。
1. 背景介绍
在实际开发中,常常出现下面几种报错情况:
在可选链
?.
出现之前,我们会使用短路操作&&
运算符来解决该问题:这种写法的缺点就是 太麻烦了 。
2. 可选链介绍
可选链
?.
是一种 访问嵌套对象属性的防错误方法 。即使中间的属性不存在,也不会出现错误。如果可选链
?.
前面部分是undefined
或者null
,它会停止运算并返回undefined
。语法:
**
我们改造前面示例代码:
3. 使用注意
可选链虽然好用,但需要注意以下几点:
我们应该只将
?.
使用在一些属性或方法可以不存在的地方,以上面示例代码为例:这样写会更好,因为
leo
对象是必须存在,而name
属性则可能不存在。?.
之前的变量必须已声明;在可选链
?.
之前的变量必须使用let/const/var
声明,否则会报错:4. 其他情况:?.() 和 ?.[]
需要说明的是
?.
是一个特殊的语法结构,而不是一个运算符,它还可以与其()
和[]
一起使用:4.1 可选链与函数调用 ?.()
?.()
用于调用一个可能不存在的函数,比如:?.()
会检查它左边的部分:如果 admin 函数存在,那么就调用运行它(对于user1
)。否则(对于user2
)运算停止,没有错误。4.2 可选链和表达式 ?.[]
?.[]
允许从一个可能不存在的对象上安全地读取属性。5. 可选链
?.
语法总结可选链
?.
语法有三种形式:obj?.prop
—— 如果obj
存在则返回obj.prop
,否则返回undefined
。obj?.[prop]
—— 如果obj
存在则返回obj[prop]
,否则返回undefined
。obj?.method()
—— 如果obj
存在则调用obj.method()
,否则返回undefined
。正如我们所看到的,这些语法形式用起来都很简单直接。
?.
检查左边部分是否为null/undefined
,如果不是则继续运算。?.
链使我们能够安全地访问嵌套属性。七、Symbol
规范规定,JavaScript 中对象的属性只能为 字符串类型 或者 Symbol类型 ,毕竟我们也只见过这两种类型。
1. 概念介绍
ES6引入
Symbol
作为一种新的原始数据类型,表示独一无二的值,主要是为了防止属性名冲突。ES6之后,JavaScript一共有其中数据类型:
Symbol
、undefined
、null
、Boolean
、String
、Number
、Object
。简单使用:
Symbol 支持传入参数作为 Symbol 名,方便代码调试:
**
2. 注意事项**
Symbol
函数不能用new
,会报错。由于
Symbol
是一个原始类型,不是对象,所以不能添加属性,它是类似于字符串的数据类型。Symbol
都是不相等的,即使参数相同。Symbol
不能与其他类型的值计算,会报错。Symbol
不能自动转换为字符串,只能显式转换。Symbol
可以转换为布尔值,但不能转为数值:Symbol
属性不参与for...in/of
循环。3. 字面量中使用 Symbol 作为属性名
在对象字面量中使用
Symbol
作为属性名时,需要使用 方括号 ([]
),如[leo]: "leo"
。好处:防止同名属性,还有防止键被改写或覆盖。
需要注意 :Symbol作为对象属性名时,不能用点运算符,并且必须放在方括号内。
常常还用于创建一组常量,保证所有值不相等:
4. 应用:消除魔术字符串
魔术字符串:指代码中多次出现,强耦合的字符串或数值,应该避免,而使用含义清晰的变量代替。
常使用变量,消除魔术字符串:
使用
Symbol
消除强耦合,使得不需关系具体的值:5. 属性名遍历
Symbol作为属性名遍历,不出现在
for...in
、for...of
循环,也不被Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
返回。Object.getOwnPropertySymbols
方法返回一个数组,包含当前对象所有用做属性名的Symbol值。另外可以使用
Reflect.ownKeys
方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。由于Symbol值作为名称的属性不被常规方法遍历获取,因此常用于定义对象的一些非私有,且内部使用的方法。
6. Symbol.for()、Symbol.keyFor()
6.1 Symbol.for()
用于重复使用一个Symbol值,接收一个字符串作为参数,若存在用此参数作为名称的Symbol值,返回这个Symbol,否则新建并返回以这个参数为名称的Symbol值。
Symbol()
和Symbol.for()
区别:6.2 Symbol.keyFor()
用于返回一个已使用的Symbol类型的key:
7. 内置的Symbol值
ES6提供11个内置的Symbol值,指向语言内部使用的方法:
7.1 Symbol.hasInstance
当其他对象使用
instanceof
运算符,判断是否为该对象的实例时,会调用这个方法。比如,foo instanceof Foo
在语言内部,实际调用的是Foo[Symbol.hasInstance](foo)
。P是一个类,new P()会返回一个实例,该实例的
Symbol.hasInstance
方法,会在进行instanceof
运算时自动调用,判断左侧的运算子是否为Array
的实例。7.2 Symbol.isConcatSpreadable
值为布尔值,表示该对象用于
Array.prototype.concat()
时,是否可以展开。7.3 Symbol.species
指向一个构造函数,在创建衍生对象时会使用,使用时需要用
get
取值器。解决下面问题:
7.4 Symbol.match
当执行
str.match(myObject)
,传入的属性存在时会调用,并返回该方法的返回值。7.5 Symbol.replace
当该对象被
String.prototype.replace
方法调用时,会返回该方法的返回值。7.6 Symbol.hasInstance
当该对象被
String.prototype.search
方法调用时,会返回该方法的返回值。7.7 Symbol.split
当该对象被
String.prototype.split
方法调用时,会返回该方法的返回值。7.8 Symbol.iterator
对象进行
for...of
循环时,会调用Symbol.iterator
方法,返回该对象的默认遍历器。7.9.Symbol.toPrimitive
该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。调用时,需要接收一个字符串参数,表示当前运算模式,运算模式有:
7.10 Symbol.toStringTag
在该对象上面调用
Object.prototype.toString
方法时,如果这个属性存在,它的返回值会出现在toString
方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制[object Object
]或[object Array]
中object
后面的那个字符串。7.11 Symbol.unscopables
该对象指定了使用with关键字时,哪些属性会被with环境排除。
上面代码通过指定
Symbol.unscopables
属性,使得with
语法块不会在当前作用域寻找foo
属性,即foo
将指向外层作用域的变量。八、原始值转换
前面复习到字符串、数值、布尔值等的转换,但是没有讲到对象的转换规则,这部分就一起看看:。
需要记住几个规则:
true
,并且不存在转换为布尔值的操作,只有字符串和数值转换有。Date
对象可以相减,如date1 - date2
结果为两个时间的差值。alert(obj)
这种形式。当然我们可以使用特殊的对象方法,对字符串和数值转换进行微调。下面介绍三个类型(hint)转换情况:
1. object to string
对象到字符串的转换,当我们对期望一个字符串的对象执行操作时,如 “alert”:
2. object to number
对象到数字的转换,例如当我们进行数学运算时:
3. object to default
少数情况下,当运算符“不确定”期望值类型时。
例如,二进制加法
+
可用于字符串(连接),也可以用于数字(相加),所以字符串和数字这两种类型都可以。因此,当二元加法得到对象类型的参数时,它将依据"default"
来对其进行转换。此外,如果对象被用于与字符串、数字或 symbol 进行
==
比较,这时到底应该进行哪种转换也不是很明确,因此使用"default"
。4. 类型转换算法
为了进行转换,JavaScript 尝试查找并调用三个对象方法:
obj[Symbol.toPrimitive](hint)
—— 带有 symbol 键Symbol.toPrimitive
(系统 symbol)的方法,如果这个方法存在的话,"string"
—— 尝试obj.toString()
和obj.valueOf()
,无论哪个存在。"number"
或"default"
—— 尝试obj.valueOf()
和obj.toString()
,无论哪个存在。5. Symbol.toPrimitive
详细介绍可阅读《MDN | Symbol.toPrimitive》 。
Symbol.toPrimitive
是一个内置的 Symbol 值,它是作为对象的函数值属性存在的,当一个对象转换为对应的原始值时,会调用此函数。简单示例介绍:
6. toString/valueOf
toString
/valueOf
是两个比较早期的实现转换的方法。当没有Symbol.toPrimitive
,那么 JavaScript 将尝试找到它们,并且按照下面的顺序进行尝试:toString -> valueOf
。valueOf -> toString
。这两个方法必须返回一个原始值。如果
toString
或valueOf
返回了一个对象,那么返回值会被忽略。默认情况下,普通对象具有toString
和valueOf
方法:toString
方法返回一个字符串"[object Object]"
。valueOf
方法返回对象自身。简单示例介绍:
我们也可以结合
toString
/valueOf
实现前面第 5 点介绍的user
对象:总结
本文作为《初中级前端 JavaScript 自测清单》第二部分,介绍的内容以 JavaScript 对象为主,其中有让我眼前一亮的知识点,如
Symbol.toPrimitive
方法。我也希望这个清单能帮助大家自测自己的 JavaScript 水平并查缺补漏,温故知新。The text was updated successfully, but these errors were encountered: