JS进阶

闭包

变量作用域

变量根据作用域不同可以将函数分为全局变量和局部变量

  1. 函数内部可以使用全局变量
  2. 函数外部不可以使用局部变量
  3. 函数执行完毕后,本作用域的局部变量会销毁

在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换成constlet或者不加任何修饰符,结果均报错

类和对象

类继承和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

原型链查找机制
  1. 当访问一个对象属性(方法)时,首先查找这个对象自身有没有该属性
  2. 如果没有就查找它的原型(也就是_proto_指向的prototype原型对象)
  3. 如果没有就查找原型对象的原型(Object原型对象)
  4. 以此类推,一致找到Object为止
  5. 查找遵循就近原则
原型对象里面的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 是一个对象,属性如下

  1. value 设置属性的值,默认为undefined
  2. writable 值是否可以重写,默认为false,设置为true则不能修改
  3. enumerate 目标属性是否可以被枚举,默认为false
  4. 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))
  1. currentValue 数组当前项的值
  2. index 数组当前项的索引
  3. arr 数组对象本身
filter
array.filter(function(currentValue, index, arr))
  1. filter()方法创建一个新的数组,新数组中的元素通过检查指定数组中符合条件的所有元素,主要用于筛选数组
  2. 该函数直接返回新的数组
  3. currentValue 数组当前值
  4. index 数组当前项的索引
  5. 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))
  1. some()方法用于检测数组中元素是否满足指定条件,即查找数组中是否有满足条件的元素
  2. 返回的是布尔值,查找成功返回true,失败返回false
  3. 找到第一个满足条件的就终止循环不再查找
  4. currentValue 数组当前项的值
  5. index 数组当前项的索引
  6. arr 数组对象本身
every
array.every(function(currentValue, index, arr))
  1. every()方法用于检测数组中元素是否都满足指定条件
  2. 返回的是布尔值
  3. currentValue 数组当前项的值
  4. index 数组当前项的索引
  5. arr 数组对象本身
map
array.map(function(currentValue, index, arr),thisValue)
  1. map()方法用于对数组的每个元素进行处理,返回一个新的数组
  2. currentValue 数组当前项的值
  3. index 数组当前项的索引
  4. 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中的不同表现

  1. forEach()迭代遇到return不会终止(),NodeJS会

  2. 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])
  1. thisArg 在函数运行时的this指向
  2. argsArray 传递的值,可以为空,如果不空必须包含在数组里面
  3. 返回值就是函数的返回值

实例:求最值

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指向

  1. thisArg 在函数运行时的this指向
  2. arg1, arg2 传递的参数
  3. 返回有指定的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总结
  1. call()经常用作继承
  2. apply()经常跟数组有关系
  3. 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指向问题
  1. 正常模式下全局函数的this指向的是window,在严格模式下全局函数this指向的是undefined
'use strict'
function fn() {
  console.log(this) // undefined
}
  1. 普通模式下没有加new关键字当普通函数调用,严格模式下构造函数不加new调用,this会报错。
'use strict'
function fn(name) {
  this.name = name
}
var ff = new fn('aa')
console.log(ff.name) // aa
  1. 通过new创造的实例this指向实例本身
  2. 定时器this还是指向window
  3. 事件、对象还是指向调用者
函数参数不允许重名
函数必须声明在顶层

不允许在非函数的代码块内写函数,如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)

  1. regexObj 是正则表达式实例
  2. str 是要检测的文本
  3. 文本符合正则表达式返回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
案例:用户名验证
  1. 假定用户名规定只能为英文字母、数字、下划线或者短横线组成,并且用户名长度为6-16位
  2. /^[a-zA-Z0-9-_]{6,16}$/
  3. 当表单失去焦点时开始验证
  4. 如果符合正则规范,则让后面的span标签添加right类
  5. 如果不符合正则规范,则让后面的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

  1. 第一个参数 被替换的字符串或者正则表达式
  2. 第二个参数 替换为的字符串
  3. 返回值是替换完毕的字符串
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=>{}) 将类数组或可以迭代的对象转化为真正的数组。

  1. 第一个参数是一个类数组元素
  2. 第二个参数类似于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}`));
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,324评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,356评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,328评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,147评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,160评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,115评论 1 296
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,025评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,867评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,307评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,528评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,688评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,409评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,001评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,657评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,811评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,685评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,573评论 2 353