闭包
变量作用域
变量根据作用域不同可以将函数分为全局变量和局部变量
- 函数内部可以使用全局变量
- 函数外部不可以使用局部变量
- 函数执行完毕后,本作用域的局部变量会销毁
在JS中函数的闭包有广泛的应用场景,闭包常用于私有化对象数据、事件处理、回调函数等
闭包的定义
闭包是指有权访问另一个函数作用域中变量的函数,简单来说,一个作用域可以访问另一个作用域内部的局部变量
function func() {
var num = 10
return function () {
console.log(num)
}
}
var f = func()
f() // 10
闭包如何使用
在一个函数中定义另一个函数,并把它暴露出去。有了两种方式:作为返回值返回;调用另一个函数将它作为参数。
闭包的使用
循环注册点击事件
函数定义完后成立即执行,往往只执行一次
var lis = document.querySelectorAll('li')
for (var i = 0; i < lis.length; i++) {
lis[i].onclick = function () {
console.log(i)
}
}
上面的代码有一个问题,当按钮点击的时候,打印的总是lis.length。因为点击按钮的时候循环已经结束了
for (var i = 0; i < lis.length; i++) {
(function (i) {
lis[i].onclick = function () {
console.log(i)
}
})(i)
}
上面的立即执行函数构成一个闭包,因为它里面有一个匿名函数使用了它作用域范围内的变量
循环中的setTimeout()
需求:在等待3秒后打印所有节点
var lis = document.querySelectorAll('li')
for (var i = 0; i < lis.length; i++) {
(function (i) {
setTimeout(function () {
console.log(lis[i].innerHTML)
}, 3000)
})(i)
}
闭包的案例:打车计费
var car = (function () {
var start = 13
var total = 0
return {
price: function (n) {
if (n <= 3) {
total = start
} else {
total = start + (n - 3) * 5
}
return total
},
// 拥堵后的费用
yd(flag) {
return flag ? total + 10 : total
}
}
})()
console.log(car.price(10)) // 48
console.log(car.yd(true)) // 58
思考题
var name = 'The Window'
var object = {
name: 'My Object',
getNameFunc: function () {
return function () {
return this.name
}
}
}
console.log(object.getNameFunc()())
// The Window
object.getNameFunc()返回的是一个函数,和后面的括号构成立即执行函数,而立即执行函数的this指向window。这段代码中没有闭包,因为不满足闭包的条件
var name = 'The Window'
var object = {
name: 'My Object',
getNameFunc: function () {
var that = this
return function () {
return that.name
}
}
}
console.log(object.getNameFunc()())
// My Object
getNameFunc构成了一个闭包,因为它return的函数使用了其的局部变量that。
递归
函数内部调用自己,必须添加递归退出条件
无限递归的例子
(function fn() {
console.log('start')
fn()
})()
递归的案例
var data = [
{
id: 1,
name: '水果',
goods: [
{
id: 11,
name: '苹果'
},
{
id: 12,
name: '梨'
}
]
},
{
id: 2,
name: '蔬菜'
}
]
function getID(json, id) {
var o = {}
json.forEach(function (item) {
if (item.id === id) {
o = item
// 递归遍历子列表
} else if (item.goods && item.goods.length > 0) {
// 注意: getID()是有返回值的,必须有变量进行接收
o = getID(item.goods, id)
}
})
return o
}
console.log(getID(data, 11))
变量提升
var
定义的变量会被提升到最前面
cnsole.log(a) // undefined
var a = 10
如果把上面的
var
换成const
和let
或者不加任何修饰符,结果均报错
类和对象
类继承和super
class A {
constructor(a, b) {
this.x = a
this.y = b
}
sum() {
return this.x + this.y
}
}
class B extends A {
constructor(a, b, c) {
// super 在this前面
super(a, b)
this.c = c
}
}
console.log(new B(1, 2, 3).sum()) // 3
super
关键字调用父类构造方法。子类在构造函数中使用super,必须在this前面
通过super关键字调用父类普通方法
class A {
sayHi() {
console.log('Hello')
}
}
class B extends A {
sayHi() {
super.sayHi()
}
}
new B().sayHi() // Hello
类里面this指向
constructor
里面的this指向的是实例对象,方法里面的this指向的是这个方法的调用者
var that
class A {
constructor(name) {
that = this
this.name = name
this.btn = document.querySelector('button')
this.btn.onclick = this.click
}
click() {
// console.log(`${this.name} clicked`)
console.log(`${that.name} clicked`)
}
}
var a = new A('aaa')
a.click()
由于上面调用click函数的是按钮,所以
this
指向的是按钮,不能通过this.name
访问到类里面的属性。上面解决方案是用一个全局变量保存this
对象
在es6之前,有三种方式构造对象:对象字面量、new Object()、自定义对象构造函数
对象字面量
var obj = new Object()
对象字面量
var obj = {}
函数构造法
function person(name, age) {
this.name = name
this.age = age
this.sing = function () {
console.log(`${this.name} can sing`)
}
}
var aa = new person('aa', 12)
aa.sing()
构造函数是一种特殊的函数,它主要用来初始化对象,总是和
new
同时使用,new
在执行时进行四件操作:1. 在内存中创建一个新的对象,2. 让this指向这个新的对象 3. 执行构造函数中的代码,给这个新对象添加属性和方法 4. 返回这个新的对象(无需return)
实例成员和静态成员
实例成员
只能通过实例化的对象访问的成员
静态成员
只能通过构造函数来访问,不能通过对象来访问
function person(name, age) {
this.name = name
this.age = age
this.sing = function () {
console.log(`${this.name} can sing`)
}
}
person.gender = 'unkonwn'
console.log(person.gender) // unknown
构造函数和原型
构造函数
构造函数的问题:每创建一个对象,都会对开辟单独空间存放成员,复杂类型如方法等也不例外,造成内存空间浪费。
function person(name, age) {
this.name = name
this.age = age
this.sing = function () {
console.log(`${this.name} can sing`)
}
}
var aa = new person('aa', 12)
var bb = new person('bb', 22)
console.log(aa.sing === bb.sing) // false
上面两个实例对象的sing函数不在同一个地址,打印的false
构造函数原型prototype
每个构造函数都有一个
prototype
属性,它是一个对象,称为原型对象,这个对象的所有属性(通过.prototyoe.prop
添加)和方法(通过.prototyoe.func
添加)都会被构造函数所拥有。一般把方法定义在原型上,这样所有实例都可以访问这些方法
function person(name, age) {
this.name = name
this.age = age
}
person.prototype.sing = function () {
console.log(`${this.name} can sing`)
}
var aa = new person('aa', 12)
var bb = new person('bb', 22)
console.log(aa.sing === bb.sing)
aa.sing()
对象原型
对象都会有一个属性
__proto__
指向构造函数的原型对象。对象原型__proto__
和原型对象prototype
是等价的,对象原型是非标准属性,在实际开发中,不可以使用
console.log(person.prototype === aa.__proto__) // true
constructor
原型对象或对象原型有一个属性
constructor
,这个属性用于指定构造函数
function person(name, age) {
this.name = name
this.age = age
}
person.prototype.sing = function () {
console.log(`${this.name} can sing`)
}
var aa = new person('aa', 12)
var bb = new person('bb', 22)
console.log(person.prototype.constructor) // [Function: person]
console.log(aa.__proto__.constructor) // [Function: person]
修改原型对象
一般在函数方法比较多的时候会以对象形式修改原型对象,这时必须手动指定原型对象的构造函数
function person(name, age) {
this.name = name
this.age = age
}
// person.prototype.sing = function () {
// console.log(`${this.name} can sing`)
// }
// 以对象形式修改原型对象
person.prototype = {
constructor: person,
sing: function () {
console.log(`${this.name} can sing`)
},
dance: function () {
console.log(`${this.name} can dance`)
}
}
var aa = new person('aa', 12)
var bb = new person('bb', 22)
aa.sing()
原型链
Star构造函数的原型对象(prototype)也具有对象原型(_proto_),指向的是
Object.prototype
,Object构造函数的原型对象的对象原型是null
原型链查找机制
- 当访问一个对象属性(方法)时,首先查找这个对象自身有没有该属性
- 如果没有就查找它的原型(也就是_proto_指向的prototype原型对象)
- 如果没有就查找原型对象的原型(Object原型对象)
- 以此类推,一致找到Object为止
- 查找遵循就近原则
原型对象里面的this指向问题
原型对象里面的this指向的是实例对象
扩展内置对象
修改内置对象的原型对象可以扩展自定义方法
Array.prototype.sum = function () {
var sum = 0
// 这里this指向的是实例对象
for (var i = 0; i < this.length; i++) {
sum += this[i]
}
return sum
}
继承
es6之前没有提供继承,可以通过
构造函数+原型对象
来模拟实现继承,称为组合继承
call()
有两个作用:1.修改函数运行时的this指向 2.调用函数
fun.call(thisArg, arg1, arg2, ...)
thisArg 当前调用函数this的指向对象
arg1, arg2 传递的其他参数
function fn(x, y) {
console.log(this)
console.log(x + y)
}
// fn.call() // 此时this指向的是Window
var o = {
name: 'andy'
}
// fn.call(o) // 此时this指向的是对象o
fn.call(o, 1, 2)
使用call()实现继承
function Animal(name) {
this.name = name
}
Animal.prototype.eat = function () {
console.log(`${this.name} need eat`)
}
function Cat(name) {
// 修改了Animal的this指向,实现继承Animal的属性
Animal.call(this, name)
}
// 利用构造函数修改Cat的原型对象,constructor也会变成Animal的构造函数
Cat.prototype = new Animal()
// 将Cat的构造函数改回之前的
Cat.prototype.constructor = Cat
var cat = new Cat('huahua')
cat.eat() // huahua need eat
类的本质
es6类的本质是函数,它具有构造函数的一切特点
对象方法
Object.defineProperty()
定义新属性或者修改原有属性
Object.defineProperty(obj, prop, descriptor)
obj 新增或者修改属性的对象
prop 属性名
descriptor 是一个对象,属性如下
- value 设置属性的值,默认为undefined
- writable 值是否可以重写,默认为false,设置为true则不能修改
- enumerate 目标属性是否可以被枚举,默认为false
- configurable 目标属性是否可以被删除或者可以再次修改特性,默认为false。设置一次后不能再次上午好自
var person = {
name: 'siri'
}
// way 1
// person.age = 3
// way 2
Object.defineProperty(person, 'age', {
value: 3,
// 在nodejs中下面这句是必须加的,否则遍历的时候不显示
enumerable: true,
// 不允许删除
configurable: false
})
console.log(person)
es5中新增的方法
数组方法
迭代方法:forEach() 、map() 、filter() 、some()、every()
forEach
array.forEach(function(currentValue, index, arr))
- currentValue 数组当前项的值
- index 数组当前项的索引
- arr 数组对象本身
filter
array.filter(function(currentValue, index, arr))
- filter()方法创建一个新的数组,新数组中的元素通过检查指定数组中符合条件的所有元素,主要用于筛选数组
- 该函数直接返回新的数组
- currentValue 数组当前值
- index 数组当前项的索引
- arr 数组对象本身
var arr = [12, 40, 55, 23]
var newArr = arr.filter(function (value, index) {
return value > 20
})
console.log(newArr) // [ 40, 55, 23 ]
some
array.some(function(currentValue, index, arr))
- some()方法用于检测数组中元素是否满足指定条件,即查找数组中是否有满足条件的元素
- 返回的是布尔值,查找成功返回true,失败返回false
- 找到第一个满足条件的就终止循环不再查找
- currentValue 数组当前项的值
- index 数组当前项的索引
- arr 数组对象本身
every
array.every(function(currentValue, index, arr))
- every()方法用于检测数组中元素是否都满足指定条件
- 返回的是布尔值
- currentValue 数组当前项的值
- index 数组当前项的索引
- arr 数组对象本身
map
array.map(function(currentValue, index, arr),thisValue)
- map()方法用于对数组的每个元素进行处理,返回一个新的数组
- currentValue 数组当前项的值
- index 数组当前项的索引
- arr 数组对象本身
var arr = [2, 4, 6, 8]
var newArr = arr.map(function (value) {
return value + 1
})
console.log(newArr) // [ 3, 5, 7, 9 ]
forEach()和some()的区别
注意NodeJS和JS中的不同表现
forEach()迭代遇到return不会终止(),NodeJS会
some()迭代遇到
return true
终止循环,NodeJS遇到return就终止循环
字符串
trim()
去除字符串
两边
的字符
应用场景:判断用户输入是否为空
var input = document.querySelector('input')
var btn = document.querySelector('button')
btn.onclick = function () {
if (input.value.trim() === '') {
alert('请输入内容')
}
}
startsWith()和endsWith()
这两个方法是ES6中字符串新增的方法。
startsWith()
表示参数字符串是否在原字符串的头部,返回布尔值;endsWith()
表示字符串是否在原字符串尾部,返回布尔值
let str = 'Hello World!'
console.log(str.startsWith('Hello')) // true
console.log(str.endsWith('World!')) // true
repeat()
这个方法也是ES6中的语法。方法将原字符串重复n次,返回一个新的字符串
console.log('x'.repeat(5)) // xxxxx
函数
函数的定义
使用function关键字
匿名函数
new Function()
new Function("arg1", "arg2", "函数体")
函数均为对象
函数的调用
普通函数
function fn() {
console.log('hello')
}
fn()
// 或者
fn.call()
对象的方法
var person = {
sayHi: function () {
console.log('Hi')
}
}
person.sayHi()
构造函数
new 关键字调用
function Person() {}
var p = new Person()
绑定事件的函数
由事件触发
btn.onclick()=function(){}
定时器函数
setInterval(function(){}, 1000)
立即执行函数
(function(){
console.log("Hello")
})()
函数中的this指向
调用方式 | this指向 |
---|---|
普通函数调用 | window |
构造函数调用 | 实例对象,原型对象里面的方法中this也指向实例对象 |
对象方法函数 | 该方法所属对象 |
事件绑定方法 | 绑定事件的对象 |
定时器函数 | window |
立即执行函数 | window |
改变函数内的this指向
call()
fn.call(obj,param1,param2)
实例
var o = {
name: 'he'
}
function fn() {
console.log(this)
}
fn.call(o)
apply()
fn.apply(thisArg, [argsArray])
- thisArg 在函数运行时的this指向
- argsArray 传递的值,可以为空,如果不空必须包含在数组里面
- 返回值就是函数的返回值
实例:求最值
var arr = [1, 6, 2, 7]
var max = Math.max.apply(Math, arr)
// 这里Math用null可能会出问题
console.log(max)
var min = Math.min.apply(Math, arr)
bind()
fn.bind(thisArg, arg1, arg2)
bind()方法不会调用函数,但是能改变函数内部的this指向
- thisArg 在函数运行时的this指向
- arg1, arg2 传递的参数
- 返回有指定的this值和初始化参数改造的原函数的拷贝
var o = {
name: 'andy'
}
function fn(a, b) {
console.log(a + b)
}
var newFn = fn.bind(o, 1, 2)
newFn(1, 2)
bind应用场景:需要改变this指向但不需要立即执行
var btn = document.querySelector('button')
btn.onclick = function () {
this.disabled = true
setTimeout(
function () {
this.disabled = false
}.bind(this),
2000
)
}
上面
.bind(this)
将定时器函数中的this指向由window改变为btn
this在循环中的应用
var btns = document.querySelectorAll('btns')
for (var i = 0; i < btns.length; i++) {
btns[i].onclick = function () {
this.disabled = true
setTimeout(
function () {
// btns[i].disabled = false 错误写法,会下标越界
this.disabled = false
}.bind(this),
2000
)
}
}
call apply bind总结
- call()经常用作继承
- apply()经常跟数组有关系
- bind 不调用函数但仍想改变this的指向,比如定时器内部的this指向
高阶函数
高阶函数是对其他函数进行操作的函数,它接受函数作为参数或者将函数作为返回值输出
// 将函数作为参数
function fn(callback) {
callback && callback()
}
// 将函数作为返回值
function fn() {
return function () {}
}
严格模式
JS除了正常模式外还提供严格模式。严格模式下消除了JS中语法不合理、不严谨之处,减少一些怪异行为;消除代码一些不安全因素;提高编译效率和运行速度;禁用ECMAScript未来版本中可能会定义的语法。如关键字不能作为变量名
为脚本开启严格模式
在整个脚本开启严格模式,需要在所有语句之前使用
'use strict;'
。可以加在脚本最上方,也可以加在立即执行函数上。加在立即执行函数的优点是创建独立作用域,从而不影响引入的非严格模式的文件。
<script>
'use strict;'
</script>
<!-- 加在立即执行函数上 -->
<script>
(function () {
'use strict'
})()
</script>
为函数开启严格模式
function fn() {
'use strict';
}
严格模式的变化
变量需要先声明
正常模式下,一个变量没有声明就赋值,默认是全局变量,严格模式下禁止这种用法
'use strict'
num = 10
console.log(num) // error
声明的变量不能删除
正常模式下声明的变量可以用delete删除,在严格模式下是不可行的
严格模式下this指向问题
- 正常模式下全局函数的this指向的是window,在严格模式下全局函数this指向的是undefined
'use strict'
function fn() {
console.log(this) // undefined
}
- 普通模式下没有加
new
关键字当普通函数调用,严格模式下构造函数不加new调用,this会报错。
'use strict'
function fn(name) {
this.name = name
}
var ff = new fn('aa')
console.log(ff.name) // aa
- 通过new创造的实例this指向实例本身
- 定时器this还是指向window
- 事件、对象还是指向调用者
函数参数不允许重名
函数必须声明在顶层
不允许在非函数的代码块内写函数,如if语句中
浅拷贝和深拷贝
浅拷贝只是拷贝一层,更深层次对象级别的只拷贝引用;深拷贝拷贝多层,每一级别数据都会拷贝
浅拷贝
Object.assign(target, source)
var aa = {
name: 'a',
hobbies: ['swim', 'football']
}
var bb = {}
Object.assign(bb, aa)
bb.hobbies[1] = 'sing'
console.log(aa.hobbies) // [ 'swim', 'sing' ]
console.log(bb.hobbies) // [ 'swim', 'sing' ]
浅拷贝后的对象和原来对象引用类型数据公用一个地址,会相互影响
深拷贝
var aa = {
name: 'a',
hobbies: ['swim', 'football'],
friends: {
cc: 18,
dd: 19
}
}
var bb = {}
function deepCopy(newObj, oldObj) {
for (var k in oldObj) {
// 判断属性值属于那种数据类型
var item = oldObj[k]
// 判断这个值是否是数组
if (item instanceof Array) {
newObj[k] = []
deepCopy(newObj[k], item)
} else if (item instanceof Object) {
// 判断这个值是否属于对象
newObj[k] = {}
deepCopy(newObj[k], item)
} else {
// 简单数据类型
newObj[k] = item
}
}
}
deepCopy(bb, aa)
console.log(bb)
数组也属于对象,这里需要判断是否属于数组
正则
正则表达式
用于匹配字符串中的字符组合。在JS中,正则表达式也是对象。正则表达式常见于
检索
、替换
、提取
。
正则表达式创建
利用RegExp对象方式创建
var regexp = new RegExp(/123/)
利用字面量方式创建
var rg = /123/
测试正则表达式
regexObj.test(str)
- regexObj 是正则表达式实例
- str 是要检测的文本
- 文本符合正则表达式返回true,不符合返回false
var regexp = new RegExp(/123/)
console.log(regexp.test('123')) // true
正则表达式参数
/表达式/[switch] switch(也称为修饰符),表示按照什么方式来匹配
修饰符 | 说明 |
---|---|
无 | 只匹配一个结果 |
g | 全局匹配 |
i | 忽略大小写 |
gi | 全局匹配+忽略大小写 |
正则表达式中的特殊字符
正则表达式由普通字符和特殊字符(元字符)构成。元字符是在正则表达式中具有特殊意义的专用符号
边界符
正则表达式中的边界符用来提示字符所处的位置,主要有两个字符
边界符 | 说明 |
---|---|
^ | 表示匹配行首的文本 |
$ | 表示匹配行尾的文本 |
匹配行首
var regexp = new RegExp(/^abc/)
console.log(regexp.test('abcd')) // true
console.log(regexp.test('dabc')) // false
匹配行尾
var regexp = new RegExp(/abc$/)
console.log(regexp.test('abcd')) // false
console.log(regexp.test('dabc')) // true
精确匹配
var regexp = new RegExp(/^abc$/)
console.log(regexp.test('abc')) // true
console.log(regexp.test('dabc')) // false
console.log(regexp.test('abcabc')) // false
字符类
[]
符号 | 说明 |
---|---|
[] | 只要匹配其中一个即可 |
var rg = /[abcd]/
console.log(rg.test('ddos')) // true
只有满足其中任何一个
var rg = /^[abcd]$/ // a、b、c、d中的一个
console.log(rg.test('df')) // false
[-] 方括号内部范围符
表示某个范围内的任意字符
var rg = /^[a-z]$/
console.log(rg.test('c')) // true
字符组合
var rg = /^[a-zA-Z0-9]$/
console.log(rg.test(9)) // true
[^]方括号内部取反符
表示不能出现方括号内部任意字符
var rg = /^[^abc]$/
console.log(rg.test('bcd')) // false
() 优先级
表示优先级。比如将
abc
作为整体重复三次
var correct = /^(abc){3}$/ // 正确写法
var wrong = /^abc{3}$/ // 错误写法,只是c重复三次
(|) 或者
表示几个表达式符合任何一个即可
var rg = /^abc|edg$/
console.log(rg.test('edgf')) // false
量词符号
量词符号用来设定某个模式出现的次数
量词 | 说明 |
---|---|
* | 重复零次或更多次 |
+ | 重复一次或者更多次 |
? | 重复零次或者一次 |
{n} | 重复n次 |
{n,} | 重复n次或者更多次 |
{n, m} | 重复n次到m次 |
var rg = /^a*$/ // 必须以a开头,或者为空
console.log(rg.test('')) // true
console.log(rg.test('c')) // false
案例:用户名验证
- 假定用户名规定只能为英文字母、数字、下划线或者短横线组成,并且用户名长度为6-16位
- /^[a-zA-Z0-9-_]{6,16}$/
- 当表单失去焦点时开始验证
- 如果符合正则规范,则让后面的span标签添加right类
- 如果不符合正则规范,则让后面的span标签添加wrong类
var rg = /^[a-zA-Z0-9_-]{6,16}$/
var uname = document.querySelector('.uname')
var span = document.querySelector('span')
uname.onblur = function () {
if (rg.test(this.value)) {
span.className = 'right'
span.innerHTML = '用户名输入正确'
} else {
span.className = 'wrong'
span.innerHTML = '用户名输入错误'
}
}
预定义类
预定义类是指某些常见模式的简写方式
预定义类 | 说明 |
---|---|
\d | 匹配0-9之间的任一数字,相当于[0-9] |
\D | 匹配所有0-9以外的字符,相当于[^0-9] |
\w | 匹配任意的字母、数字、下划线,相当于[A-Za-z0-9_] |
\W | 除任意的字母、数字、下划线以外的字符,相当于[^A-Za-z0-9_] |
\s | 匹配空格(包括换行符、制表符、空格符),相当于[\t\r\n\v\f] |
\S | 匹配非空格字符,相当于[^\t\r\n\v\f] |
案例:电话号码
全国座机号码:两种格式:010-12345678 或者 0530-1234567
var reg = /^\d{3}-\d{8}|\d{4}-\d{7}$/
console.log(reg.test('010-23333333'))
案例:表单验证
var regmsg = /^\d{6}$/
var msg = document.querySelector('.msg')
regExp(msg, regmsg)
function regExp(ele, reg) {
ele.onblur = function () {
if (reg.test(this.value)) {
// 符合条件
} else {
// 不符合条件
}
}
}
替换
replace()方法实现替换字符串操作,用来替换的参数可以是一个字符串或者一个正则表达式
str.replace(regexp/substr, replacement)
。如果需要替换所有匹配项,带上修饰符g
- 第一个参数 被替换的字符串或者正则表达式
- 第二个参数 替换为的字符串
- 返回值是替换完毕的字符串
var str = 'abc、abc、efg'
console.log(str.replace('abc', 'abcd')) // abcd、abc、efg
console.log(str.replace(/abc/g, 'abcd')) // abcd、abcd、efg
ES6新增语法
let
let声明的变量只在块级有效,var声明的变量没有这个特点。
for (var i = 0; i < 3; i++) {}
console.log(i) // 3
for (let j = 0; j < 3; j++) {}
console.log(j) // not defined
let声明的变量没有变量提升,只能先声明后使用
console.log(a) // Cannot access 'a' before initialization
let a = 5
console.log(a) // undefined
var a = 5
使用let声明的变量具有暂时性死区特性。在块中声明和外部相同的名称的变量,块内变量会覆盖外部变量
var num = 5
if (true) {
console.log(num) // Cannot access 'num' before initialization
let num = 10
}
面试题
var arr = []
for (var i = 0; i < 2; i++) {
arr[i] = function () {
console.log(i)
}
}
arr[0]() // 2
arr[1]() // 2
用var声明的i是全局变量,循环结束后调用函数,这时函数访问的是全局中的i,所以结果为2
var arr = []
for (let i = 0; i < 2; i++) {
arr[i] = function () {
console.log(i)
}
}
arr[0]() // 0
arr[1]() // 1
循环时产生了多个块级作用域,这些块级作用域的i的值各不相同
const
声明常量,具有块级作用域,必须赋初始值,赋值后不能更改(即内存地址不能更改)
const、var、let的比较
var | let | const |
---|---|---|
函数级作用域 | 块级作用域 | 块级作用域 |
变量提升 | 不存在变量提升 | 不存在变量提升 |
值可以更改 | 值可更改 | 值不可更改 |
解构赋值
从数组数组和对象中提取值,为变量赋值
数组解构
let [a, b, ...c] = [1, 2, 3, 4]
console.log(a) // 1
console.log(b) // 2
console.log(c) // [ 3, 4 ]
解构不成功,值为undefined
let [foo] =[]
console.log(foo) // undefined
对象解构
let person = {
name: 'aa',
age: 19
}
let { name, age } = person
console.log(name) // aa
console.log(age) // 19
对解构后的变量重新命名
let person = {
name: 'aa',
age: 19
}
let { name: myName, age: myAge } = person
console.log(myName) // aa
console.log(myAge) // 19
箭头函数
const fn = ()=>{}
const fn = () => {
console.log('hello')
}
fn() // hello
如果函数体只有一句代码,且代码执行结果就是返回值,可以省略大括号
const fn = (a, b) => a + b
console.log(fn(1, 2)) // 3
如果形参只有一个,可以省略小括号
const fn = a => a + 1
console.log(fn(1)) // 2
箭头函数中的this指向问题
箭头函数不绑定this关键字,箭头函数中的this指向的是
函数定义位置的上下文this
没有使用箭头函数
const obj = {
name: 'aa'
}
function fn() {
console.log(this) // obj
return function () {
console.log(this) // Window
}
}
const resFn = fn.call(obj)
使用了箭头函数
const obj = {
name: 'aa'
}
function fn() {
console.log(this) // obj
return () => {
console.log(this) // obj
}
}
const resFn = fn.call(obj)
面试题
var age = 100
var obj = {
age: 20,
say: () => {
console.log(this.age)
}
}
obj.say() // 100
箭头函数中的this指向的是Window,在NodeJS中是undefined
剩余参数
...args
将不定数量的参数放到一个数组中
const sum = (...args) => {
let total = 0
args.forEach((item) => (total += item))
return total
}
console.log(sum(1, 2, 3)) // 6
剩余参数和解构一起使用
let arr = [1, 2, 3]
let [aa, ...bb] = arr
console.log(aa) // 1
console.log(bb) // [ 2, 3 ]
Array
扩展运算符
扩展运算符定义
将数组或者对象转化为用逗号分隔的参数序列
let arr = [1, 2, 3]
console.log(...arr) // 1 2 3
扩展运算符用于合并数组
// 方法一
let arr1 = [1, 2, 3]
let arr2 = [4, 5, 6]
let arr = [...arr1, ...arr2]
console.log(arr) // [ 1, 2, 3, 4, 5, 6 ]
// 方法二
arr1.push(...arr2)
console.log(arr1) // [ 1, 2, 3, 4, 5, 6 ]
将伪数组转化为真正的数组
DOM元素构成的数组不是真正意义上的数组,可以通过扩展运算符将它转化为真正的数组
let lis = document.querySelectorAll('li')
lis = [...lis]
console.log(lis)
Array.from()
Array.from(arrLike, item=>{}) 将类数组或可以迭代的对象转化为真正的数组。
- 第一个参数是一个类数组元素
- 第二个参数类似于map方法,对数组每项进行处理,返回新数组
对象转化为数组
对象的键表示下标,必须有length属性
let arr = {
'0': 1,
'1': 2,
'2': 4,
length: 3
}
let newArr = Array.from(arr, (item) => item ** 2)
console.log(newArr) // [ 1, 4, 16 ]
find()
查找数组中第一个符合条件的数组成员,如果没有找到返回undefined
var arr = [
{
id: 1,
name: 'aa'
},
{
id: 2,
name: 'bb'
}
]
console.log(arr.find((item) => item.id === 2)) // { id: 2, name: 'bb' }
findIndex()
用于查找第一个符合条件的数组成员的位置,如果没有找到返回-1
let arr = [1, 2, 3, 4, 5, 6]
let index = arr.findIndex((item) => item >= 4)
console.log(index) // 3
includes()
表示某个数组是否包含给定的值,返回布尔值
let arr = [1, 2, 4, 5, 6]
console.log(arr.includes(2)) // true
模板字符串
ES6新增的创建字符串的形式,使用反引号定义
模板字符串可以解析变量
function Person(name) {
this.name = name
}
Person.prototype.sayHi = function () {
console.log(`${this.name} say Hi`)
}
let aa = new Person('aa')
aa.sayHi() // aa say Hi
模板字符串可以换行
let person = {
name: 'aa',
age: 15
}
let html = `
<div>
<span>${person.name}</span>
<span>${person.age}</span>
</div>
`
console.log(html)
模板字符串可以调用函数
const fn = () => 'fn()'
let html = `${fn()} 在模板字符串中被调用`
console.log(html) // fn() 在模板字符串中被调用
Set数据结构
Set是一种数据结构,它类似于数组,但其成员是唯一的,没有重复的值
创建Set
const s1 = new Set()
console.log(s1) // Set(0) {}
console.log(s1.size) // 0
创建Set时传递一个数组进行初始化
const s1 = new Set([1, 2, 2, 2, 3, 4, 5, 5])
console.log(s1) // Set(5) { 1, 2, 3, 4, 5 }
console.log(s1.size) // 5
利用Set进行数组去重
const arr1 = [1, 2, 2, 2, 3, 4, 5, 5]
const s1 = new Set(arr1)
const arr = [...s1]
console.log(arr) // [ 1, 2, 3, 4, 5 ]
Set实例的方法
add(value)
添加某个值,返回修改后的Set
delete(value)
删除某个值,返回一个布尔值,表示删除是否成功
has(value)
返回一个布尔值,表示该值是否为Set的成员
clear()
清除所有成员,没有返回值
const s = new Set()
console.log(s.add(1).add(2)) // Set(2) { 1, 2 }
console.log(s.delete(1)) // true
console.log(s.has(1)) // false
s.clear()
console.log(s.size) // 0
遍历Set
Set和数组一样,也拥有forEach()方法,用于对每个成员执行某种操作,没有返回值
let s = new Set([1, 2, 3, 4, 5])
s.forEach(item => {
console.log(item)
})
Promise
Promise基础
为什么使用Promise?Promise解决了异步编程的回调地狱问题。回调是JS实现异步编程的方式。以下是一个例子方便了解回调是如何工作的
function getPosts() {
setTimeout(() => {
console.log('posts fetched')
}, 1000)
}
function createPost(callback) {
setTimeout(() => {
console.log('posts created')
callback()
}, 1000)
}
createPost(getPosts)
当创建一个帖子时,我们希望更新帖子列表,这就需要用到回调函数,向
createPost
传递一个函数,在创建帖子完成后调用这个函数。下面用Promise改写代码
function getPosts() {
setTimeout(() => {
console.log('posts fetched')
}, 1000)
}
function createPost() {
return new Promise((resolve, reject) => {
setTimeout(() => {
// 异步操作的代码
console.log('post making ... ')
const error = false
if (!error) {
console.log('post created')
resolve()
} else {
reject({ msg: 'post create failed' })
}
}, 1000)
})
}
createPost()
.then(getPosts)
.catch((err) => {
console.log(err)
})
createPost
函数返回一个Promise,在这个Promise里面进行异步操作。异步操作成功则调用resolve
,失败则调用reject
。这两个回调函数都可以传递参数。如果一个函数返回的是Promise,则可以使用.then()
和.catch()
的语法,前者在异步操作成功时执行,后者在异步操作失败时执行。大多数情况下,Promise不是我们自己来构造,如使用mongoose进行数据库操作,axios、fetch进行网络请求,它们返回的结果都是Promise,我们只需处理这些Promise。
Promise.all()
用于批量处理Promise,接受一个数组。Promise.all()使用场景:多个异步操作可以同时进行,如同时查询用户A和B的资料。如果两个异步操作是有先后顺序的,则不能使用Promise.all()
const promise1 = Promise.resolve('Hello')
const promise2 = 10
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 2000, 'ok')
})
const promise4 = fetch('https://v1.hitokoto.cn').then((res) => res.json())
Promise.all([promise1, promise2, promise3, promise4]).then((values) => console.log(values))
async和await
每个异步函数都会返回一个Promise(用
async
标记的函数如果有return语句,返回结果一定是Promise),await
的对象也是一个Promise,这点非常重要
function getPosts() {
setTimeout(() => {
console.log('posts fetched')
}, 1000)
}
function createPost() {
return new Promise((resolve, reject) => {
setTimeout(() => {
// 异步操作的代码
console.log('post making ... ')
const error = false
const bb = 100
if (!error) {
console.log('post created')
resolve(error, bb)
} else {
reject({ msg: 'post create failed' })
}
}, 1000)
})
}
async function init() {
await createPost()
getPosts()
}
init()
// post making ...
// post created
// posts fetched
await createPost()
表示createPost()
执行结束后才会执行后面的代码
async/await捕获异常
使用await之后,异步代码代码执行有变得同步,可以使用try/catch语句来捕获异常
myApp.registerEndpoint('GET', '/api/firstUser', async function(req, res) {
try {
let firstUser = await getFirstUser();
res.json(firstUser)
} catch (err) {
console.error(err);
res.status(500);
}
});
没有使用await的情况
一般情况下,如果函数返回的是Promise,那么必须使用await来获取结果,除非你确实需要这个Promise,参考记住Promise
async/async的应用
async function fetchData() {
const res = await fetch('https://v1.hitokoto.cn')
const data = await res.json()
console.log(data)
}
fetchData()
async和await的好处是避免使用
.then()
和.catch()
的语法。注意fetch()
和res.json()
返回的都是Promise。
使用Promise改写回调函数
参考 异步回调函数传值问题
Promise.race()
将返回最先完成的Promise的结果。
const promise1 = new Promise((resolve, reject) => {
setTimeout(resolve, 500, 'one')
})
const promise2 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'two')
})
Promise.race([promise1, promise2])
.then((value) => {
console.log(value)
// Both resolve, but promise2 is faster
})
.catch((err) => {
console.log(err)
})
两个promise结果都是resolve,但promise2先返回,所以只有promise的结果。
Promise.race()
可用于判断请求是否超时,如用定时函数设置2s后reject,这样不管请求完成如何,只要超过2s秒,结果都是reject
const promise1 = new Promise((resolve, reject) => {
setTimeout(reject, 2000, 'one')
})
const promise2 = new Promise((resolve, reject) => {
setTimeout(resolve, 3000, 'two')
})
Promise.race([promise1, promise2])
.then((value) => {
console.log(value)
})
.catch((err) => {
console.log(err)
})
高阶函数使用Promise
const arr = [ { key: 1 }, { key: 2 }, { key: 3 } ]
const results = arr.map(async (obj) => { return obj.key; });
// document.writeln( `Before waiting: ${results}`);
Promise.all(results).then((completed) => document.writeln( `\nResult: ${completed}`));