数据类型
内置类型
JavaScript
中分为七种内置类型,七种内置类型又分为两大类:基本类型和对象(Object
)。- 基本类型有七种:
null
、undefined
、boolean
、number
、string
、symbol
、bigint
。 - 其中
JavaScript
的数字类型是浮点类型,没有整形。并且浮点类型基于IEEE 754
标准实现,在使用中会遇到某些Bug。NaN
也属于number
类型,并且NaN
不等于自身。 - 对于基本类型来说,如果使用字面量的方式,那么这个变量只是个字面量,只有在必要的时候才会转换为对应的类型。
引用数据类型:
对象
Object
(包含普通对象-Object
,数组对象-Array
,正则对象-RepExp
,日期对象-Date
,数学函数-Math
,函数对象-Function
)1
2let a = 111 // 这只是字面量,不是 number 类型
a.toString() // 使用时候才会转换为对象类型对象(
Object
)是引用类型,在使用过程中会遇到浅拷贝
和深拷贝
的问题。1
2
3
4let a = { name: 'FE' }
let b = a
b.name = 'EF'
console.log(a.name) // EF说出下面运行的结果,解释原因。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function test(person) {
person.age = 26
person = {
name: 'hzj',
age: 18
}
return person
}
const p1 = {
name: 'fyq',
age: 19
}
const p2 = test(p1)
console.log(p1) // -> ? -> p1:{name: “fyq”, age: 26}
console.log(p2) // -> ? -> p2:{name: “hzj”, age: 18}原因: 在函数传参的时候传递的是对象在堆中的内存地址值,test函数中的实参person是p1对象的内存地址,通过调用person.age = 26确实改变了p1的值,但随后person变成了另一块内存空间的地址,并且在最后将这另外一份内存空间的地址返回,赋给了p2。
null和undefined区别
Undefined类型只有一个值,即undefined。当声明的变量还未被
初始化
时,变量的默认值为undefined。用法
- 变量被声明了,但没有赋值时,就等于
undefined
。 - 调用函数时,应该提供的参数没有提供,该参数等于
undefined
。 - 对象没有赋值的属性,该属性的值为
undefined
。 - 函数没有返回值时,默认返回
undefined
Null类型也只有一个值,即null。null用来表示尚
未存在
的对象,常用来表示函数企图返回一个不存在的对象。用法
- 作为函数的参数,表示该函数的参数不是对象。
- 作为对象原型链的终点
null是对象吗?为什么?
null不是对象。
解释: 虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象然而 null 表示为全零,所以将它错误的判断为 object 。
‘1’.toString()为什么可以调用?
其实在这个语句运行的过程中做了这样几件事情:
1 | var s = new Object('1'); |
- 第一步: 创建Object类实例。注意为什么不是String ? 由于Symbol和BigInt的出现,对它们调用new都会报错,目前ES6规范也不建议用new来创建基本类型的包装- 类。
- 第二步: 调用实例方法。
- 第三步: 执行完方法立即销毁这个实例。
整个过程体现了基本包装类型的性质,而基本包装类型恰恰属于基本数据类型,包括Boolean, Number和String。
0.1+0.2为什么不等于0.3?
0.1和0.2在转换成二进制后会无限循环,由于标准位数的限制后面多余的位数会被截掉,此时就已经出现了精度的损失,相加后因浮点数小数位的限制而截断的二进制数字在转换为十进制就会变成0.30000000000000004
BigInt
什么是BigInt?
BigInt
是一种新的数据类型,用于当整数值大于Number
数据类型支持的范围时。这种数据类型允许我们安全地对大整数执行算术操作,表示高分辨率的时间戳,使用大整数id,等等,而不需要使用库。
为什么需要BigInt?
在JS中,所有的数字都以双精度64位浮点格式表示,那这会带来什么问题呢?
这导致JS中的Number无法精确表示非常大的整数,它会将非常大的整数四舍五入,确切地说,JS中的Number类型只能安全地表示-9007199254740991(-(253-1))和9007199254740991((253-1)),任何超出此范围的整数值都可能失去精度。
1 | console.log(999999999999999); //=>10000000000000000 |
要创建BigInt
,只需要在数字末尾追加n即可
1 | console.log( 9007199254740995n ); // → 9007199254740995n |
另一种创建BigInt
的方法是用BigInt()
构造函数
1 | BigInt("9007199254740995"); // → 9007199254740995n |
值得警惕的点
BigInt不支持一元加号运算符, 这可能是某些程序可能依赖于 + 始终生成
Number
的不变量,或者抛出异常。另外,更改 + 的行为也会破坏 asm.js 代码。
因为隐式类型转换可能丢失信息,所以不允许在 Bigint
和 Number
之间进行混合操作。当混合使用大整数和浮点数时,结果值可能无法由BigInt
或Number
精确表示。
1 | 10 + 10n; // → TypeError |
不能将
BigInt
传递给Web api
和内置的JS
函数,这些函数需要一个Number
类型的数字。尝试这样做会报TypeError
错误。
1 | Math.max(2n, 4n, 6n); // → TypeError |
当
Boolean
类型与BigInt
类型相遇时,BigInt
的处理方式与Number
类似,换句话说,只要不是0n
,BigInt
就被视为truthy
的值。
1 | if(0n) {//条件判断为false |
- 元素都为
BigInt
的数组可以进行sort
。 BigInt
可以正常地进行位运算,如|
、&
、<<
、>>
和^
类型检测
typeof
在写业务逻辑的时候,经常要用到JS数据类型的判断,面试常见的案例深浅拷贝也要用到数据类型的判断。
typeof
- 优点:能够快速区分基本数据类型
- 缺点:不能将
Object
、Array
和Null
区分,都返回object
1 | console.log(typeof 2); // number |
instenceof
- 优点:能够区分
Array
、Object
和Function
,适合用于判断自定义的类实例对象 - 缺点:
Number
,Boolean
,String
基本数据类型不能判断
1 | console.log(2 instanceof Number); // false |
Object.prototype.toString.call()
- 优点:精准判断数据类型
- 缺点:写法繁琐不容易记,推荐进行封装后使用
1 | const toString = Object.prototype.toString; |
判断是否是promise对象
1 | function isPromise (val) { |
typeof 于 instanceof 区别
- typeof 对于基本类型,除了 null都可以显示正确的类型
- typeof 对于对象,除了函数都会显示 object
- 对于 null 来说,虽然它是基本类型,但是会显示 object,这是一个存在很久了的 Bug
- instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 iprototype
Object.is和===的区别
Object在严格等于的基础上修复了一些特殊情况下的失误,具体来说就是+0和-0,NaN和NaN。
1 | // ES6 中提供了新的 Object.is() 方法,它具有 === 的一些特点,而且更好、更精确,在一些特殊案例中表现的很好 |
类型总结
typeof
- 直接在计算机底层基于数据类型的值(二进制)进行检测
typeof null
为object
原因是对象存在在计算机中,都是以000开始的二进制存储,所以检测出来的结果是对象typeof
普通对象/数组对象/正则对象/日期对象 都是object
typeof NaN === 'number'
instanceof
- 检测当前实例是否属于这个类的
- 底层机制:只要当前类出现在实例的原型上,结果都是
true
- 不能检测基本数据类型
constructor
- 支持基本类型
constructor
可以随便改,也不准
Object.prototype.toString.call([val])
- 返回当前实例所属类信息
判断
Target
的类型,单单用typeof
并无法完全满足,这其实并不是bug
,本质原因是JS
的万物皆对象的理论。因此要真正完美判断时,我们需要区分对待:
- 基本类型(
null
): 使用String(null)
- 基本类型(
string
/number
/boolean
/undefined
) +function
: - 直接使用typeof
即可 - 其余引用类型(
Array
/Date
/RegExp Error
): 调用toString
后根据[object XXX]
进行判断
1 | 很稳很实用的判断封装: |
类型转换
JS
中在使用运算符号或者对比符时,会自带隐式转换,规则如下:
转化规则
-
、*
、/
、%
:一律转换成数值后计算+
:- 数字 + 字符串 = 字符串, 运算顺序是从左到右
- 数字 + 对象, 优先调用对象的
valueOf -> toString
- 数字 +
boolean/null
-> 数字 - 数字 +
undefined -> NaN
[1].toString() === '1'
{}.toString() === '[object object]'
NaN !== NaN
、+undefined
为NaN
在 JS 中类型转换只有三种情况,分别是:
- 转换为布尔值
- 转换为数字
- 转换为字符串
Boolean
在条件判断时,除了
undefined
,null
,false
,NaN
,''
,0
,-0
,其他所有值都转为true
,包括所有对象
对象转原始类型是根据什么流程运行的
对象转原始类型,会调用内置的[ToPrimitive]函数,对于该函数而言,其逻辑如下:
- 如果有
Symbol.toPrimitive()
方法,优先调用再返回 - 调用
valueOf()
,如果转换为原始类型,则返回 - 调用
toString()
,如果转换为原始类型,则返回 - 如果都没有返回原始类型,会报错
1 | const obj = { |
如何让if(a == 1 && a == 2)条件成立
其实就是上一个问题的应用
1 | const a = { |
四则运算符
它有以下几个特点:
- 运算中其中一方为字符串,那么就会把另一方也转换为字符串
- 如果一方不是字符串或者数字,那么会将它转换为数字或者字符串
1
2
31 + '1' // '11'
true + true // 2
4 + [1,2,3] // "41,2,3"- 对于第一行代码来说,触发特点一,所以将数字 1 转换为字符串,得到结果 ‘11’
- 对于第二行代码来说,触发特点二,所以将
true
转为数字 1 - 对于第三行代码来说,触发特点二,所以将数组通过
toString
转为字符串1,2,3
,得到结果41,2,3
另外对于加法还需要注意这个表达式 ‘a’ + + ‘b’
1 | 'a' + + 'b' // -> "aNaN" |
- 因为 + ‘b’ 等于 NaN,所以结果为 “aNaN”,你可能也会在一些代码中看到过 + '1’的形式来快速获取 number 类型。
- 那么对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字
1 | 4 * '3' // 12 |
比较运算符
- 如果是对象,就通过 toPrimitive 转换对象
- 如果是字符串,就通过 unicode 字符索引来比较
1 | let a = { |
[] == ![]结果是什么?为什么?
==
中,左右两边都需要转换为数字然后进行比较[]
转换为数字为0
![]
首先是转换为布尔值,由于[]
作为一个引用类型转换为布尔值为true
, 因此![]
为false
,进而在转换成数字,变为0
0 == 0
, 结果为true
== 和 ===有什么区别
===
叫做严格相等,是指:左右两边不仅值要相等,类型也要相等,例如'1'===1
的结果是false
,因为一边是string
,另一边是number
==不像===那样严格,对于一般情况,只要值相等,就返回true,但==还涉及一些类型转换,它的转换规则如下
- 两边的类型是否相同,相同的话就比较值的大小,例如1==2,返回false
- 判断的是否是null和undefined,是的话就返回true
- 判断的类型是否是String和Number,是的话,把String类型转换成Number,再进行比较
- 判断其中一方是否是Boolean,是的话就把Boolean转换成Number`,再进行比较
- 如果其中一方为Object,且另一方为String、Number或者Symbol,会将Object转换成字符串,再进行比较
闭包
红宝书(p178)上对于闭包的定义:闭包是指有权访问另外一个函数作用域中的变量的函数,MDN 对闭包的定义为:闭包是指那些能够访问自由变量的函数。(其中自由变量,指在函数中使用的,但既不是函数参数arguments也不是函数的局部变量的变量,其实就是另外一个函数作用域中的变量。)
产生原因
首先要明白作用域链的概念,其实很简单,在ES5中只存在两种作用域————
全局作用域
和函数作用域
,当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不在父作用域中,这就是作用域链,值得注意的是,每一个子函数都会拷贝上级的作用域,形成一个作用域的链条。 比如:
1 | var a = 1; |
在这段代码中,f1的作用域指向有全局作用域(window)和它本身,而f2的作用域指向全局作用域(window)、f1和它本身。而且作用域是从最底层向上找,直到找到全局作用域window为止,如果全局还没有的话就会报错。就这么简单一件事情
闭包产生的本质就是,当前环境中存在指向父级作用域的引用。还是举上面的例子:
1 | function f1() { |
这里x会拿到父级作用域中的变量,输出2。因为在当前环境中,含有对f2的引用,f2恰恰引用了window、f1和f2的作用域。因此f2可以访问到f1的作用域的变量。
那是不是只有返回函数才算是产生了闭包呢?回到闭包的本质,我们只需要让父级作用域的引用存在即可,因此我们还可以这么做:
1 | var f3; |
让
f1
执行,给f3
赋值后,等于说现在f3
拥有了window
、f1
和f3
本身这几个作用域的访问权限,还是自底向上查找,最近是在f1中找到了a
,因此输出2。
在这里是外面的变量f3存在着父级作用域的引用,因此产生了闭包,形式变了,本质没有改变
表现形式
明白了本质之后,我们就来看看,在真实的场景中,究竟在哪些地方能体现闭包的存在?
- 返回一个函数。刚刚已经举例。
- 作为函数参数传递
1
2
3
4
5
6
7
8
9
10
11
12
13
14var a = 1;
function foo(){
var a = 2;
function baz(){
console.log(a);
}
bar(baz);
}
function bar(fn){
// 这就是闭包
fn();
}
// 输出2,而不是1
foo(); - 在定时器、事件监听、Ajax请求、跨窗口通信、Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包
1
2
3
4
5
6
7
8
9// 定时器
setTimeout(function timeHandler() {
console.log('111');
}, 100)
// 事件监听
$('#app').click(function(){
console.log('DOM Listener');
}) IIFE
(立即执行函数表达式)创建闭包, 保存了全局作用域window和当前函数的作用域,因此可以访问全局的变量1
2
3
4
5var a = 2;
(function IIFE() {
// 输出2
console.log(a);
})();
如何解决下面的循环输出问题
1 | for (var i = 1; i <= 5; i ++) { |
为什么会全部输出6?如何改进,让它输出1,2,3,4,5?(方法越多越好) 因为setTimeout为宏任务,由于JS中单线程eventLoop机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后setTimeout中的回调才依次执行
,但输出i的时候当前作用域没有,往上一级再找,发现了i,此时循环已经结束,i变成了6。因此会全部输出6。
解决方法:
- 利用IIFE(立即执行函数表达式)当每次for循环时,把此时的i变量传递到定时器中
1
2
3
4
5
6
7for(var i = 1;i <= 5;i++){
(function(j){
setTimeout(function timer(){
console.log(j)
}, 0)
})(i)
} - 给定时器传入第三个参数, 作为
timer
函数的第一个函数参数1
2
3
4
5for(var i=1;i<=5;i++){
setTimeout(function timer(j){
console.log(j)
}, 0, i)
} - 使用ES6中的let
1
2
3
4
5for(let i = 1; i <= 5; i++){
setTimeout(function timer(){
console.log(i)
},0)
}let
使JS
发生革命性的变化,让JS
有函数作用域变为了块级作用域,用let
后作用域链不复存在。代码的作用域以块级为单位,以上面代码为例:1
2
3
4
5
6
7
8
9
10
11
12
13
14// i = 1
{
setTimeout(function timer(){
console.log(1)
},0)
}
// i = 2
{
setTimeout(function timer(){
console.log(2)
},0)
}
// i = 3
...
原型链
原型/构造函数/实例
- 原型(
prototype
): 一个简单的对象,用于实现对象的 属性继承。可以简单的理解成对象的爹。在Firefox
和Chrome
中,每个JavaScript
对象中都包含一个__proto__
(非标准)的属性指向它爹(该对象的原型),可obj.__proto__
进行访问。 - 构造函数: 可以通过
new
来 新建一个对象 的函数。 - 实例: 通过构造函数和
new
创建出来的对象,便是实例。 实例通过__proto__
指向原型,通过constructor
指向构造函数。
以
Object
为例,我们常用的Object
便是一个构造函数,因此我们可以通过它构建实例。
则此时, 实例为instance
, 构造函数为Object
,我们知道,构造函数拥有一个prototype
的属性指向原型,因此原型为:
1 | // 实例 |
这里我们可以来看出三者的关系:
实例.__proto__ === 原型
原型.constructor === 构造函数
构造函数.prototype === 原型
实例.constructor === 构造函数
1 | // 这条线其实是是基于原型进行获取的,可以理解成一条基于原型的映射线 |
原型对象和构造函数有何关系
- 在JavaScript中,每当定义一个函数数据类型(普通函数、类)时候,都会天生自带一个prototype属性,这个属性指向函数的原型对象。
- 当函数经过new调用时,这个函数就成为了构造函数,返回一个全新的实例对象,这个实例对象有一个__proto__属性,指向构造函数的原型对象。
描述原型链
JavaScript
对象通过__proto__
指向父类对象,直到指向Object
对象为止,这样就形成了一个原型指向的链条, 即原型链
- 对象的
hasOwnProperty()
来检查对象自身中是否含有该属性 - 使用
in
检查对象中是否含有某个属性时,如果对象中没有但是原型链中有,也会返回true
继承
借助call
这样写的时候子类虽然能够拿到父类的属性值,但是问题是父类原型对象中一旦存在方法那么子类无法继承。那么引出下面的方法。
1 | function Parent1(){ |
借助原型链
1 | function Parent2() { |
this
我们先来看几个函数调用的场景
1 | function foo() { |
箭头函数中的 this
1 | function a() { |
- 首先箭头函数其实是没有
this
的,箭头函数中的this
只取决包裹箭头函数的第一个普通函数的this
。在这个例子中,因为包裹箭头函数的第一个普通函数是a
,所以此时的this
是window
。另外对箭头函数使用bind
这类函数是无效的。 - 最后种情况也就是
bind
这些改变上下文的API
了,对于这些函数来说,this
取决于第一个参数,如果第一个参数为空,那么就是window
。 - 那么说到
bind
,不知道大家是否考虑过,如果对一个函数进行多次bind
,那么上下文会是什么呢?
1 | let a = {} |
如果你认为输出结果是 a
,那么你就错了,其实我们可以把上述代码转换成另一种形式
1 | // fn.bind().bind(a) 等于 |
可以从上述代码中发现,不管我们给函数 bind
几次,fn
中的 this
永远由第一次 bind
决定,所以结果永远是 window
1 | let a = { name: 'poetries' } |
以上就是 this 的规则了,但是可能会发生多个规则同时出现的情况,这时候不同的规则之间会根据优先级最高的来决定 this 最终指向哪里。
首先,new 的方式优先级最高,接下来是 bind 这些函数,然后是 obj.foo() 这种调用方式,最后是 foo 这种调用方式,同时,箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。
总结
this执行主体,谁把它执行的和在哪创建的在哪执行的都没有必然的关系
- 函数执行,看方法前面是否有点,没有点
this
是window
(严格模式下是undefined
),有点,点前面是谁·this
·就是谁 - 给当前元素的某个事件行为绑定方法,当事件行为触发,方法中的
this
是当前元素本身(排除attachEvent
) - 构造函数体中
this
是当前类的实例 - 箭头函数中没有执行主体,所用到的
this
都是所处上下文中的this
- 可以基于
Function.prototype
上的call/apply/bind
改变this
指向
内存机制
基本数据类型用栈存储,引用数据(
对象
)类型用堆存储。闭包变量是存在堆内存中的。
具体而言,以下数据类型存储在栈中:
boolean
null
undefined
number
string
symbol
bigint
1 | let obj = { a: 1 }; |
之所以会这样,是因为 obj
和 obj1
是同一份堆空间的地址,改变 obj1
,等于改变了共同的堆内存,这时候通过 obj
来获取这块内存的值当然会改变。 当然,你可能会问: 为什么不全部用栈来保存呢?
首先,对于系统栈来说,它的功能除了保存变量之外,还有创建并切换函数执行上下文的功能。举个例子:
1 | function f(a) { |
- 假设用
ESP
指针来保存当前的执行状态,在系统栈中会产生如下的过程:
- 调用
func
, 将func
函数的上下文压栈,ESP
指向栈顶。 - 执行
func
,又调用f
函数,将f
函数的上下文压栈,ESP
指针上移。 - 执行完
f
函数,将ESP
下移,f
函数对应的栈顶空间被回收。 - 执行完
func
,ESP 下移,func
对应的空间被回收。
因此你也看到了,如果采用栈来存储相对基本类型更加复杂的对象数据,那么切换上下文的开销将变得巨大!
不过堆内存虽然空间大,能存放大量的数据,但与此同时垃圾内存的回收会带来更大的开销
执行上下文
当执行 JS
代码时,会产生三种执行上下文
- 全局执行上下文
- 函数执行上下文
eval
执行上下文
每个执行上下文中都有三个重要的属性- 变量对象(
VO
),包含变量、函数声明和函数的形参,该属性只能在全局上下文中访问 - 作用域链(
JS
采用词法作用域,也就是说变量的作用域是在定义时就决定了) this
1 | var a = 10 |
对于上述代码,执行栈中有两个上下文:全局上下文和函数 foo 上下文。
1 | b() // call b |
想必以上的输出大家肯定都已经明白了,这是因为函数和变量提升的原因。通常提升的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于大家理解。但是更准确的解释应该是:在生成执行上下文时,会有两个阶段。第一个阶段是创建的阶段(具体步骤是创建
VO
),JS
解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为undefined
,所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用。
在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升
1 | b() // call b second |
var
会产生很多错误,所以在ES6
中引入了let
。let
不能在声明前使用,但是这并不是常说的let
不会提升,let
提升了声明但没有赋值,因为临时死区导致了并不能在声明前使用。
对于非匿名的立即执行函数需要注意以下一点
1 | var foo = 1 |
因为当
JS
解释器在遇到非匿名的立即执行函数时,会创建一个辅助的特定对象,然后将函数名称作为这个对象的属性,因此函数内部才可以访问到foo
,但是这个值又是只读的,所以对它的赋值并不生效,所以打印的结果还是这个函数,并且外部的值也没有发生更改。
小结
执行上下文可以简单理解为一个对象
- 它包含三个部分:
- 变量对象(VO)
- 作用域链(词法作用域)
- this指向
- 它的类型:
- 全局执行上下文
- 函数执行上下文
eval
执行上下文
- 代码执行过程:
- 创建 全局上下文 (
global EC
) - 全局执行上下文 (
caller
) 逐行 自上而下 执行。遇到函数时,函数执行上下文 (callee
) 被push到执行栈顶层 - 函数执行上下文被激活,成为
active EC
, 开始执行函数中的代码,caller
被挂起 - 函数执行完后,
callee
被pop
移除出执行栈,控制权交还全局上下文 (caller
),继续执行
- 创建 全局上下文 (
变量提升
当执行
JS
代码时,会生成执行环境,只要代码不是写在函数中的,就是在全局执行环境中,函数中的代码会产生函数执行环境,只此两种执行环境。
1 | b() // call b |
想必以上的输出大家肯定都已经明白了,这是因为函数和变量提升的原因。通常提升的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于大家理解。但是更准确的解释应该是:在生成执行环境时,会有两个阶段。第一个阶段是创建的阶段,
JS
解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为undefined
,所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用
在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升
1 | b() // call b second |
var
会产生很多错误,所以在 ES6中引入了let
。let
不能在声明前使用,但是这并不是常说的let
不会提升,let
提升了,在第一阶段内存也已经为他开辟好了空间,但是因为这个声明的特性导致了并不能在声明前使用
模块化
模块化开发在现代开发中已是必不可少的一部分,它大大提高了项目的可维护、可拓展和可协作性。通常,我们 在浏览器中使用
ES6
的模块化支持,在Node
中使用commonjs
的模块化支持。
分类:
es6
:import
/export
commonjs
:require
/module.exports
/exports
amd
:require
/defined
require与import的区别require
支持 动态导入,import
不支持,正在提案 (babel
下可支持)require
是 同步 导入,import
属于 异步 导入require
是 值拷贝,导出值变化不会影响导入值;import
指向 内存地址,导入值会随导出值而变化
setTimeout、Promise、Async / Await 的区别
- 首先,我们先来了解一下基本概念:
js EventLoop
事件循环机制:JavaScript
的事件分两种,宏任务(macro-task
)和微任务(micro-task
)
- 宏任务:包括整体代码script,
setTimeout
,setInterval
- 微任务:Promise.then(非
new Promise
),process.nextTick
(node中) - 事件的执行顺序,是先执行宏任务,然后执行微任务,这个是基础,任务可以有同步任务和异步任务,同步的进入主线程,异步的进入Event Table并注册函数,异步事件完成后,会将回调函数放入Event Queue中(宏任务和微任务是不同的Event Queue),同步任务执行完成后,会从Event Queue中读取事件放入主线程执行,回调函数中可能还会包含不同的任务,因此会循环执行上述操作。
- 注意:
setTimeOut
并不是直接的把你的回掉函数放进上述的异步队列中去,而是在定时器的时间到了之后,把回掉函数放到执行异步队列中去。如果此时这个队列已经有很多任务了,那就排在他们的后面。这也就解释了为什么setTimeOut
为什么不能精准的执行的问题了。 setTimeout
执行需要满足两个条件:- 主进程必须是空闲的状态,如果到时间了,主进程不空闲也不会执行你的回掉函数
- 这个回掉函数需要等到插入异步队列时前面的异步函数都执行完了,才会执行
- 上面是比较官方的解释,说一下自己的理解吧:
- 了解了什么是宏任务和微任务,就好理解多了,首先执行 宏任务 => 微任务的Event Queue => 宏任务的Event Queue
promise
、async/await
- 首先,
new Promise
是同步的任务,会被放到主进程中去立即执行。而.then()函数是异步任务会放到异步队列中去,那什么时候放到异步队列中去呢?当你的promise
状态结束的时候,就会立即放进异步队列中去了。 - 带
async
关键字的函数会返回一个promise
对象,如果里面没有await
,执行起来等同于普通函数;如果没有await,async
函数并没有很厉害是不是 await
关键字要在async
关键字函数的内部,await
写在外面会报错;await
如同他的语意,就是在等待,等待右侧的表达式完成。此时的await
会让出线程,阻塞async
内后续的代码,先去执行async
外的代码。等外面的同步代码执行完毕,才会执行里面的后续代码。就算await
的不是promise
对象,是一个同步函数,也会等这样操作
- 首先,
根据图片显示我们来整理一下流程:
- 执行
console.log('script start')
,输出script start
; - 执行
setTimeout
,是一个异步动作,放入宏任务异步队列中; - 执行
async1()
,输出async1 start
,继续向下执行; - 执行
async2()
,输出async2
,并返回了一个promise
对象,await
让出了线程,把返回的promise
加入了微任务异步队列,所以async1()
下面的代码也要等待上面完成后继续执行; - 执行
new Promise
,输出promise1
,然后将resolve
放入微任务异步队列; - 执行
console.log('script end')
,输出script end
; - 到此同步的代码就都执行完成了,然后去微任务异步队列里去获取任务
- 接下来执行
resolve
(async2
返回的promise
返回的),输出了async1 end
。 - 然后执行
resolve
(new Promise
的),输出了promise2
- 最后执行
setTimeout
,输出了settimeout
async原理
async/await
语法糖就是使用Generator
函数+自动执行器来运作的
1 | // 定义了一个promise,用来模拟异步请求,作用是传入参数++ |
JS 整数
通过 Number
类型来表示,遵循 IEEE754 标准,通过 64 位来表示一个数字,(1 + 11 + 52),最大安全数字是 Math.pow(2, 53) - 1
,对于 16 位十进制。(符号位 + 指数位 + 小数部分有效位)Math.pow(2, 53)
,53 为有效数字,会发生截断,等于 JS 能支持的最大数字。
setTimeout(fn, 0)多久才执行,Event Loop
setTimeout
按照顺序放到队列里面,然后等待函数调用栈清空之后才开始执行,而这些操作进入队列的顺序,则由设定的延迟时间来决定
js脚本加载问题,async、defer问题
- 如果依赖其他脚本和
DOM
结果,使用defer
- 如果与
DOM
和其他脚本依赖不强时,使用async
script 引入方式 html
静态<script>
引入js
动态插入<script>
<script defer>
: 异步加载,元素解析完成后执行<script async>
: 异步加载,但执行时会阻塞元素渲染
垃圾回收机制
- 对于在
JavaScript
中的字符串,对象,数组是没有固定大小的,只有当对他们进行动态分配存储时,解释器就会分配内存来存储这些数据,当JavaScript
的解释器消耗完系统中所有可用的内存时,就会造成系统崩溃。 - 内存泄漏,在某些情况下,不再使用到的变量所占用内存没有及时释放,导致程序运行中,内存越占越大,极端情况下可以导致系统崩溃,服务器宕机。
JavaScript
有自己的一套垃圾回收机制,JavaScript
的解释器可以检测到什么时候程序不再使用这个对象了(数据),就会把它所占用的内存释放掉。- 针对
JavaScript
的来及回收机制有以下两种方法(常用):标记清除,引用计数 - 标记清除
几种类型的DOM节点
Document
节点,整个文档是一个文档节点;Element
节点,每个HTML
标签是一个元素节点;Attribute
节点,每一个HTML
属性是一个属性节点;Text
节点,包含在HTML
元素中的文本是文本节点
对象的创建方式
工厂模式,创建方式
1 | function createPerson(name,age,job){ |
构造函数模式
1 | function Person(name,age,ob){ |
原型模式
1 | function Person() {} |
组合使用构造函数模式和原型模式
1 | function Person(name,age){ |
动态原型模式
1 | function Person(name,age,job){ |
转化类数组成数组
因为arguments本身并不能调用数组方法,它是一个另外一种对象类型,只不过属性从0开始排,依次为0,1,2…最后还有callee和length属性。我们也把这样的对象称为类数组
常见的类数组还有:
- 用
getElementsByTagName
/ClassName()
获得的HTMLCollection
- 用
querySelector
获得的nodeList
- Array.prototype.slice.call()
1 | function sum(a, b) { |
- Array.from()
1 | function sum(a, b) { |
- ES6展开运算符
1 | function sum(a, b) { |
- concat + apply
1 | function sum(a, b) { |
中断forEach
在
forEach
中用return
不会返回,函数会继续执行。
1 | let nums = [1, 2, 3]; |
中断方法:
- 使用
try
监视代码块,在需要中断的地方抛出异常。 - 官方推荐方法(替换方法):用
every
和some
替代forEach
函数。every
在碰到return false
的时候,中止循环。some
在碰到return true
的时候,中止循环
数组中是否包含某个值
- array.indexOf
此方法判断数组中是否存在某个值,如果存在,则返回数组元素的下标,否则返回-1。
1
2
3var arr = [1,2,3,4];
var index = arr.indexOf(3);
console.log(index); - array.includes(searcElement[,fromIndex])
数组中是否存在某个值,如果存在返回true,否则返回false
1
2
3
4
5var arr = [1,2,3,4];
if(arr.includes(3))
console.log("存在");
else
console.log("不存在"); - array.find(callback[,thisArg])
返回数组中满足条件的第一个元素的值,如果没有,返回undefined
1
2
3
4
5var arr=[1,2,3,4];
var result = arr.find(item =>{
return item > 3
});
console.log(result); - array.findeIndex(callback[,thisArg])
返回数组中满足条件的第一个元素的下标,如果没有找到,返回-1
1
2
3
4
5var arr=[1,2,3,4];
var result = arr.findIndex(item =>{
return item > 3
});
console.log(result);
数组扁平化
1 | let ary = [1, [2, [3, [4, 5]]], 6];// -> [1, 2, 3, 4, 5, 6] |
需求:多维数组=>一维数组
flat
方法1
ary = ary.flat(Infinity);
replace + split
1
ary = str.replace(/(\[|\])/g, '').split(',')
replace + JSON.parse
1
2str = str.replace(/(\[|\])/g, '');
ary = JSON.parse('[' + str + ']');- 普通递归
1
2
3
4
5
6
7
8
9
10
11let result = [];
let fn = function(ary) {
for(let i = 0; i < ary.length; i++) {
let item = ary[i];
if (Array.isArray(ary[i])){
fn(item);
} else {
result.push(item);
}
}
} reduce
迭代1
2
3
4
5
6
7function flatten(ary) {
return ary.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
}, []);
}
let ary = [1, 2, [3, 4], [5, [6, 7]]]
console.log(flatten(ary))- 扩展运算符(
...
)
1 | //只要有一个元素有数组,那么循环继续 |
中浅拷贝
什么是拷贝?
首先来直观的感受一下什么是拷贝
1 | let arr = [1, 2, 3]; |
- 浅拷贝:
Object.assign()
任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象1
2
3
4
5let obj1 = { person: {name: "kobe", age: 41},sports:'basketball' };
let obj2 = Object.assign({}, obj1);
obj2.person.name = "wade";
obj2.sports = 'football'
console.log(obj1); // { person: { name: 'wade', age: 41 }, sports: 'basketball' }函数库lodash的_.clone方法
该函数库也有提供_.clone用来做 Shallow Copy,后面我们会再介绍利用这个库实现深拷贝。1
2
3
4
5
6
7
8var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.clone(obj1);
console.log(obj1.b.f === obj2.b.f);// true展开运算符(...)
ES6特性,它提供了一种非常方便的方式来执行浅拷贝, 这与Object.assign()
的功能相同1
2
3
4
5let obj1 = { name: 'Kobe', address:{x:100,y:100}}
let obj2= {... obj1}
obj1.address.x = 200;
obj1.name = 'wade'
console.log('obj2',obj2) // obj2 { name: 'Kobe', address: { x: 200, y: 100 } }Array.prototype.concat()
1
2
3
4
5
6let arr = [1, 3, {
username: 'kobe'
}];
let arr2 = arr.concat();
arr2[2].username = 'wade';
console.log(arr); //[ 1, 3, { username: 'wade' } ]Array.prototype.slice()
1
2
3
4
5
6let arr = [1, 3, {
username: ' kobe'
}];
let arr3 = arr.slice();
arr3[2].username = 'wade'
console.log(arr); // [ 1, 3, { username: 'wade' } ]
- 深拷贝:
JSON.parse(JSON.stringify())
不能处理函数和正则1
2
3
4
5
6let arr = [1, 3, {
username: ' kobe'
}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'duncan';
console.log(arr, arr4)- 函数库
lodash
的_.cloneDeep
方法1
2
3
4
5
6
7
8var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false jQuery.extend()
方法1
2
3
4
5
6
7
8var $ = require('jquery');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); // false手写递归
遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24function deepClone(obj, hash = new WeakMap()) {
if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 可能是对象或者普通的值 如果是函数的话是不需要深拷贝
if (typeof obj !== "object") return obj;
// 是对象的话就要进行深拷贝
if (hash.get(obj)) return hash.get(obj);
let cloneObj = new obj.constructor();
// 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 实现一个递归拷贝
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
let obj = { name: 1, address: { x: 100 } };
obj.o = obj; // 对象存在循环引用的情况
let d = deepClone(obj);
obj.address.x = 200;
console.log(d);
数组(array)
map
: 遍历数组,返回回调返回值组成的新数组forEach
: 无法break
,可以用try/catch
中throw new Error
来停止filter
: 过滤some
: 有一项返回true
,则整体为true
every
: 有一项返回false
,则整体为false
join
: 通过指定连接符生成字符串push / pop
: 末尾推入和弹出,改变原数组, 返回推入/弹出项unshift / shift
: 头部推入和弹出,改变原数组,返回操作项sort(fn) / reverse
: 排序与反转,改变原数组concat
: 连接数组,不影响原数组, 浅拷贝slice(start, end)
: 返回截断后的新数组,不改变原数组splice(start, number, value...)
: 返回删除元素组成的数组,value
为插入项,改变原数组indexOf / lastIndexOf(value, fromIndex)
: 查找数组项,返回对应的下标reduce / reduceRight(fn(prev, cur), defaultPrev)
: 两两执行,prev
为上次化简函数的return
值,cur
为当前值(从第二项开始)
数组乱序:
1 | var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; |
数组拆解: flat: [1,[2,3]] --> [1, 2, 3]
1 | Array.prototype.flat = () => this.toString().split(',').map(item => +item) |
Array(3)和Array(3, 4)的区别
1 | console.log(Array(3)) // [empty x 3] |
请创建一个长度为100,值都为1的数组
1 | new Array(100).fill(1) |
请创建一个长度为100,值为对应下标的数组
1 | // cool的写法: |
代码的复用
当你发现任何代码开始写第二遍时,就要开始考虑如何复用。一般有以下的方式:
- 函数封装
- 继承
- 复制
extend
- 混入
mixin
- 借用
apply/call
DOM节点
创建新节点
1 | createDocumentFragment() //创建一个DOM片段 |
添加、移除、替换、插入
1 | appendChild() //添加 |
查找
1 | getElementsByTagName() //通过标签名称 |
Ajax
- Ajax 的原理简单来说是在用户和服务器之间加了—个中间层(
AJAX
引擎),通过XmlHttpRequest
对象来向服务器发异步请求,从服务器获得数据,然后用javascript
来操作DOM
而更新页面。使用户操作与服务器响应异步化。这其中最关键的一步就是从服务器获得请求数据Ajax
的过程只涉及JavaScript
、XMLHttpRequest
和DOM
。XMLHttpRequest
是ajax
的核心机制
1 | // 1. 创建连接 |
优点:
- 通过异步模式,提升了用户体验.
- 优化了浏览器和服务器之间的传输,减少不必要的数据往返,减少了带宽占用.
- Ajax在客户端运行,承担了一部分本来由服务器承担的工作,减少了大用户量下的服务器负载。
- Ajax可以实现动态不刷新(局部刷新)
缺点:
- 安全问题 AJAX暴露了与服务器交互的细节。
- 对搜索引擎的支持比较弱。
- 不容易调试。
for in/for of
for in
性能很差,迭代当前对象中可枚举的属性,并且一直查找到原型上去。
- 问题1:遍历顺序数字优先
- 问题2:无法遍历symbol属性
- 问题3:可以遍历到原型属性中可枚举的
1
2
3
4
5
6
7
8
9
10
11
12
13
14let obj = {
name: 'poetry',
age: 22,
[Symbol('aa')]: 100,
0: 200,
1: 300
}
for(let key in obj) {
// 不遍历原型上的属性
if(!obj.hasOwnProperty(key)) {
break;
}
}
for of
- 部分数据结构实现了迭代器规范
- Symbol.itertor
- 数组/set/map
- 对象没有实现,for of不能遍历对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43// 数组具备迭代器规范,模拟实现
var arr = [1,2,3,4,5]
arr[Symbol.iterator] = function() {
let self = this, index = 0;
return {
next() {
if(index > self.length - 1) {
return {
done: true,
value: undefined
}
}
return {
done: false,
value: self[index++]
}
}
}
}
// 数组具备迭代器规范,模拟实现
var arr = [1,2,3,4,5]
arr[Symbol.iterator] = function() {
let self = this, index = 0;
return {
next() {
if(index > self.length - 1) {
return {
done: true,
value: undefined
}
}
return {
done: false,
value: self[index++]
}
}
}
}