ES6+ | 青训营笔记

159 阅读20分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天

当年记笔记图的就是全,甚至连教学视频也截图了,字也也全打上去了,当然笔记部分也有自己的思考和优化整理的地方。现在看来,这样记笔记的效率太低了,在我看来,记笔记和敲代码的时间之比应该略小于1,而且笔记应该根据自己敲得代码来整理,而不是照抄老师讲的。JS 笔记主要是 ES6+ 标准,这里讲到了更多地 API,有助于让我们以后更好地学习 React 框架(非常考验 JS 功底),这次的笔记可能比较繁琐,而且字数也挺多的。自己大概看了一遍,也算是重新学习了吧。

一 JavaScript 面向对象

1. 面对对象编程介绍

1.1 两大编程思想

面向过程、面向对象

1.2 面向过程变成 POP( Process-oriented programming )

面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候在一个一个的依次调用就可以了。

例子:将大象装进冰箱,面向过程做法:

面向过程,就是按照我们分析好了的步骤,按照步骤解决问题。

1.3 面向对象 OOP( Object Oriented Programming )

面对对象是吧事物分解成为一个个对象,然后由对象之间分工与合作

例子:把大象装进对象,面向对对象做法:

先找处对象,并写出这些对象的功能

    1. 大象对象
  • 进去
    1. 冰箱对象
  • 打开
  • 关闭
    1. 使用大象和冰箱的功能

面对对象是以对象功能来划分问题,而不是步骤。

在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工。

面向对象编程具有灵活、代码可服用、容易维护和开发的优点,更适合多人合作的大型软件项目。

面对对象的特性

  • 封装性
  • 继承性
  • 多态性

1.4 面向过程和面向对象的对比

面向过程:

  • 优点:性能比面向对象高,适合硬件联系很紧密的东西,例如单片机就采用的面向过程编程。
  • 缺点:没有面向对象易维护、易复用、易扩展

面向对象:

  • 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、国家易于维护。
  • 缺点:性能比面向过程低

用面向过程的方法写出来的程序是一份蛋炒饭,而用面向对象写出来的程序是一份盖浇饭。

2. ES6 中的类和对象

面向对象: 面向对象更贴近我们实际的生活,可以使用面向对象描述现实世界事物,但是事物分为具体的事物和抽象的事物

面向对象的思维特点:

  1. 抽取(抽象)对象共用的属性和行为组织(封装)乘一个类(模板)
  2. 对类进行实例化,获取类的对象

面向对象变成我们考虑的是有哪些对象,按照面向对象的思维特点,不断的创建对象,使用对象,指挥对象做事情。

2.1 对象

现实生活中:万物皆是对象,对象是一个具体的事物,看得见摸得着的实物。例如,一本书、一辆汽车、一个人可以是"对象",一个数据库、一个网页、一个与远程服务器的连接亦可以是"对象"。

在 JavaScript 中,对象是一组无序的相关属性和方法的集合,所有的事情都是对象,例如字符串、数值、数组、函数等。

对象是由属性和方法构成的:

  • 属性:事物的特征,在对象中用属性来表示( 常用名词 )
  • 方法:事物的行为,在对象中用方法来表示( 常用动词 )

2.2 类 class

在 ES6 中新增了类的概念,可以使用class 关键字声明一个类,之后以这个类来实例化对象。

类抽象了对象的公共部分,它泛指某一大类( class )

对象特指某一个,通过类实例化一个具体的对象。

2.3 创建类

语法:

class name {
    // class body
}

创建实例:

var xx=new name()

注意:类必须使用 new 实例化对象

2.4 类 constructor 构造函数

constructor() 方法是类的构造函数(默认方法),用于传递参数,返回实例对象,通过 new 命令生成对象实例时,自动调用该方法。如果没有显示定义,类内部会自动给我们创建一个 constructor()

注意:

  1. 通过class 关键字创建类,类名我们还是习惯性定义首字母大写
  2. 类里面有个constructor 函数,可以接受传递过来的参数,同时返回实例对象
  3. constructor 函数,只要 new 生成实例时,就会自动调用这个函数,如果我们不写这个函数,类名也会自动生成这个函数
  4. 生成实例 new bunengshenlgve
  5. 最后注意语法规范,创建类 类名后面不要加小括号,生成实例 类名后面加小括号,构造函数不需要加 function

2.5 类添加方法

语法:

class Person {
    constructor(name, age) {    // constructor 构造器或者构造函数
        this.name = name
        this.age = age
    }
    say() {
        console.log(thi.name + '你好')
    }
}

3. 类的继承

3.1 继承

现实中的继承:子承父业,比如我们都继承了父亲的姓

程序中的继承:子类可以继承父类的一些属性和方法

语法:

class Father { // 父类
    
}
class Son extends Father { // 子类继承父类
    
}

3.2 super 关键字

super 关键字用于访问和调用对象父类上的函数。可以调用父类和构造函数,也可以调用父类的普通函数。

语法:

class Person { // 父类
    constructor(surname) {
        this.surname = surname
    }
}
class Student extends Person { // 子类继承父类
    constructor(surname) {
        super(surname)        // 调用父类的constructor(surname)
        this.firstname = firstname // 定义子类独有的属性
    }
}

注意:自雷在构造函数中使用super,必须放到 this 前面(必须先调用弗雷德构造方法,在使用子类构造方法)

4. 三个注意点

  1. 在 ES6 中类没有变量提升,所以必须先有类,才能通过实例化对象
  2. 类里面的共有的属性和方法一定要加this调用
  3. 注意this的指向问题
  4. constructor 里面的this指向实例对象,方法里面的this 指向这个方法的调用者

5. 额外

5.1 insertAdjacentHTML()

element.insertAdjacentHTML()

语法:

  • element.insertAdjacentHTML(position, test)

position

一个 DOMString,表示插入内容相对于元素的位置,并且必须是以下字符串之一:

  • 'beforebegin':元素自身的前面。
  • 'afterbegin':插入元素内部的第一个子节点之前。
  • 'beforeend':插入元素内部的最后一个子节点之后。
  • 'after':元素自身的后面

text

是要被解析为HTML 或XML 元素,并插入到DOM树中的 DOMString

位置名称的可视化

<!-- beforebegin -->
<p>
    <!-- afterbegin -->
    foo
    <!-- beforeend -->
</p>
<!-- afterend -->

5.2 ondblclick

双击事件。

如果双击文字,会默认选定文字,此时需要双击禁止选中文字,使用一下代码:

window.getSelection ? window.getSelection().removeAllRanges() : document.selection.empty()

5.3 input 事件

  • input.select()

文本框文字全选

  • input.onkeyup()

监听键盘按下事件

input.onkeyup = function(e) {
    // 按下的是回车键
    if(e.keyCode === 13) {
        // 手动调用 blur() 事件,不需要鼠标离开操作
        this.blur()
    }
}

二 构造函数和原型

1. 构造函数和原型

1.1 概述

在典型的 OOP 的语言中(如 Java),都存在类的概念,类就是对象的模板,对象就是类的实例,但在 ES6之前,JS 中并没有引入类的概念。

ES6,全称 ECMAScript6.0,2015.6 发版。但是目前浏览器的 JavaScript 是 ES5 版本,大多数高版本的浏览器也支持 ES6,不过只实现了部分 ES6 的特性和功能。

new 在执行时会做四件事情

① 在内存中创建一个新的空对象。

② 让 this 指向这个新的对象。

③ 执行构造函数里面的代码,给这个新对象添加属性和方法。

④ 返回这个新对象(所以构造函数里面不需要 return)。

1.2 构造函数

JavaScript的构造函数中可以添加一些成员,可以在构造函数本身上添加,也可以在构造函数内部的 this 上添加。通过这两种方式添加的成员,就分别称为静态成员和实例对象。

  • 静态成员:在构造函数本上添加的成员称为静态成员,只能由构造函数本身来访问。
  • 实例成员:在构造函数内部创建的对象成员称为实例成员,只能由实例化的对象来访问。
// 1.实例成员就是构造函数内部通过this 添加的成员,(this.)uname age sing 就是实例成员
// 实例成员只能通过实例化的对象来访问
console.log(ldh.uname)
ldh.sing()
// console.log(Star.uname) // 不可以通过构造函数来访问实例成员
// 2.静态成员,在构造函数本身上添加的成员,sex 就是静态成员
Star.sex = '男'
// 静态成员只能通过构造函数来访问
console.log(Star.sex)
// console.log(ldh.sex) // 不能通过对象来访问

1.3 构造函数的问题

构造函数方法很好用,但是存在浪费内存的问题。

1.4 构造函数原型 prototype

构造函数通过原型分配的函数是所有对象所共享的。

JavaScript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象。注意这个 prototype 就是一个对象,这个对象的所有属性和方法,都会被构造函数所拥有。

我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。

问答

  1. 原型是什么?

一个对象,我们也称为 prototype 为原型对象。

  1. 圆形的作用是什么?

共享方法。

1.5 对象原型 proto

对象都会有一个属性 proto__指向构造函数的 prototype 原型对象。之所以我们对象可以使用构造函数 prototype 原型对象的属性和方法,就是因为对象 _ proto_ 原型的存在。

  • proto 对象原型和原型对象 prototype 是等价的
  • __protp__对象原型的意义就在于为对象的查找机制提供一个方向,或者说一条路线,但是它是一个非标准属性,因此在实际可罚中,不可以使用这个属性,它只是内部指向原型对象 prototype

1.6 constructor 构造函数

对象原型( proto ) 和构造函数( prototype ) 原型对象里面都一个属性 constructor 属性,constructor 我们成为构造函数,因为它指回构造函数本身。

constructor 主要用于记录该对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。

1.7 构造函数、实例、原型对象三者之间的关系

1.8 原型链

1.9 JavaScript 的查找机制(规则)

① 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。

② 如果没有就查找它的原型( 也就是 proto 指向的 prototype 原型对象 )。

③ 如果还没有就查找原型的像的原型 ( Object的原型对象 )。

④ 依次类推一直找到 Object 为止( null )。

proto 对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线。

1.10 原型对象this指向

function Star(uname, age) {
    this.uname = uname
    this.age = age
  }
  var that
  // 圆形函数
  Star.prototype.sing = function() {
    console.log('我会唱歌')
    that = this
  }
  var ldh = new Star('刘德华', 18)
  ldh.sing()
  console.log(that === ldh) // true
  1. 在构造函数中,里面this指向的是对象实例 ldh
  2. 原型对象函数里面的this 指向的是 实例对象 ldh

1.11 扩展内置对象

可以通过原型对象,对原来的内置对象进行扩展自定义的方法。比如给数组增加自定义求偶数和的功能。

Array.prototype.sum = function() {
    var sum = 0
    for(var i=0;i<this.length;i++) {
        if(this[i] % 2 === 0) {
            sum += this[i]
        }
    }
    return sum
}
var arr = [1,2,3,4,5]
console.log(arr.sum())

注意: 数组和字符串内置对象不能给原型对象覆盖操作 Array.prototype = {},只能是 Array.prototype.xxx = function() {} 的方式。(constructor是指回构造函数)

2. 继承

ES6 之前并没有给我们提供 extends 继承。我们可以通过构造函数+原型对象模拟实现继承,被称为组合继承。

2.1 call()

调用这个函数,并且修改函数运行时的 this 指向。

fun.call(thisArg, arg1, arg2, ...)
  • thisArg:当前调用函数 this 的指向对象
  • arg1,arg2:传递的其他参数
function fn(x, y) {
    console.log(this) // o:{ name:'andy' }
    console.log(x + y) // 3
}
var o = {
    name: 'andy'
}
fn.call(o, 1, 2)

2.2 借用构造函数继承父类型属性

核心原理:通过 call() 把父类型的 this 指向子类型的this,这样就可以实现子类型继承父类型的属性。

2.3 借用原型对象继承父类型方法

// 借用父构造函数继承属性
    // 1.父构造函数
function Father(uname, age) {
  // this 指向父构造函数的对象实例
  this.uname = uname
  this.age = age
}
Father.prototype.money = function() {
  console.log(100000)
}
// 2.子构造函数
function Son(uname, age, score) {
  // this 指向子构造函数的对象实例
  Father.call(this, uname, age)
  this.score = score
}
// Son.prototype = Father.prototype // 这样直接赋值有问题,如果修改了子原型对象,父原型对象也会跟着一起变化
// 这个是子构造函数的方法
Son.prototype = new Father()
Son.prototype.exam = function() {
  console.log('孩子要考试')
}
var son = new Son('刘德华', 18, 100)
console.log(son)
console.log(Father.prototype)

2.4 类的本质

  1. class 本质还是function。
  2. 类的所有方法都定义在类的 prototype 上。
  3. 类创建的实例,里面也有 proto 指向类的prototype原型对象。
  4. 所以ES6的类它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰,更像面向对象编程的语法而已。
  5. 所以ES6的类其实就是语法糖。
  6. 语法糖:语法糖就是一种便捷写法,简单理解,有两种方法可以实现同样的功能,但是一种写法更加清晰、方便,那么这个方法就是语法糖

3.ES5 中新增的方法

3.1 ES5 新增方法概述

ES5 中给我们新增了一些方法,可以很方便的操作数组或者字符串,这些方法主要包括:

  • 数组方法
  • 字符串方法
  • 对象方法

3.2 数组方法

迭代(遍历)方法:forEach()、map()、filter()、some()、every()

遍历数组每一项

array.forEach(function(currentValue,index,arr) {})
  • 在foreEach 里面return 不会终止迭代
  • currentValue:数组当前项的值
  • index:数组当前项的索引
  • arr:数组对象本身
array.filter(function(currentValue, index, arr) {})

filter() 方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素,主要用于筛选数组

  • 注意他直接返回一个新数组
  • currentValue:数组当前项的值
  • index:数组当前的索引
  • arr:数组对象本身
array.some(function(currentValue, index, arr) {})

some() 方法用于检测数组中的元素是否满足指定条件,通俗点 查找数组中是否有满足条件的元素

  • 注意它返回值是布尔值,如果查找到这个元素,就返回 true,如果查找不到就返回false
  • 在 some 里面遇到return true 就会终止遍历,迭代效率高
  • 如果第一个满足条件的元素,则终止循环,不再继续查找
  • currentValue:数组当前项的值
  • index:数组当前项的索引
  • arr:数组对象本身

3.3 字符串方法

trim() 方法会从一个字符串的两端删除空白字符。

str.trim()

trim() 方法并不影响原字符串本身,它返回的是一个新的字符串。

3.4 对象方法

  1. Object.keys() 用于获取对象自身所有的属性
Object.keys(obj)
  • 效果类似于 for...in
  • 返回一个由属性名组成的数组
  1. Object.defineProperty() 定义对象中新属性或修改原有的属性。
Object.defineProperty(obj, prop, descriptor)
  • obj:必需,目标对象
  • prop:必需,需定义或修改的属性的名字
  • descriptor:必需,目标属性所拥有的特性

Object.defineProperty() 第三个参数 descriptor 说明:以对象形式 { } 书写

  • value:设置属性的值 默认为undefined
  • writable:值是否可以重写。 true | false 默认为false
  • enumerable:目标属性是否可以被枚举。true | false 默认为 false
  • configurable:目标属性是否可以被删除或是否可以再次修改特性 true | false 默认为 false

三 函数进阶

1.函数的定义和调用

1.1 函数声明方式 function 关键字(命令函数)

  1. 函数声明方式 function 关键字(命令函数)
  2. 函数表达式(匿名函数)
  3. new Function()

var fn = new Function('参数1', '参数2', '函数体')

  • Function 里面的参数都必须是字符串格式
  • 第三种方式执行效率低,也不方便书写,因此较少使用
  • 所有函数都是 Function 的实例(对象)
  • 函数也属于对象

1.2 函数的调用方式

  1. 普通函数
  2. 对象的方法
  3. 构造函数
  4. 绑定事件函数
  5. 定时器函数
  6. 立即执行函数

2. this

2.1 函数内 this 的指向

这些 this 的指向,是当我们调用函数的时候确定的。调用方式的不同决定了this 的指向不同,一般指向我们的调用者。

调用方式this 指向
普通函数调用window
构造函数调用实例对象,原型对象里面的方法也指向实例对象
对象方法调用该方法所述对象
事件绑定方法绑定事件对象
定时器函数window
立即执行函数window

2.2 改造函数内部 this 指向

JavaScript 为我们专门提供了一些函数方法来帮我们更优雅的处理函数内部 this 的只想问题,常用的有 bind()、call()、apply() 三种方法。

2.2.1 call 方法

call() 方法调用一个对象。简单理解为调用函数的方式,但是它可以改变函数的 this 指向。

fun.call(thisArg, arg1, arg2, ...)

2.2.2 apply 方法

apply() 方法调用一个函数,简单理解为调用函数的方式,但是它可以改变函数的 this 指向。

fun.apply(thisArg, [argsArray])
  • thisArg:在 fun 函数运行时指定的 this 值
  • argsArray:传递的值,必须包含在数组里面
  • 返回值就是函数的返回值,因为它就是调用函数

应用:

var arr = [1, 2, 3, 4, 5, 6, 7]
var max = Math.max.apply(Math, arr) // 相当于 Math.max(1, 2, 3, 4, 5, 6, 7)

2.2.3 bind 方法

bind() 方法不会调用函数,但是能改变函数内部this 指向

fun.bind(thisArg, arg1, arg2, ...)
  • thisArg:在 fun 函数运行时指定的 this 值
  • arg1 arg2:传递的其他参数
  • 返回由指定的this 值和初始化参数改造的原函数对象拷贝

应用:

如果函数不需要我们立即调用(计时器),但又希望改变this 的指向,可以用bind函数

// 延迟三秒按钮恢复正常
var btn = document.querySelector('button')
btn.onclick = function() {
    this.disabled = true
    setTimeout(function() {
       this.disabled = false 
    }.bind(this), 3000) // 这个this 指向的是btn 这个对象,apply()和call()会立即调用,所以不合适
}

2.3 call apply bind 总结

相同点:

都可以改变函数内部的this指向。

区别点:

  1. call 和 apply 会调用函数,并且改变函数内部this指向。
  2. call 和 apply 传递的参数不一样,call传递参数 aru1,aru2...形式,apply 必须数组形式 [arg]
  3. bind 不会调用函数,可以改变函数内部this指向

主要应用场景:

  1. call 经常做继承
  2. apply 经常和数组有关系,比如借助于数学对象固始县数组最大值最小值
  3. bind 不调用桉树,但是还想改变this指向,比如改变定时器内部this指向

3. 严格模式

1. 什么是严格模式

JavaScript 除了提供正常模式外,还提供了雅阁模式(strict mode)。ES5 的严格模式是采用具有限制性JavaScript辩题的一种方式,即在严格的条件下运行 JS 代码。

严格模式在 IE10 以上版本的浏览器才会被支持,旧版本浏览器中会被忽略。

雅阁模式对正常的 JavaScript 语义做了修改:

  1. 消除了 JavaScript 语法的一些不合理、不严谨只出,减少了一些怪异行为。
  2. 消除了代码运行一些不安全之处,保证代码运行的安全。
  3. 提高编译器效率,增加运行速度。
  4. 禁用了在ECMAScript的未来版本可能会定义的一些语法,为未来新版本的JavaScript做好铺垫。比如一些保留字,如:class、enum、export、extends、import、super 不能做变量名

2. 开启严格模式

严格模式可以应用到整个脚本或个别函数中。因此在使用时,我们可以将严格模式分为脚本开启严格模式和为函数开启严格模式两种情况。

2.1 为脚本开启严格模式

为整个脚本开启严格模式,需要在所有语句之前放入一个特定于巨“use strict”;(或 'use strict';)

<script>
	"use strict";
    consol.log('这是严格模式')
</script>

有的script基本是严格模式,有的script脚本是正常模式,这样不利于文件合并,所以可以将整个脚本文件放在一个立即执行的匿名函数之中:

<script>
	(function() {
       "use script";
        var num = 10
        function fn () {}
    })()
</script>

2.2 为函数开启严格模式

要给某个函数开启严格模式,需要把"user strict";(或"use strict";)声明放在函数体所有语句之前。

3. 严格模式中的变化

严格模式对JavaScript的语法或行为,都做了一些改变。

3.1 变量规定

  1. 在正常模式中,如果一个变量没有声明就赋值,默认是全局变量。严格模式禁止这种用法,变量都必须先用 var 命令声明,然后再使用。
  2. 严禁删除已经声明的变量。例如,delete x;语法是错误的。

3.2 严格模式下 this 指向的问题

  1. 以前在全局作用域函数中的this指向window对象。
  2. 严格模式下全局作用域中函数中的this是undefined。
  3. 以前构造函数不加new也可以调用,当普通函数,this指向全局对象。
  4. 严格模式下,如果构造函数不加new调用,this 会报错。
  5. new 实例化的构造函数指向创建的对象实例。
  6. 定时器this还是指向window。
  7. 事件、对象还是指向调用者。

3.3 函数变化

  1. 函数不能有重名的参数。
  2. 函数必须生命在顶层,新版本的JavaScript会引入“块级作用域”(ES6中已引入)。为了与新版本接轨,不允许在费函数的代码块内声明函数。

更多严格模式要求参考:developer.mozilla.org/zh-CN/docs/…

4.高阶函数

高阶函数是对其他函数进行操作的函数,它接收函数作为参数或将函数作为返回值输出。

<script>
function fn(callback) {
    callback&&callback()
}
fn(function(){ alert('hi') })
</script>
<script>
function fn() {
    return function() {}
}
fn()
</script>

此时fn 就是一个高阶函数

函数也是一种数据类型,同样可以作为参数,传递给另外一个参数使用。最典型的就是作为回调函数。

五 闭包

1. 变量作用域

变量根据作用域的不同分为两种:全局变量和局部变量。

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

2. 什么是闭包

闭包(closure)指有权访问另一个函数作用域中变量的函数。——————JavaScript 高级程序设计

简单理解就是,一个作用于可以访问另一个函数内部的局部变量。

<script>
function fn1() { // fn1 就是闭包函数
    var num = 10
    function fn2() {
        console.log(num) // 10
    }
    fn2()
}
fn1()
</script>

3. 在 chrome 中调试闭包

  1. 打开浏览器,按 F12 键启动 chrome 调试工具。
  2. 设置断点。
  3. 找到 Scope 选项(Scope 作用域的意思)。
  4. 当我们重新刷新页面,会进入断点调试,Scope 里面会有两个参数(global 全局作用域、local 局部作用域)。
  5. 当执行到 fn2() 时,Scope 里面会多一个 Closure 参数 ,这就表明产生了闭包

4. 闭包的作用

延伸了变量的作用范围

<script>
function fn() {
    var num = 10
    return function {
        console.log(num) // 10
    }
}
var f = fn()
f()
</script>

5. 闭包案例

  1. 循环注册点击事件。
var lis = document.querySelector('ul').querySelectorAll('li')
for(var i=0; i<lis.length; i++) {
	lis[i].onclick = function() {
        console.log(i) // 这是在回调函数内,循环执行完毕后才会调用,所有获取不到每个li的索引值
    }
}
  1. 循环中的 setTimeout()。
var lis = document.querySelector('ul').querySelectorAll('li')
for(var i=0; i<lis.length; i++) {
    lis[i].onclick = function() {
        console.log(lis[i].innerHTML) // 任务队列最后执行,i此时为4
    }
}
  1. 计算打车价格。
var car = (function() {
	var start = 13
	var total = 0
    return {
        price: function(n) {
            if(n <= 3) {
                total = 13
            }
            else {
                total = start + (n-3)*5
            }
            return total
        },
        yd: function(flag) {
            return flag ? total+10:total
        }
    }
})()
console.log(car.price())

6. 闭包总结

6.1 闭包是什么?

闭包是一个函数(一个作用域可以访问另外一个函数的局部变量)

6.2 闭包的作用是什么?

衍生变量的作用范围

7. 递归

1. 什么是递归?

如果一个函数内可以调用其本身,那么这个函数就是递归函数。

简单理解:函数内部自己调用自己,这个函数就是递归函数。

递归函数的作用和循环效果一样。

由于递归很容易发生“栈溢出”错误( stack overflow ),所以必须要加退出条件 return。

2. 利用递归求数学题

  1. 求 1234....*n
function fn(n) {
    if(n === 1) {
        return 1
    }
    return n * f(n-1)
}
  1. 求斐波那契数列
function fn(n) {
    if(n === 1 || n===2) {
        return 1
    }
    return fn(n-1) + fn(n-2)
}
  1. 根据id返回对应的数据对象

3. 浅拷贝与深拷贝

  1. 浅拷贝只是拷贝一层,更深层对象级别的只拷贝引用。
Object.assign(o, obj)
// o 是拷贝的对象,obj是拷贝模板,assign还是浅拷贝对象
  1. 深拷贝拷贝多层,每一级别的数据都会拷贝。
var obj = {
    name: 'ldh',
    gender: '男',
    color: ['red', 'blue', 'green'],
    song: {
        name1: '冰雨',
        name2: '笨小孩'
    }
}
function deepCopy(o, obj) {
	// 循环
    for(var k in obj) {
        // 1.获取数值
        var item = obj[k] // 属性值
        // 判断数据类型
        // 2.数组
        if(item instanceof Array) {
            o[k] = []
            deepCopy(o[k], item)
        }
		// 3.对象(注意数组也属于Object,所以先判断Array)
        else if(item instanceof Object) {
            o[k] = {}
            deepCopy(o[k], item)
        }
       	// 4.简单数据类型
        else {
            o[k] = item
        }
    }
}
deepCopy(o, obj)

四 正则表达式

1. 正则表达式概述

1.1 什么是正则表达式

正则表达式( Regular Expression )是用于匹配字符串中字符组合的模式。在 JavaScript中,正则表达式也是对象。

正则表达式通常用来检索、替换那些复合某个模式(规则)的文本,例如验证表单:用户名表单只能输入英文字母、数字或者下划线,名呢输入框中可以输入中文(匹配)。此外,正则表达式还常用于过滤掉页面内容中的一些敏感词(替换),或从字符串中获取我们想要的特定部分(提取)等。

1.2 正则表达式的特点

  1. 灵活性、逻辑性和功能性非常强。
  2. 可以迅速地用极简单的方式达到字符串的的复杂控制。
  3. 比较晦涩难懂
  4. 实际开发,一般都是直接复制写好的正则表达式,但是具体要求还要进行一定修改。

比如

^\w+([-+.]\w+) @\w+([-.]\w+) .\w+([-.]\w+)*$

/^[a-z0-9_-]{3,16}$/

2.正则表达式在 JavaScript 中的使用

2.1 创建正则表达式

在 JavaScript 中,可以通过两种方式创建一个正则表达式:

1.通过调用 RegExp 对象的构造函数创建

var name = new RegExp(/123/)

2.通过字面量创建

var name = /123/

// 注释中间放表达式就是正则字面量

2.2 测试正则表达式 test

test() 正则对象方法,用于检测字符串是否符合该规则,该对象会返回 true 或 false,其参数是测试字符串。

regexObj.text(str)
  1. regexObj 是写的正则表达式。
  2. str 我们要测试的文本。
  3. 就是检测str文本是否符合我们写的正则表达式规范。

3. 正则表达式中的特殊字符

3.1 正则表达式的组成

一个正则表达式可以由简单的字符构成,比如/abc/,也可以是简单和特殊字符的组合,比如/ab*c/。其中特殊字符也被称为元字符,在正则表达式中具有特殊意义的专用符号,如^、$、+ 等。

特殊字符非常多,可以参考:

特殊字符非常多,可以参考:

3.2 边界符

正则表达式中的边界符(位置符)用来提示字符所处的位置,主要有两个字符。

边界符说明
^表示匹配首行的文本(以谁开始)
$表示匹配行尾的文本(以谁结束)

注意:

  1. 正则表达式里面不需要加引号,不管是数字型还是字符串型。
  2. 如果 ^ 和 $ 在一起,表示必须是精确匹配。

3.3 字符串

字符类表示有一列字符可供选择,只要匹配其中一个就可以了。所有可供选择的字符都放在方括号内。

  1. []方括号
/[abc]/.test('andy')

后面的字符只要包含字符串 abc,都返回 true。

  1. [-]方括号内部范围符-
/^[a-z]$/.test('andy-red')

方括号内部加上 - 表示范围,这里表示 a 到 z 26个英文字母都可以。

  1. 字符组合和取反符
/[^a-zA-Z0-9-_]/.test('andy-red') // []内的^表示取反的意思,与边界符区分开!

3.4 量词符

量词符用来设定某个模式出现的次数

量词说明
*重复零次或更多次
+重复一次或更措辞
?重复零次或一次
{n}重复n次
{n,}重复n次或更多次
{n,m}重复n到m次

具体例子:

var reg = /^[a-zA-Z0-9_-]{6,16}$/ // 这种模式可以重复6~16次

3.5 案例

功能需求:

  1. 如果用户名输入合法, 则后面提示信息为 : 用户名合法,并且颜色为绿色
  2. 如果用户名输入不合法, 则后面提示信息为: 用户名不符合规范, 并且颜色为绿色
<style>
    .left {
        color: red;
    }
    .right {
        color: green;
    }
</style>
<body>
    <input type="test" class="uname"><span class="messsage">请输入用户名</span>
</body>
<script>
	var uname = document.querySelector('.uname')
    var message = document.querySelector('.message')
    var reg = /^[a-zA-Z0-9_-]{6,16}$/
    uname.onblur = function() {
        if(reg.test(this.value)) {
            uname.className = 'right'
            uname.innerHTML = '用户名格式正确'
        }
        else {
            uname.className = 'left'
            uname.innerHTML = '用户名格式错误'
        }
    }
</script>

3.6 括号总结

  1. 大括号 量词符:里面表示重复字数。
  2. 中括号 字符集合:匹配方括号中的任意字符。
  3. 小括号 表示优先级

在线测试:c.runnob.com/

3.7 预定义类

预定类说明
\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}$/

4. 正则表达式中的替换

4.1 replace 替换

replace() 方法可以实现替换字符串操作,用来替换的参数可以是一个字符串或是一个正则表达式。

stringObject.replace(regexp/substr,replacement)
  1. 第一个参数:被替换的字符串 或者 正则表达式
  2. 第二个参数:替换为的字符串
  3. 返回值是一个替换完毕的新字符串

4.2 正则表达式参数

/表达式/[switch]
// 例如 /激情/g

switch(也被称为修饰符)按照什么样的模式来匹配,有三种值:

  • g:全局匹配
  • i:忽略大小写
  • gi:全局匹配 + 忽略大小写

五 ES6

1.ES6 新增语法

ES6中新增的用于声明变量的关键字。

1.1 let

  • let声明的变量只在所处于的块级有效
if(true) {
    let a = 10
}
console.log(a) // a is not defined

注意:使用let关键字声明的变量才具有块级作用域,使用var 声明的变量不具有这个特点。

  • 不存在变量提升
console.log(a) // a is not defined
let a = 20
  • 暂时性死区
var tmp = 123
if(true) {
	tmp = '123'
	let tmp
} 

1.2 let 经典面试题

var arr = []
for(var i = 0; i < 2; i++) {
    arr[i] = function() {
        console.log(i)
    }
}
arr[0]()
arr[1]()

var arr = []
for(let i = 0; i < 2; i++) {
    arr[i] = function() {
        console.log(i) // let 每次循环
    }
}
arr[0]()
arr[1]()

此题关键在于每次循环都会产生一个块级作用域,每个块级作用域中的变量都是不同的,函数执行时输出的是自己上一级(循环产生的块级作用域)作用域下的值。

1.3 const

作用:声明常量,常量就是值(内存地址)不能变化的量。

  • 具有块级作用域
if(true) {
    const a = 10
}
console.log(a) // a is not defined
  • 使用常量时必须赋值
const PI // Missing initalizer in const declaration
  • 常量赋值后,值不能修改。
const PI = 10
pi = 10 // Assignment to constant variable
const arr = [100,200]
arr[0] = 'a'
arr[1] = 'b'
console.log(arr) // ['a', 'b']
ary = ['a', 'b'] // Assignment to constant variable

1.4 let、const、var 的区别

  1. 使用 var 声明的变量,其作用域为该语句所在的函数内,且存在变量提升现象。
  2. 使用 let 声明的变量,其作用域为该语句所在的代码块内,不存在变量提升。
  3. 使用 const 声明的是常量,在后面出现的代码中不能再修改该常量的值。

2.解构赋值

ES6中允许从数组中提取值,按照对应位置,对变量赋值。对象也可以实现解构。

2.1 数组解构

let a[a, b, c] = [1, 2, 4]
console.log(a)
console.log(b)
console.log(c)

如果结构不成功,变量的值为 undefined

let [foo] = []
let [bar, foo] = [1]

2.2 对象解构

let person = { name:'zhangsan', age:20 }
let { name, age } = person
console.log(name) // 'zhangsan'
console.log(age) // 20
let { name:Myname, age: Myage} = person // myName,myAge 属于别名
console.log(Myname) // 'zhangsan'
console.log(Myage)

3. 箭头函数

ES6中新增的定义函数的方式,为了简化函数声明操作。

() => {}
const fn = () => {
    
}

函数体中只有一句代码,且代码执行结果就是返回值,可以省略大括号

// 传统方式
function sum(n1, n2) {
    return n1 + n2
}
const sum = (num1, num2) => num1 + num2

如果形参只有一个,可以省略小括号

function fn(a) {
    return a
}
const fn = a => a

箭头函数不绑定this关键字,箭头函数中的this,指向的是函数定义位置的上下文的关键字

const obj = { name:'张三' }
function fn() {
    console.log(this)
    return () => {
        console.log(this)
    }
}
const resFn = fn.call(obj)
resFn()

4. 剩余参数

剩余参数语法允许我们将一个不定数量的参数表示为一个数组。

function sum (first, ...args) {
    console.log(first) // 10
    console.log(args) // [20,30]
}
sum(10, 20, 30)

剩余参数和结构配合使用

let stu = ['wangwu', 'zhangsan', 'lisi']
let [s1, ...s2] = students
console.log(s1) // 'wangwu'
console.log(s2) // ['zhangsan', 'lisi']

5. Array 的扩展方法

5.1 扩展运算符(展开语法)

扩展运算符可以将数组或者对象转为用逗号分隔的参数序列。

let arr = [1, 2, 3]
...arr // 1, 2, 3
console.log(...arr) // 1 2 3 // 逗号被当成console.log的分隔符了
console.log(1, 2, 3) // 相当于以上代码

5.2 扩展运算符的应用

5.2.1 ...

合并数组

// 方法一
let arr1 = [1, 2, 1]
let arr2 = [3, 4, 3]
let arr3 = [...arr1, ...arr2]
// 方法二
arr1.push(...arr2)

将伪数组或可遍历对象转换成真正的数组

let divs = document.querySelectorAll('div')
divs = [...div]

5.2.2 构造函数 Array.from()

将类数组或可遍历对象转换为真正的数组

let arrayLike = {
    '0': 'a',
    '1': 'b',
    '2': 'c'.
    length: 3
}
let arr2 = Array.from(arrayLike) // ['a', 'b', 'c']

方法还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。

let arrayLike = {
	'0': 1,
	'1': 2,
	'length': 2
}
let newAry = Array.from(aryLike, item => item * 2)

5.2.3 实例方法: find()

用于找出第一个符合条件的数组成员,如果没有找到返回undefined

let arr = [{
    id: 1,
    name: '张三'
}, {
    id: 2,
    name: '李四'
}]
let target = arr.find((item, index) => item.id === 2)

5.2.4 实例findIndex()

用于找出第一个符合条件的数组成员的位置,如果没有就返回 -1

let arr = [1, 5, 10, 15]
let index = arr.findIndex((value, index) => value >9)
console.log(index)

5.2.5 实例方法:includes()

表示某个函数是否包含给定的值,返回布尔值。

[1, 2, 3].include(2) // true
[1, 2, 3].include(3) // false

6.String 的扩展方法

6.1 模板字符串

ES6新增的创建字符串的方式,使用反引号定义。

let name = `zhangsan`

模板字符串中可以解析变量。

let name = '张三'
let sayHello = `hello, my name is ${name}` // hello, my name is zhangsan

模板字符串可以换行。

let result = {
    name: 'zhangsan',
    age: 20,
    sex: '男'
}
let html = `<div>
	<span>${result.name}</span>
	<span>${result.age}</span>
	<span>${result.sex}</span>
</div>`

在模板字符串中可以调用函数,

const sayHello = function() {
    return `嗨嗨嗨`
}
let greet = `${sayHello() 哈哈哈}`
console.log(greet) // 嗨嗨嗨 哈哈哈

6.2 实例方法

6.2.1 startWith() 和 endsWith()

  • startsWith() 表示参数字符串是否在原字符串的头部,返回布尔值
  • endsWith() 表示参数字符串是否原字符串的尾部,返回布尔值
let str = 'Hello'
str.startWith('Hello') // true
str.endsWith('!') // true

6.2.2 repeat()

repeat方法表示将原字符串重复n次,发返回一个新字符串。

'x'.repeat(3) // 'xxx'
'hello'.repeat(2) // 'hellohello'

7. Set 数据结构

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

Set本身是一个构造函数,用来生成 Set 数据结构。

const s = new Set()

Set函数可以接受一个数组作为参数,用来初始化。

const set = new Set([1, 2, 3, 4])

7.1 实例方法

  • add(value) 添加某个值,返回 Set结构本身
  • delete(value) 删除某个值,返回一个布尔值,表示删除是否成功
  • has(value) 返回一个布尔值,表示该值是否为 Set 的成员。
  • clear() 清除所有成员,没有返回值
const s = new Set()
s.add(1).add(2).add(3) // 向 set 结构中添加值
s.delete(2) // 删除 set 结构中的2值
s.has(1) // 表示 set 结构中是否有1这个值,返回布尔值
s.clear() // 清除 set 结构中的所有值

7.2 遍历

Set 结构的实例与数组一样,也拥有forEach方法,用于对每个对象执行某种操作,没有返回值,

s.forEach(item => console.log(item))