# js
# 三种基础数据结构
一、栈 (stack)
不同场景的含义:
场景1:栈是一种 数据结构
,表达数据的一种存取方式 —— 后进先出 LIFO
,如数组
场景2:栈可以用来规定代码的执行顺序,如函数调用栈 call stack
场景3:栈表达数据在内存中的存储区域,通常叫做 栈区
(js没有同其他语言那样区分栈区或堆区,可以简单粗暴的认为在js中,所有的数据都是存放在堆内存空间中)
数组的栈方法:
push
(进栈):可以接受任意参数,并把他们逐个添加到数组末尾,并返回数组修改后的长度
pop
(出栈):删除数组最末尾的一个元素,并返回该数组
二、堆 (heap)
堆数据通常是一种树状结构,如:对象
堆的存取方式与书架中取书类似,只需知道书的名字就能找到它并可以很方便地取出,无需关心书的存放顺序
三、队列 (quene)
在js中,理解队列数据结构的目的是为了搞清楚事件循环 Event Loop
机制
先进先出 FIFO
# 内存空间
因为js有垃圾自动回收机制,所以对于前端开发人员来说,内存空间并不是一个经常被提及的概念,很容易被大家忽视。
# 一、基础数据类型与变量对象
最新到 ECMAScript
标准定义了7种数据类型,包括6种基础数据类型与一种引用数据类型 Object
类型 | 值 |
---|---|
Boolean | 只有两个值:true与false |
Null | 只有一个值:null |
Undefined | 只有一个值:undefined |
Number | 所有的数字 |
String | 所有到字符串 |
Symbol | 符号类型 var sym = Symbol('testsymbol') |
由于目前常用到浏览器版本还不支持 Symbol
,而且通过Babel编译之后的代码量过大,因此在实践中建议暂时不要使用Symbol
函数运行时,会创建一个执行环境,这个执行环境叫作 执行上下文
。在执行上下文中,会创建一个叫作变量对象的特殊对象。基础数据类型往往都保存在 变量对象
中
# 二、引用数据与堆内存空间
引用数据类型 Object
的值是保存在 堆内存空间
中堆对象。在js中,不允许直接访问堆内存空间中的数据,因此不能直接操作对象的堆内存空间。
在操作对象时,实际上是在操作对象的引用而不是实际的对象。因此,引用数据类型都是按引用访问的。这里的引用,可以理解为保存在变量对象中的一个地址,该地址与堆内存中堆对象相关联
# 三、内存空间管理
var a = 20
console.log(a + 100)
a = null
上面三条语句,分别对应如下三个过程:
- 分配内存
- 使用分配到的内存
- 不需要时释放内存
第三步涉及js垃圾回收机制,js的垃圾回收实现主要依靠“引用”的概念。当一块内存空间中的数据能够被访问时,垃圾回收器就认为“该数据能够被获得”。不能够被获得的数据,就会被打上标记,并回收内存空间。这种方式叫作 标记-清除算法
上例中将a设置为 null
时,那么刚开始分配的 20
,就无法被访问到了,而是很快会被自动回收。
全局变量什么时候需要自动释放内存空间很难判断,因此我们在开发时,应尽量避免使用全局变量。如果使用了全局变量,则建议不再使用它时,通过 a=null
这样到方式释放引用,以确保能够及时回收内存空间
# 执行上下文
执行上下文可以理解为当前代码的运行环境,主要包括以下三种情况:
- 全局环境:代码运行起来后会首先进去全局环境
- 函数环境:当函数被调用执行时,会进入当前函数中执行代码
- eval环境:不建议使用
在一个js程序中,必定会出现多个执行上下文,js引擎会以栈的方式来处理它们,即 函数调用栈
,它规定了js代码的执行顺序。栈底永远都是 全局上下文
,栈顶则是当前正在执行的上下文,处于栈顶的上下文执行完毕之后,会自动出栈
# 一、实例
function f1() {
var n=999
function f2() {
console.log(n)
}
return f2
}
var result = f1()
result() // 999
函数在执行时才会创建执行上下文
全局上下文入栈 -> f1
创建对应的执行上下文并入栈 -> f1
执行结束出栈 -> result
创建执行上下文并入栈 -> result
执行( f2
中的代码)完毕后出栈
# 二、生命周期
当一个函数调用时,一个新的执行上下文就会被创建。一个执行上下文的生命周期大致可以分为两个阶段:创建阶段和执行阶段
- 创建阶段:执行上下文会分别创建变量对象,确认作用域链,以及确认this的指向
- 执行阶段:创建阶段之后,就开始执行代码,这个时候会完成变量赋值、函数引用,以及执行其他可执行代码
# 变量对象
# 一、变量对象(VO, Variable Object)
js代码中声明的所有变量都保存在变量对象中,除此之外,变量对象中还可能包含:
- 函数的所有参数(
Firefox
中为参数对象arguments
) - 当前上下文中的所有函数声明(通过
function
声明的函数) - 当前上下文中的所有变量声明(通过
var
声明的变量)
var关键字声明的变量,变量声明是在上下文创建阶段进行的(获取变量名并赋值为undefined),变量赋值是在上下文执行阶段进行的,因此会存在变量提升。 es6
支持新的变量声明方式 let/const
,规则与 var
完全不同,它们是在上下文的执行阶段开始执行的,不存在变量提升
# 二、活动对象(AO, Activation Object)
在函数调用栈中,如果当前执行上下文处于函数调用栈的栈顶,则意味着当前上下文处于激活状态,此时变量对象称之为活动对象。活动对象中包含变量对象的所有属性,并且此时所有的属性都已经完成了赋值,除此之外,活动对象还包含了 this
的指向
function test() {
console.log(a)
console.log(foo())
var a = 1
function foo() {
return 2
}
}
test()
// 创建过程, test的执行上下文
testEC = {
VO: {}, // 变量对象
scopeChain: [], // 作用域链
this: {}
}
VO = {
arguments: {...},
foo: <foo reference>,
a: undefined
}
// 执行阶段,VO -> AO
AO = {
arguments: {},
foo: <foo reference>,
a: 1,
this: Window
}
// 因此上面代码的实际执行顺序如下
function test() {
function foo() {
return 2
}
var a = undefined
console.log(a)
console.log(foo())
a = 1
}
test()
# 三、全局上下文的变量对象
以浏览器为例,全局对象为 window
对象
全局上下文的变量对象有一个特殊的地方,即它的变量对象就是window对象,而且全局上下文的变量对象不能变为活动对象
// 全局上下文
windowEC = {
VO: window,
scopeChain: {},
this: window
}
只要程序运行不结束(比如关掉浏览器窗口),全局上下文就会一直存在,其他所有的上下文环境都能直接访问全局上下文的属性
# 作用域与作用域链
# 一、全局作用域
全局作用域中声明的变量与函数可以在代码的任何地方被访问
一般来说,以下三种情形拥有全局作用域
- 全局对象下拥有的属性和方法:
window.name、window.location、window.top
- 在最外层声明的变量和方法:全局上下文的变量对象实际上就是
window
变量本身,因此在全局上下文中声明的变量和方法其实就变成了window的属性和方法,所以它们也拥有全局作用域 - 非严格模式下,函数作用域中未定义但直接赋值的变量与方法
在实践中,无论是从避免多人协作带来的冲突的角度考虑,还是从性能优化的角度考虑,都要尽量避免自定义全局变量与方法
# 二、函数作用域
函数作用域中声明的变量与方法,只能被下层子作用域访问,而不能被其他不相干的作用域访问
es6之前没有块级作用域,可以用自执行函数来模拟块级作用域
var arr = [1, 2, 3, 4, 5]
(function() {
for(var i = 0; i < arr.length; i++) {
console.log(i)
}
})()
console.log(i) // i is not defined
es5时往往通过函数自执行的方式来实现模块化,而模块化时实际开发中需要重点掌握的开发思维
# 三、作用域链
作用域链是由当前执行环境与上层执行环境的一系列变量对象组成的,它保证了当前执行环境对符合访问权限的变量和函数的有序访问
var a = 20
function test() {
var b = a + 10
function innerTest() {
var c = 10
return b + c
}
return innerTest()
}
test()
上例中,先后创建了全局函数test和函数innerTest的执行上下文。假设它们的变量对象分别为VO(global)、VO(test)和VO(innerTest)
,那么innerTest的作用域链则同时包含了这三个变量对象
innerTest的执行上下文可表示如下:
innerTestEC = {
VO: {...}, // 变量对象
scopeChain: [VO(innerTest), VO(test), VO(global)], // 作用域链
this: {}
}
很多人会用父子关系或者包含关系来理解当前作用域与上层作用域之间的关系,但其实这种说法并不准确,以当前上下文的变量对象为起点,以全局变量对象为终点的单方向通道这样的描述更加贴切
# 闭包
# 一、概念
- 闭包是一种特殊的对象
- 它由两部分组成-执行上下文(代号A),以及在该执行上下文中创建的函数(代号B)
- 当B执行时,如果访问了A中变量对象的值,那么闭包就会产生
许多地方都以函数B的名字代指这里生成的闭包。而在 Chrome
中,则以执行上下文A的函数名代指闭包
我们只需知道,一个闭包对象,由A、B共同组成,在以后的篇幅中,都将以Chrome的标准来称呼
# 二、示例
demo01
function foo() {
var a = 20
var b = 30
function bar() {
return a + b
}
return bar
}
var bar = foo()
bar()
当 bar
执行时,访问了 foo
内部当变量 a和b
,因此产生了闭包,如图
demo02
function foo() {
var a = 20
var b = 30
function bar() {
return a + b
}
bar()
}
foo()
仍然是 foo
中定义的 bar
函数在执行时访问了foo中的变量,因此这个时候仍会形成闭包,如图
demo03
function add(x) {
return function _add(y) {
return x + y
}
}
add(2)(3) // 5
这个例子有闭包产生吗?
当然有,当内部函数 _add
被调用执行时,访问了 add
函数变量对象中当 x
,这个时候,闭包就会产生,如图
demo04
var name = 'window'
var p = {
name: 'Perter',
getName: function() {
return function() {
return this.name
}
}
}
var getName = p.getName()
var _name = getName()
console.log(_name) // window
getName
在执行时,它的 this
其实指向的是 window
对象,而这个时候并没有形成闭包的环境,因此这个例子没有闭包,如图
下面改动一下,分别利用 call
与 变量保存
的方式保证 this
指向的都为 p
对象,那么它们是否会产生闭包?
demo05
var name = 'window'
var p = {
name: 'Perter',
getName: function() {
return function() {
return this.name
}
}
}
var getName = p.getName()
// 利用call的方式让this指向p对象
var _name = getName.call(p)
console.log(_name) // Perter
结果如图
没有产生闭包,因为函数参数中的变量传递给函数之后也会加到变量对象中,而 p
就是通过 call
函数参数传递的,因此会加入函数自身的变量对象 Local
中,而没有产生闭包
demo06
var name = 'window'
var p = {
name: 'Perter',
getName: function() {
// 利用变量保存的方式保证其访问的是p对象
var self = this
return function() {
return self.name
}
}
}
var getName = p.getName()
var _name = getName()
console.log(_name) // Perter
结果如图 产生了闭包,因为访问了执行上下文p.getName中定义的变量self
# 三、闭包与垃圾回收机制
当一个函数的执行上下文运行完毕之后,内部的所有内容都会失去引用而被垃圾回收机制回收,而闭包的本质就是在函数外部保持了内部变量的引用,因此闭包会阻止垃圾回收机制进行回收
function f1() {
var n = 999
nAdd = function() {
n += 1
}
return function f2() {
console.log(n)
}
}
var result = f1()
result() // 999
nAdd()
result() // 1000
因为 nAdd、f2
都访问了 f1中的n
,因此它们都与f1形成了闭包。这个时候变量n的引用被保留了下来。因为 f2(result)
与 nAdd
执行时都访问了 n
,而nAdd每运行一次就会将n加1
正是因为闭包中保存的内容不会被释放,我们在使用闭包时要足够警惕,如果滥用闭包,很可能会因为内存的原因导致程序性能过差
# 四、闭包与作用域链
闭包会导致函数的作用域链发生变化吗?
var fn = null
function foo() {
var a = 2
function innerFoo() {
console.log(a)
}
fn = innerFoo // 将innerFoo的引用赋值给全局变量中的fn
}
function bar() {
fn() // 此处保留innerFoo的引用
}
foo()
bar() // 2
foo内部的 innerFoo
访问了 foo的变量a
,因此当innerFoo执行的时候会有闭包产生。不一样的地方在于全局变量fn,fn在foo内部获取了innerFoo的引用,并在bar中执行,那么innerFoo的作用域链会是怎样的呢?
先利用断点调试观察一下,如图
需注意函数调用栈 Call Stack
与作用域链 Scope
的区别,因为函数调用栈其实是在代码执行时才确定的,而作用域链则在代码编译阶段就已经确定,虽然作用域链是在代码执行时才生成的,但是它的规则并不会在执行时发生改变,所以这里闭包的存在并不会导致作用域链发生变化
四、基础概念回顾
函数在被调用执行时,会创建一个当前函数的执行上下文。在该执行上下文的创建阶段,变量对象、作用域链、闭包、this等会分别确认。而一个程序中一般来说会有多个函数执行,因此执行引擎会使用函数调用栈来管理这些函数的执行顺序。函数调用栈的执行顺序与栈数据结构一致
在最新的MDN中,对闭包是这样定义的:闭包是指这样的作用域,它包含了一个函数,这个函数可以调用被这个作用域所封闭的变量、函数或者闭包等内容。我们通常通过闭包所对应的函数来获得对闭包的访问
# 五、应用闭包
1、循环、setTimeout与闭包
for(var i=1; i<=5; i++) {
setTimeout(function timer() {
console.log(i)
}, i*1000)
}
当timer函数被setTimeout运行时,循环已经结束(js的事件循环机制),即i已经变成了6,因此这段代码的执行结果是隔秒输出 6、6、6、6、6
我们想要的是隔秒输出 1、2、3、4、5
,因此需要借助闭包的特性,将每一个i值都用一个闭包保存起来。每一轮循环,都把当前的i值保存在一个闭包中,当setTimeout中定义的操作执行时,访问对应的闭包即可
for(var i=1; i<=5; i++) {
(function (i) {
setTimeout(function timer() {
console.log(i)
}, i*1000)
})(i)
}
// or
for(var i=1; i<=5; i++) {
setTimeout((function(i) {
return function timer() {
console.log(i)
}
})(i), i*1000)
}
1、单例模式与闭包
在js中有许多解决特定问题的编码思维(设计模式),例如工厂模式、订阅通知模式、装饰模式、单例模式等。其中,单例模式是实践中最常用的模式之一,而它的实现,与闭包息息相关.
单例模式,就是只有一个实例
对象字面量的方法就是最简单的单例模式,我们可以将属性与方法依次放在字面量里
var per = {
name: 'Jake',
age: 20,
getName: function() {
return this.name
},
getAge: function() {
return this.age
}
}
这样的单例模式有个一严重的问题,即它的属性可以被外部修改
有私有方法/属性的单例模式
ver per = (function() {
var name = 'Jake'
var age = 20
return {
getName: function() {
return name
},
getAge: function() {
return age
}
}
})()
// 访问私有变量
per.getName()
私有变量的好处在于,外界对于私有变量能够进行什么操作是可以控制的,我们可以提供一个 getName
方法让外界可以访问名字,也可以额外提供一个 setName
方法,来修改它的名字。对外提供什么样的能力,完全由我们自己决定。在模块化的开发中,每一个模块都是一个与此类似的单例模式。
模块化与闭包 如果想在所有的地方都能访问同一个变量,如全局的状态管理。前面提到过,实际开发中,不要轻易使用全局变量,那么应该怎么办呢?
模块化开发是目前最流行,也是必须掌握的一种开发思路。而模块化开发其实是建立在单例模式基础之上的,因此模块化开发和闭包息息相关。目前流行的模块化开发思路,无论是require,还是ES6的modules,虽然实现方式不同,但是核心的思路一样。
以建立在函数自执行基础之上的单例模式为例,一起来感受一下模块化开发
1)每个单例就是一个模块
var module_test = (function() {
})()
2)每一个模块要想与其他模块交互,则必须有获取其他模块的能力,例如 requirejs中的require
与 ES6 modules中的import
// require
var $ = require('jquery')
// es6 modules
import $ from 'jquery'
3)每一个模块都应该有对外的接口,以保证与其他模块交互的能力,这里直接使用 return
返回一个字面量对象的方式来对外提供接口
var module_test = (function() {
...
return {
testfn1: function() {},
testfn2: function() {}
}
})()
<script>
// 首先创建一个专门用来管理全局状态的模块
var module_status = (function() {
var status = {
number: 0,
color: null
}
var get = function(prop) {
return status[prop]
}
var set = function(prop, value) {
status[prop] = value
}
return {
get: get,
set: set
}
})()
// 再创建一个模块,专门负责body背景颜色的改变,对外只需使用render方法就可以了
var module_color = (function() {
// 假装用这种方式执行第二步引入模块
// 类似于import state from 'module_status'
var state = module_status
var colors = ['orange', '#ccc', 'pink']
function render() {
var color = colors[state.get('number') % 3]
document.body.style.backgroundColor = color
}
return {
render: render
}
})()
// 创建另一个模块来负责显示当前的number值,用于对比参考
var module_context = (function() {
var state = module_status
function render() {
document.body.innerHTML = 'this Number is ' + state.get('number')
}
return {
render: render
}
})()
// 最后创建一个主模块即可
var module_main = (function() {
var state = module_status
var color = module_color
var context = module_context
setInterval(function() {
var newNumber = state.get('number') + 1
state.set('number', newNumber)
color.render()
context.render()
}, 1000)
})()
</script>
# this
当前函数的this是在函数被调用执行的时候才确定的。如果当前的执行上下文处于函数调用栈的栈顶,那么这个时候变量对象会变成活动对象,同时this的指向确认。因此,this的指向非常灵活且不确定,而难以理解。
# 一、全局对象中的this
全局对象的this指向它本身,在浏览器下,指向 Window
# 二、函数中的this
在一个函数的执行上下文中,this由该函数的调用者提供,由调用函数的方式来决定其指向
如果调用者被某一个对象所拥有,那么在调用该函数时,内部的this指向该对象。如果调用者函数独立调用,那么该函数内部的this则指向 undefined
。但是在非严格模式中,当this指向undefined时,它会自动指向全局对象
function fn() {
'use strict'
console.log(this)
}
fn() // undefined
window.fn() // Window
我们一般都是在非严格模式下,因此函数独立调用时,this指向全局对象
var a = 20
var obj = {
a: 40
}
function fn() {
console.log('fn this: ', this)
function foo() {
console.log('this.a: ', this.a)
}
foo()
}
fn.call(obj) // fn this: Object {a: 40} this.a: 20
fn() // fn this: Window {} this.a: 20
箭头函数中的this,就是声明函数时所处上下文中的this,它不会被其他方式所改变
# 三、call/apply/bind 显式指定this
var a = 20
var object = {
a: 40
}
function fn() {
console.log(this.a)
}
fn() // 20
fn.call(object) // 40
fn.apply(object) // 40
当函数调用 call/apply
时,表示会立即执行该函数
function fn(num1, num2) {
return this.a + num1 + num2
}
var a = 20
var object = { a: 40 }
fn(10, 10) // 40
fn.call(object, 10, 10) // 60
fn.apply(object, [10, 10]) // 60
call
的第一个参数是为函数内部指定this指向,后续的参数则是函数执行时所需要的参数,一个个传递
apply
的第一个参数也是函数内部this指向,而函数的参数,则以数组的形式传递,作为apply的第二个参数
bind
方法也能指定函数内部的this指向,但是它与call/apply有所不同
当函数调用call/apply时,函数的内部this被显式指定,并且函数会立即执行。而当函数调用bind时,函数并不会立即执行,而是返回一个新的函数,这个新的函数与原函数有共同的函数体,但它并非原函数,并且新函数的参数与this指向都已经被绑定,参数为bind的后续参数
function fn(num1, num2) {
return this.a + num1 + num2
}
var a = 20
var object = { a: 40 }
var _fn = fn.bind(object, 1, 2)
console.log(_fn === fn) // false
_fn() // 43
_fn(1, 4) // 43 因为参数被绑定,因此重新传如参数是无效的
call/apply/bind
的特性让js变得十分灵活,它们的应用场景很多,例如,将类数组转化成数组,实现继承,实现函数柯里化等
function fn(num1, num2) {
console.log(arguments, arguments instanceof Array) // false
var arr = Array.prototype.slice.apply(arguments) // 将类数组转化成数组
console.log(arr, arr instanceof Array) // true
}
fn(1, 2)
# 函数与函数式编程
# 一、函数
实际开发中,经常能遇到的函数形式大概有四种:函数声明、函数表达式、匿名函数、自执行函数
1、函数声明
利用关键字function来声明一个函数
function fn() {
console.log('function')
}
在变量对象的创建过程中,function声明的函数比var声明的变量更加优先执行,即我们常常提到的函数声明提前。因此在同一个执行上下文中,无论在什么地方声明了函数,都可以直接使用
2、函数表达式
将一个函数体赋值给一个变量的过程
var fn = function() {}
// 等同于
var fn = undefined // 变量提升
fn = function() {}
函数表达式,必须先定义后使用
3、匿名函数
就是没有名字的函数,一般会作为一个参数或者作为一个返回值来使用,通常不使用变量来保存它的引用,常见的场景如下:
// 1)setTimeout中的参数
var timer = setTimeout(function() {
console.log('延迟1000ms执行该匿名函数')
}, 1000)
// 2)数组方法中的参数
var arr = [1, 2, 3]
arr.map(function(item) {
return item + 1
})
// 3)作为一个返回值
function add() {
var a = 10
return function() {
return a + 20
}
}
add()()
4、自执行函数
自执行函数是匿名函数一个非常重要的应用场景。因为函数会产生独立的作用域,因此我们常常利用自执行函数来模拟块级作用域,并进一步实现模块化的应用
(function() {
// ...
})()
# 二、函数式编程
1、概念
其实就是我们平常说的函数封装,与 函数式编程
相对立的叫作 命令式编程
// 命令式编程
var array = [1, 3, 'h', 5, 'm', '4']
var res = []
for(var i = 0; i< array.length; i++) {
if(typeof array[i] === 'number') {
res.push(array[i])
}
}
// 函数式编程
function getNumbers(array) {
var res = []
array.forEach(function(item) {
if(typeof item === 'number') {
res.push(item)
}
})
return res
}
var array = [1, 3, 'h', 5, 'm', '4']
var res = getNumbers(array)
我们只需要知道getNumbers这个工具方法能做什么即可,而不关心它的内部如何实现
2、函数是一等公民
所谓“一等公民”,其实就是普通公民,也就是说,函数其实没什么特殊的,我们可以像对待任何其他数据类型一样对待函数
- 可以把函数赋值给一个变量
var fn = function() {}
- 也可以把函数作为参数传递
function fn(callback) {
var a = 20
return callback(20, 30) + a
}
function add(a, b) {
return a + b
}
fn(add) // 70
- 还可以把函数作为另一个函数运行的返回值
function add(x) {
var y = 20
return function() {
return x + y
}
}
var _add = add(100)
_add() // 120
这些都是js的基本概念,但是很多人都无视这些概念,下面用一个简单的例子来验证一下
// 首先自定义一个函数,要求在5000ms之后执行该函数,我们应该怎么做?
function delay() {
console.log('5000ms之后执行该函数')
}
// 有的人可能会这样写
var timer = setTimeout(function() {
delay()
}, 5000)
// 很显然,这样能够达到我们的目的,但这也正是我们忽视了上面的概念写出来的糟糕代码
// 函数既然能够作为一个参数传入另一个函数,那么是不是可以直接将delay函数传入,而不用在固有的思维上额外再封装一层多余的function呢?
var timer = setTimeout(delay, 5000)
思考一下如何优化下面的例子
function getUser(path, callback) {
return $.get(path, function(info)) {
return callback(info)
}
}
getUser('/api/user', function(resp) {
console.log(resp)
})
// 先优化getUser
function getUser(path, callback) {
return $.get(path, callback)
}
// 可以看出,$get方法也同样被包裹了一层没有意义的函数,再优化
var getUser = $.get // $.get是jquery自带的工具方法
3、纯函数
相同的输入总会得到相同的输出,并且不会产生副作用的函数,就是纯函数
我们可以通过一个是否会改变原始数据的两个同样功能的方法来区别纯函数与非纯函数之间的不同
function getLast(arr) {
return arr[arr.length - 1]
}
function getLast_(arr) {
return arr.pop()
}
var source = [1, 2, 3, 4]
var last = getLast(source) // 返回结果4,原数组不变
var last_ = getLast_(source) // 返回结果4,原数组最后一项被删除
getLast_改变来原数组,再次调用该方法时,得到的结果就会变得不一样。这种不可预测的封装方式是非常糟糕的,会把我们的数据搞得非常混乱。在js原生支持的数据方法中,也有许多不纯的方法
var source = [1, 2, 3, 4, 5]
source.slice(1, 3) // 纯函数返回[2, 3],source不变
source.splice(1, 3) // 不纯的函数返回[2, 3, 4],source被改变
source.pop() // 不纯的
source.push(6) // 不纯的
source.shift() // 不纯的
source.unshift() // 不纯的
source.reverse() // 不纯的
source.concat([6, 7]) // 纯函数返回[1, 2, 3, 4, 5, 6, 7],source不变
source.join('-') // 纯函数返回1-2-3-4-5, source不变
纯函数的优点:可移植性,可缓存性
# 面向对象
# 一、概念
1、对象的定义
在ECMAScript-262中,对象被定义为“无序属性的集合,其属性可以包含基本值、对象、或者函数”
2、创建对象
// 通过关键字new来创建一个对象
var person = new Object()
// 通过对象字面量的形式创建一个对象
var person = {}
// 给对象添加属性与方法
person = {
name: 'TOM',
age: 20,
getName: function() {
return this.name
}
}
// 访问对象的属性
person.name
// or
person['name']
# 二、构造函数与原型
// 构造函数
var Person = function(name, age) {
this.name = name
this.age = age
}
// Person.prototype 为Person的原型,这里在原型上添加一个方法
Person.prototype.getName = function() {
return this.name
}
这样,我们就利用构造函数与原型封装好来一个 Person
对象
具体某一个人的特定属性,通常放在构造函数中,例如此处的name、age,它们的值不是所有人的共同属性,而是仅仅属于某一个人
所有人公共的方法与属性,通常会放在原型对象中,例如此处的getName,它表示一个共同的动作
var p1 = new Person('Jake', 20)
var p2 = new Person('Tom', 22)
p1.getName() // Jake
p2.getName() // Tom
构造函数其实与普通函数并无区别,首字母大写是一种约定,用来表示这是一个构造函数。但是new关键字的存在,让构造函数变得与众不同
构造函数中的 this
,与原型方法中的 this
,指向的都是当前的实例
我们可以模拟构造new关键字的能力,实现一个New方法,来看一下new关键字到底做了什么事情
// 将构造函数以参数形式传入
function New(func) {
// 声明一个中间对象,该对象为最终返回的实例
var res = {}
if(func.prototype !== null) {
// 将实例的原型指向构造函数的原型
res.__proto__ = func.prototype
}
// ret为构造函数的执行结果,这里通过apply将构造函数内部的this指向修改为指向res,即实例对象
var ret = func.apply(res, Array.prototype.slice.call(arguments, 1))
// 当我们在构造函数中明确指定来返回对象时,那么new的执行结果就是返回该对象
if((typeof ret === "object" || typeof ret === "function") && ret !== null) {
return ret
}
// 如果没有明确指定返回对象,则默认返回res,这个res就是实例对象
return res
}
var Person = function(name) {
this.name = name
}
Person.prototype.getName = function() {
return this.name
}
var p1 = New(Person, 'Jake')
var p2 = New(Person, 'Tom')
p1.getName() // Jake
p2.getName() // Tom
new关键字在创建实例时经历了如下过程:
- 先创建一个新的、空的实例对象
- 将实例对象的原型,指向构造函数的原型
- 将构造函数内部的this,修改为指向实例
- 最后返回该实例对象
构造函数的prototype与所有实例的__proto__都指向原型对象,而原型对象的constructor则指向构造函数
# 事件循环机制
事件循环机制(Event Loop)是全面了解JavaScript代码执行顺序绕不开的一个重要的知识点,特别是在ES6正式支持Promise之后,对于新标准中事件循环的理解就变得更为重要
// demo01
setTimeout(function() {
console.log(1)
}, 0)
console.log(2)
for(var i = 0; i < 5; i++) {
console.log(3)
}
console.log(4)
// demo02
console.log(1)
for(var i = 0; i < 5; i++) {
setTimeout(function() {
console.log('2-' + i)
}, 0)
}
console.log(3)
为什么即使设置了setTimeout的延迟时间为0,它里面的代码仍然是最后执行的?
通常情况下,决定代码执行顺序的是函数调用栈。很显然这里的 setTimeout
中的执行顺序已经不是用函数调用栈能够解释清楚的了,为什么?答案是队列
JavaScript的一个特点就是单线程,但是很多时候我们仍然需要在不同的时间去执行不同的任务,例如给元素添加点击事件,设置一个定时器,或者发起Ajax请求。因此需要一个异步机制来达到这样的目的,事件循环机制也因此而来
每一个JavaScript程序都拥有唯一的事件循环,大多数代码的执行顺序是可以根据函数调用栈的规则执行的,而 setTimeout/setInterval
或者不同的事件绑定(click、mousedown等)中的代码,则通过队列来执行
setTimeout为任务源,或者任务分发器,由它们将不同的任务分发到不同的任务队列中去。每一个任务源都有对应的任务队列(setTimeout与setInterval是同源的)
任务队列又分为宏任务 macro-task
与微任务 micro-task
两种,在浏览器中(node.js中还包括更多的任务队列,此处不讨论),包括:
- macro-task: script(整体代码)、setTimeout/setInterval、I/O、UI rendering等
- micro-task: Promise
setTimeout(function() {
console.log('timeout1')
}, 0)
new Promise(function(resolve) {
console.log('promise1')
for(var i = 0; i < 1000; i++) {
i == 99 && resolve()
}
console.log('promise2')
}).then(function() {
console.log('then1')
})
console.log('global1')
- 第一步,
script
任务开始执行,全局上下文入栈 - 第二步,script任务执行时首先遇到了
setTimeout
,setTimeout为一个宏任务源,而它的作用就是将任务分发到它对应的队列中去 - 第三步,script执行时遇到
Promise
实例。Promise构造函数中的第一个参数,是在new创建实例的时候执行,因此不会进入任何其他的队列,而是在当前任务直接执行,后续的.then
则会被分发到micro-task
的Promise队列中去。因此,构造函数执行时,里面的参数进入函数调用栈执行。for循环不会进入任何队列,代码会依次执行,所以这里的promise1和promise2会依次输出。script任务继续向下执行,最后输出global1
,至此,全局任务就执行完毕了 - 第四步,第一个宏任务script执行完毕之后,就开始执行所有可执行的微任务,这个时候,微任务中,只有Promise队列中的一个任务
then1
。因此直接执行就可以了,执行结果输出then1,当然,他也是进入函数调用栈中执行的 - 第五步,当所有的micro-task执行完毕之后,表示第一轮的循环就结束了开始第二轮的循环。第二轮循环仍然从宏任务macro-task开始。此时宏任务中,只有setTimeout队列中还要一个
timeout1
的任务等待执行。因此直接执行即可 - 至此,宏任务队列与微任务队列中就都没有任务了,所以上面这个例子的输出结果显而易见
promise1
promise2
global1
then1
timeout1
# 参考文档
《JavaScript核心技术开发解密》