2025年前端面试复习 - JavaScript篇(一)

526 阅读56分钟

image.png

1. JavaScript中的数据类型

image.png

JavaScript中数据类型分为基本数据类型复杂数据类型

  • 基本数据类型(6种)

    • Number(数值)
    • Boolean(布尔值)
    • String(字符串)
    • undefined(未定义)
    • null
    • symbol
  • 引用数据类型(3种)

    • Object
    • Array
    • Function

1.1 基本数据类型

1.1.1 Number(数值)

let intNum = 55 // 10进制的55
let num1 = 071 // 8进制的57
let hexNum = 0xB // 16进制的11

浮点数类型:

let floatNum = 1.0
let floatNum1 = 3.125e7

特殊数值NaN,意为“不是值”,用于表达本来要返回数值的操作失败了(而不是抛出错误)

1.1.2 Undefined

当使用 varlet 声明变量但没有初始化,就相当于给变量赋undefined

let message; // 变量message 被声明,未赋值
console.log(message == undefined) // true

1.1.3 String

let firstName = "John";
let lastName = 'Jacob';
let aticle = `
    abc
    abc
    abc
`

1.1.4 Null

逻辑上讲, null值表示一个空对象指针。(typeof 传一个 null 会返回 "object" 的 原因)

1.1.5 Boolean

通过Boolean 可以将其他类型的数据转化成布尔值:

数据类型转换为 true 的值转换为false的值
String非空字符串“”
Number非零数值(包括无穷值)0, NaN
Object任意对象null
UndefinedN/A(不存在)undefined

1.1.6 Symbol(符号)

符号实例唯一不可变的。

用途: 确保对象属性使用唯一标识符,不会发生属性冲突的危险

1.2 引用数据类型

统称为Object

1.2.1 Object

常用 对象字面量表示法,属性名可以是字符串或数值

let person = {
    name: "NIcholas",
    "age": 29,
    5: true
}

1.2.2 Array

JavaScript 数组是一组有序的数据,但跟其他语言不同的是,数组中每个槽位可以存储任意类型的数据。并且,数组也是动态大小的,会随着数据添加而自动增长。

let colors = ["red", 2, {age: 20}]
colors.push(2)

1.2.3 Function

函数实际上是对象,每个函数都是Function类型的实例,并且Function也有属性和方法。 函数存在三种常见的表示方法:

  • 函数声明
// 函数声明
function sum(a, b) {
    returen a + b;
}
  • 函数表达式
// 函数表达式
let sum = function (a, b) {
    returen a + b;
}
  • 箭头函数
// 函数表达式
let sum = (a, b) => {
    returen a + b;
}

1.2.4 其他引用类型

除了上述说的三种以外,还包括 Date、RegExp、Map、Set等

1.3 存储区别

  • 基本数据类型存储在栈中
  • 引用数据类型存储在堆中

1.3.1 基本数据类型

let a = 10;
let b = a; // 赋值操作
b = 20;
console.log(a); // 10值

a 的值为⼀个基本类型,是存储在栈中,将 a 的值赋给 b ,虽然两个变量的值相等,但是两个变量保存了两个不同的内存地址。

image.png

1.3.2 引用数据类型

引⽤类型数据存放在堆中,每个堆内存对象都有对应的引⽤地址指向它,引⽤地址存放在栈中。

image.png

1.4 小结

  1. 声明变量时不同的内存地址分配:

    • 基本数据类型的值存在栈中,在栈中存放的是对应的值
    • 引用数据类型的值存在堆中,在栈中存放的是指向堆内存的地址
  2. 不同的类型数据导致赋值变量时的不同:

    • 基本数据类型赋值,是生成相同的值,两个对象对应不同的地址
    • 引用数据类型赋值,是将保存对象的内存地址赋值给另一个变量。也就是两个变量指向堆内存中同一个对象

2. 常见的DOM操作

image.png

DOM: 文档对象模型,是HTMLXML文档的‘编程接口’

<html>
 <div>
     <p title="title">
         content
     </p>
 </div>
</html>

上述结构中, html、div p为元素节点,content 为文本节点,title为属性节点

2.1 操作

相关库 Jquery zepto等库来操作DOM。 之后 在Vue React Angular 等框架出现之后,通过数据来控制DOM,越来越少的去直接操作DOM。 DOM操作有助于了解框架深层的内容。

常见的DOM操作

  • 创建节点
  • 查询节点
  • 更新节点
  • 添加节点
  • 删除节点

2.1.1创建节点

2.1.1.1 createElement

创建新元素,接收一个参数,即想要创建元素的标签名。

const div1 = document.createElement('div');
2.1.1.2 createTextNode

创建一个文本节点

const div1 = document.createTextNode('content');
2.1.1.3 createDocumentFragment

创建一个文档碎片,表示一种轻量级的文档,主要用来存储临时节点,然后把文档碎片的内容一次性添加到DOM中。

const fragment = document.createDocumentFragment();

当请求把一个DocumentFragment节点插入文档树时,插入的不是DocumentFragment 自身,而是它的所有子孙节点。

2.1.1.4 createAttribute

创建属性节点,可以是自定义属性。

const dataAttribute = document.createAttribute();

当请求把一个DocumentFragment节点插入文档树时,插入的不是DocumentFragment 自身,而是它的所有子孙节点。

3.1.2获取节点

3.1.2.1 querySelector

传入有效的 css选择器,即可选中单个DOM元素(首个):

docuemtn.querySeletor('.element')
docuemtn.querySeletor('#element')
docuemtn.querySeletor('div')
docuemtn.querySeletor('[name="username"]')
docuemtn.querySeletor('div + p > span')

如果页面上没有符合的元素时,返回null

3.1.2.1 querySelectorAll

返回一个包含节点子树内所有与之匹配的 Element 节点列表,如果没有相匹配的,则返回一个空节点列表:

const notLive = document.querySelectorAll('p')

需要注意的是,该方法返回的是一个 NodeList的静态实例,而非“实时”的查询。 关于 DOM 元素的查询方法还有如下几种:

document.getElementById('#id')
document.getElementByClassName('.classname') // 指定classname 的元素对象集合
document.getElementByTagName('div') // 指定标签名元素对象的集合
document.getElementByName('name属性值')// 包含 对应 name属性值 对应的对象集合
... ...

除此以外,每个DOM 还有 parentNode、childrenNodes、firstChild、lastChild、nextSibling、previousSibling 属性

image.png

3.1.3更新节点

3.1.3.1 innerHTML

即可以修改一个DOM节点的文本内容,还可以直接通过 HTML片段 修改 DOM 节点内部的子树。

var p = document.getElementById('p')
p.innerHTML = 'ABC'
3.1.3.2 innerText、textContent

即可以修改一个DOM节点的文本内容,还可以直接通过 HTML片段 修改 DOM 节点内部的子树。


// 获取<p id="p-id">...</p >

var p = document.getElementById('p-id');

// 设置⽂本:

p.innerText = '<script>alert("Hi")</script>';

// HTML被⾃动编码,⽆法设置⼀个<script>节点:

// <p id="p-id"><script>alert("Hi")</script></p >

两者的区别在于读取属性时,innerText 不返回隐藏元素的文本,而textContent返回所有文本

3.1.3.3 style

DOM节点的 style 属性对应所有的 css,可以直接获取或者设置。遇到 - 需要转化为驼峰命名。


// 获取<p id="p-id">...</p >

const p = document.getElementById('p-id');

// 设置CSS:

p.style.color = '#ff0000';

p.style.fontSize = '20px'; // 驼峰命名

p.style.paddingTop = '2em';

3.1.4添加节点

3.1.4.1 innerHTML

如果这个DOM节点是空的,例如<div></div> ,那么,直接使⽤`innerHTML = 'chil

d'` 就可以修改 DOM 节点的内容,相当于添加了新的 DOM 节点

如果这个DOM节点不是空的,那就不能这么做,因为 innerHTML 会直接替换掉原来的所有⼦节点

3.1.4.2 appendChild

把一个子节点添加到父节点的最后一个子节点

3.1.4.3 insertBefore

把子节点插入到指定的位置,使用方法如下:

parentElement.insertBefore(newElement, referenceElement)

子节点会被插入到referenceElement 之前

3.1.4.4 setAttribute

在指定元素中添加一个节点属性,如果元素中已有该属性就改变属性值

const div = document.getElementById('id')

div.setAttribute('class', 'white');//第⼀个参数属性名,第⼆个参数属性值。

3.1.5 删除节点

删除⼀个节点,⾸先要获得该节点本身以及它的⽗节点,然后,调⽤⽗节点的 removeChild 把⾃⼰删掉


// 拿到待删除节点:

const self = document.getElementById('to-be-removed');

// 拿到⽗节点:

const parent = self.parentElement;

// 删除:

const removed = parent.removeChild(self);

removed === self; // true

删除后的节点虽然不再文档树中了,但还存在在内存中,可以随时再次被添加到别的位置

4. BOM

image.png

4.1 BOM是什么

Browser Object Model, 浏览器对象模型,提供了独⽴于内容与浏览器窗⼝进⾏交互的对象 作用:跟浏览器做一些交互效果,比如页面的后退、前进、刷新。 浏览器的全部内容可以看成 DOM,整个 浏览器可以看成 BOM:

image.png

4.2 window

BOM 的核心对象是window,它表示 浏览器的一个实例。 所有全局作用域中声明的变量、函数都会变成 window 对象的属性和方法

var name = 'js每⽇⼀题';

function lookName(){

    alert(this.name);

}

console.log(window.name); //js每⽇⼀题

lookName(); //js每⽇⼀题

window.lookName(); //js每⽇⼀题

关于窗口控制方法如下:

  • moveBy(x, y):从当前位置水平移动窗体x个像素,垂直移动窗体y个像素,x为负数,向左移动窗体,y为负数,向上移动窗体
  • moveTo(x, y):移动窗体左上⻆到相对于屏幕左上⻆的(x,y)点
  • resizeBy(w, h):相对窗体当前的⼤⼩,宽度调整w个像素,⾼度调整h个像素。如果参数为负值,将缩⼩窗体,反之扩⼤窗体
  • resizeTo(w, h):把窗体宽度调整为w个像素,⾼度调整为h个像素
  • scrollBy(x, y):如果有滚动条,将横向滚动条向左移动x个像素,将纵向滚动条向下移动y个像素
  • scrollTo(x, y):如果有滚动条,将横向滚动条移动到相对于窗体宽度为x个像素的位置,将纵向滚动条移动到相对于窗体⾼度为y个像素的位置

window.open() 既可以导航到⼀个特定的 url ,也可以打开⼀个新的浏览器窗⼝ 如果 window.open() 传递了第⼆个参数,且该参数是已有窗⼝或者框架的名称,那么就会在⽬标窗 ⼝加载第⼀个参数指定的URL

window.open('htttp://www.vue3js.cn','topFrame')

==> < a href=" " target="topFrame"></ a>

window.open() 会返回新窗⼝的引⽤,也就是新窗⼝的 window 对象

const myWin = window.open('http://www.vue3js.cn','myWin')

window.close() 仅⽤于通过 window.open() 打开的窗⼝

新创建的 window 对象有⼀个 opener 属性,该属性指向打开他的原始窗⼝对象

4.3 location

url地址如下: http://www.wrox.com:80/WileyCDA/?q=javascript#contents

location 属性描述:

属性名例子说明
hash"#contents"url中#后面的字符,没有则返回空串
hostwww.wrox.com:80服务器名称和端口号
hostnamewww.wrox.com域名,不带端口号
hrefhttp://www.wrox.com:80/WileyCDA/?q=javascript#contents完整url
pathname/WileyCDA/服务器下面的文件路径
prot80url的端口号
protocolhttp:使用的协议
search?q=javascripturl的查询字符串,通常为?后面的内容

除了hash之外,修改location 的一个属性,就会导致页面重新加载新Url location.reload(), 重新刷新当前⻚⾯,如果页面上一次请求以来没有发生改变,页面就会从浏览器缓存中重新加载。

4.4 navigator

⽤来获取浏览器的属性,区分浏览器类型。

image.png image.png

4.5 screen

保存的是浏览器窗⼝外⾯的客户端显示器的信息,⽐如像素宽度和像素⾼度 image.png

4.6 history

⽤来操作浏览器 URL 的历史记录,可以通过参数向前,向后,或者向指定 URL 跳转。 常见的属性:

  • history.go()

    接收⼀个整数数字或者字符串参数:向最近的⼀个记录中包含指定字符串的⻚⾯跳转

    接收⼀个整数数字或者字符串参数:向最近的⼀个记录中包含指定字符串的⻚⾯跳转

  • history.forward() 向前跳转三个记录

  • history.back() 向后跳转一个页面

  • history.length 获取历史记录数

5. == 和 === 的区别

image.png

5.1 == 等于操作符

表示如果操作数行灯,则会返回 true 在比较时会对等号两侧的变量进行类型转换,再确定操作数是否相等。 规则:

  • 两个都为简单类型,字符串和布尔值都会转换成数值,再⽐较
  • 简单类型与引⽤类型⽐较,对象转化成其原始类型的值,再⽐较
  • 两个都为引⽤类型,则⽐较它们是否指向同⼀个对象
  • null 和 undefined 相等
  • 存在 NaN 则返回 false

5.2 === 全等操作符

只有两个操作数在不转换的前提下相等才返回 true 。即类型相同,值也需相同

undefined 和 null 与⾃身严格相等

let result1 = (null === null) //true
let result2 = (undefined === undefined) //true

5.3 区别

操作符是否进行类型转换null & undefined
==true
===false

5.4 小结

'' == '0' // false
0 == '' // true
0 == '0' // true

false == 'false' // false
false == '0' // true

false == undefined // false
false == null // false
null == undefined // true

' \t\r\n' == 0 // true

但在⽐较 null 的情况的时候,我们⼀般使⽤相等操作符==

const obj = {};

if (obj.x == null) {
    console.log('1') // 执行
}
// 等价于下面的写法
if (obj.x === null || obj.x === undefined) {
    ...
}

6. typeof 与 instanceof 区别

image.png

6.1 typeof

typeof 操作符返回一个字符串,表示未经计算的操作数的类型。 使用方法如下:

typeof operand
typeof(operand)

举个例子

typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof null // 'object'
typeof [] // 'object'
typeof {} // 'object'
typeof console // 'object'
typeof console.log // 'function'

虽然 typeof null 为 object ,但这只是 JavaScript存在的⼀个悠久 Bug ,不代表 null 就是引⽤数据类型,并且 null 本身也不是对象。所以, null 在 typeof 之后返回的是有问题的结果,不能作为判断 null 的⽅法。如果你需要在if 语句中判断是否为 null ,直接通过 ===null 来判断就好

6.2 instanceof

instanceof 运算符⽤于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上. 使用方法:

// object 为实例对象, constructor 为构造函数
object instanceof constructor

构造函数通过 new 可以实例对象, instanceof 能判断这个对象是否是之前那个构造函数⽣成的对 象

// 定义构建函数
let Car = function() {}
let benz = new Car()
benz instanceof Car // true

let car = new String('xxx')
car instanceof String // true

let str = 'xxx'
str instanceof String // false

关于 instanceof 的实现原理:

function myInstanceof(left, right) {

    // 1. 先⽤typeof来判断基础数据类型,如果是,直接返回false

    if(typeof left !== 'object' || left === null) return false;

    // 2. getProtypeOf是Object对象⾃带的API,能够拿到参数的原型对象

    let proto = Object.getPrototypeOf(left); // 等价于 left.__proto__

    while(true) {

        if(proto === null) return false;

        if(proto === right.prototype) return true;//找到相同原型对象,返回true

            proto = Object.getPrototypeof(proto);

        }

    }

也就是顺着对象变量的原型链去找,直到找到相同的原型对象,返回true;否则返回false。

6.3 区别

  • typeof 会返回⼀个变量的基本类型,instanceof 返回的是⼀个布尔值
  • instanceof 可以准确地判断复杂引⽤数据类型,但是不能正确判断基础数据类型
  • ⽽ typeof 也存在弊端,它虽然可以判断基础数据类型( null 除外),但是引⽤数据类型中,除了 function 类型以外,其他的也⽆法判断

因此采⽤ Object.prototype.toString ,调⽤该⽅法,统⼀返回格式 “[object Xxx]” 的字符串,如下:

Object.prototype.toString({}) // "[object Object]"
Object.prototype.toString.call({}) // 同上结果,加上call也ok
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call('1') // "[object String]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(function(){}) // "[object Function]"
Object.prototype.toString.call(null) //"[object Null]"
Object.prototype.toString.call(undefined) //"[object Undefined]"
Object.prototype.toString.call(/123/g) //"[object RegExp]"
Object.prototype.toString.call(new Date()) //"[object Date]"
Object.prototype.toString.call([]) //"[object Array]"
Object.prototype.toString.call(document) //"[object HTMLDocument]"
Object.prototype.toString.call(window) //"[object Window]"

下⾯就实现⼀个全局通⽤的数据类型判断⽅法

function getType(obj) {
    let tyoe = typeof obj;
    if (type !== "object") {
        return type
    }
    return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1);
}

7. 原型、原型链

7.1 原型

每个对象拥有⼀个原型对象

当试图访问⼀个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型 的原型,依次层层向上搜索,直到找到⼀个名字匹配的属性或到达原型链的末尾 准确地说,这些属性和⽅法定义在Object的构造器函数(constructor functions)之上 的 prototype 属性上,⽽⾮实例对象本身

下⾯举个例⼦: 函数可以有属性。 每个函数都有⼀个特殊的属性叫作原型 prototype

function doSomething() {}
console.log(doSomething.prototype)
// 输出如下, 被称为原型对象

{
    constructor: ƒ doSomething(),
    __proto__: {
        constructor: ƒ Object(),
        hasOwnProperty: ƒ hasOwnProperty(),
        isPrototypeOf: ƒ isPrototypeOf(),
        propertyIsEnumerable: ƒ propertyIsEnumerable(),
        toLocaleString: ƒ toLocaleString(),
        toString: ƒ toString(),
        valueOf: ƒ valueOf()
    }

}

可以看到,原型对象有一个自由属性 constructor ,这个属性指向该函数,关系如下图:

image.png

7.2 原型链

原型对象也可能拥有原型,并从中继承方法和属性,一层一层,依次类推。这种关系被称为原型链。

在对象实例和它的构造器之间建立一个链接(它是 proto 属性,是从构造函数的 prototype 属性派⽣的)

举个例子:


function Person(name) {
    this.name = name;
    this.age = 18;
    this.sayName = function() {
        console.log(this.name);

    }
}
// 第⼆步 创建实例
var person = new Person('person')

image.png

  • 构造函数 Person 存在原型对象Person.prototype
  • 构造函数⽣成实例对象 person , person 的__proto__ 指向构造函数 Person 原型对象
  • Person.prototype.proto 指向内置对象,因为 Person.prototype 是个对象,默认是由 Object 函数作为类创建的,⽽ Object.prototype 为内置对象
  • Person.proto 指向内置匿名函数 anonymous ,因为 Person 是个函数对象,默认由Function 作为类创建
  • Function.prototype 和 Function.proto 同时指向内置匿名函数 anonymous , 这样原型链的终点就是 null

7.3 总结

proto 作为不同对象之间的桥梁,⽤来指向创建它的构造函数的原型对象的 image.png

// 每个对象的__proto 都指向它的构造函数的原型对象(prototype)
person.__proto__ === Person.prototype

// 构造函数Person 为一个函数对象,通过Function构造器产生
Person.__proto__ === Function.prototype
// 原型对象本身也是一个普通对象,而普通对象的构造函数是Object
Person.prototype.__proto__ === Object.prototype
// 所有的构造器都是函数对象
Object.__proto__ === Function.prototype
// Object 的原型对象也有__proto__ 属性,指向null,null 是原型链的顶端
Object.prototype.__proto__ === null

总计:

  • 一切对象都是继承自 Object 对象, Object对象直接继承根对象 null
  • 一切的函数对象(包括Obejct 对象),都是继承自 Function对象
  • Object 对象 直接继承自 Function 对象
  • Function 对象的 __proto__ 会指向自己的原型对象(Function.__proto__ === Function.prototype),最终还是继承自Object 对象

image.png

8. 作用域、作用域链

8.1 作用域

作⽤域,即变量(变量作⽤域⼜称上下⽂)和函数⽣效(能被访问)的区域或集合 举个例子:

function myFunction() {
    let inVariable = "函数内部变量";
}
myFunction();//要先执⾏这个函数,否则根本不知道⾥⾯是啥
console.log(inVariable); // Uncaught ReferenceError: inVariable is not defi
ned

上述例⼦中,函数 myFunction 内部创建⼀个 inVariable 变量,当我们在全局访问这个变量的时 候,系统会报错 这就说明我们在全局是⽆法获取到函数内部(闭包除外)的变量 作用域分成:

  • 全局作用域
  • 函数作用域
  • 块级作用域

8.1.1 全局作用域

任何不在函数中或是⼤括号中声明的变量,都是在全局作⽤域下;全局作用域下声明的变量可以在程序的任意位置访问。

// 全局变量
var greeting = 'Hello World!';
function greet() {
    console.log(greeting);
}
// 打印 'Hello World!'
greet();

8.1.2 函数作用域

也称为局部作用域,如果一个变量是在函数内部声明的,它就在一个函数作用域下面。 这种变量只能在函数内部访问,不能在函数以外去访问。

function greet() {
    var greeting = 'Hello World!';
    console.log(greeting);

}
// 打印 'Hello World!'
greet();
// 报错: Uncaught ReferenceError: greeting is not defined
console.log(greeting);

8.1.3 块级作用域

ES6引⼊了 let 和const 关键字,和 var 关键字不同,在⼤括号中使⽤ let 和 const 声明的变 量存在于块级作⽤域中。在⼤括号之外不能访问这些变量

{
    // 块级作⽤域中的变量
    let greeting = 'Hello World!';
    var lang = 'English';
    console.log(greeting); // Prints 'Hello World!'
}
// 变量 'English'
console.log(lang);
// 报错:Uncaught ReferenceError: greeting is not defined
console.log(greeting);

8.2 词法作用域

也叫静态作用域,变量被创建时就确定好熬了,而非执行阶段确定的。也就是说我们写好代码时它的作用域就确定了, JavaScript 遵循的就是此法作用域

var a = 2;
function foo(){
    console.log(a)
}
function bar(){
    var a = 3;
    foo();
}
bar()

image.png

由于 JavaScript 遵循词法作⽤域,相同层级的 foo 和 bar 就没有办法访问到彼此块作⽤域中 的变量,所以输出2

8.3 作用域链

当在 Javascript 中使⽤⼀个变量的时候,⾸先 Javascript 引擎会尝试在当前作⽤域下去寻找该 变量,如果没找到,再到它的上层作⽤域寻找,以此类推直到找到该变量或是已经到了全局作⽤域. 如果在全局作⽤域⾥仍然找不到该变量,它就会在全局范围内隐式声明该变量(⾮严格模式下)或是直接报 错。

这⾥拿《你不知道的Javascript(上)》中的⼀张图解释: 把作⽤域⽐喻成⼀个建筑,这份建筑代表程序中的嵌套作⽤域链,第⼀层代表当前的执⾏作⽤域,顶层 代表全局作⽤域

image.png 代码演示:

var sex = '男';
function person() {
    var name = '张三';
    function student() {
        var age = 18;
        console.log(name); // 张三
        console.log(sex); // 男
    }
    student();
    console.log(age); // Uncaught ReferenceError: age is not defined
}
person();

上述代码主要做了以下工作:

  • student 函数内部属于最内层作⽤域,找不到 name ,向上⼀层作⽤域 person 函数内部找, 找到了输出“张三”
  • student 内部输出 sex 时找不到,向上⼀层作⽤域person 函数找,还找不到继续向上⼀层找,即全局作⽤域,找到了输出“男”
  • 在 person 函数内部输出 age 时找不到,向上⼀层作⽤域找,即全局作⽤域,还是找不到则报错

9. this

image.png

9.1 定义

  1. 在函数中this关键字表现略有不同
  2. 在严格模式和非严格模式之间也会有一些差别
  3. 在绝大多数情况下,函数的调⽤⽅式决定了 this 的值(运⾏时绑定)

this 关键字是函数运⾏时⾃动⽣成的⼀个内部对象,只能在函数内部使⽤,总指向调⽤它的对象

举个例子:

function baz() {
    // 当前调⽤栈是:baz
    // 因此,当前调⽤位置是全局作⽤域
    console.log( "baz" );
    bar(); // <-- bar的调⽤位置
}
function bar() {
    // 当前调⽤栈是:baz --> bar
    // 因此,当前调⽤位置在baz中
    console.log( "bar" );
    foo(); // <-- foo的调⽤位置
}
function foo() {
    // 当前调⽤栈是:baz --> bar --> foo
    // 因此,当前调⽤位置在bar中
    console.log( "foo" );
}

baz(); // <-- baz的调⽤位置

同时, this 在函数执⾏过程中, this ⼀旦被确定了,就不可以再更改

var a = 10;
var obj = {
    a: 20
}
function fn() {
    this = obj; // 修改this,运⾏后会报错
    console.log(this.a);
}
fn();

9.2 this绑定规则

根据不同的使用场合, this 有不同的值,分为:

  • 默认绑定
  • 隐式绑定
  • new 绑定
  • 显示绑定

9.2.1 默认绑定

举个例子: ”全局环境中定义 person 函数,内部使⽤ this 关键字“

var name = 'Jenny';
function person() {
    return this.name;
}
console.log(person()); //Jenny

上述代码输出 Jenny ,原因是调⽤函数的对象在游览器中位 window ,因此 this 指向 window ,所以输出 Jenny

注意: 严格模式下,不能将全局对象⽤于默认绑定,this会绑定到 undefined ,只有函数运⾏在⾮严格模式 下,默认绑定才能绑定到全局对象

9.2.2 隐式绑定

函数作为某个对象的⽅法被调⽤时,这时 this 就指这个对象。

function test() {
    console.log(this.x);
}
var obj = {};
obj.x = 1;
obj.m = test;
obj.m(); // 1

这个函数中包含多个对象,尽管这个函数是被最外层的对象所调⽤, this 指向的也只是它上⼀级的对象

var o = {
    a:10,
    b:{
        fn:function(){
            console.log(this.a); //undefined
        }
    }
} 
o.b.fn();

上述代码中, this 的上⼀级对象为 b , b 内部并没有 a 变量的定义,所以输出undefined

这⾥再举⼀种特殊情况:

var o = {
    a:10,
    b:{
        a:12,
        fn:function(){
            console.log(this.a); //undefined
            console.log(this); //window
        }
    }
}
var j = o.b.fn;
j();

此时 this 指向的是 window ,这⾥的⼤家需要记住, this 永远指向的是最后调⽤它的对象,虽 然 fn 是对象 b 的⽅法,但是 fn赋值给 j 时候并没有执⾏,所以最终指向 window

9.2.3 new 绑定

通过构建函数 new 关键字⽣成⼀个实例对象,此时 this 指向这个实例对象

function test() {
    this.x = 1;
}
var obj = new test();
obj.x // 1

上述代码之所以能过输出1,是因为 new 关键字改变了 this 的指向。

这⾥再列举⼀些特殊情况:

  1. new 过程遇到 return ⼀个对象,此时 this 指向为返回的对象
function fn(){
    this.user = 'xxx';
    return {};
}
var a = new fn();
console.log(a.user); //undefined
  1. 如果返回⼀个简单类型(这里的return 1)的时候,则 this 指向实例对象
function fn(){
    this.user = 'xxx';
    return 1;
}
var a = new fn;
console.log(a.user); //xxx
  1. 注意的是 null 虽然也是对象,但是此时 new 仍然指向实例对象
function fn(){
    this.user = 'xxx';
    return null;
}
var a = new fn;
console.log(a.user); //xxx

9.2.4 显示修改

apply()、call()、bind() 是函数的⼀个⽅法,作⽤是改变函数的调⽤对象。它的第⼀个参数就表 示改变后的调⽤这个函数的对象。因此,这时 this 指的就是这第⼀个参数

var x = 0;
function test() {
    console.log(this.x);
}
var obj = {};
obj.x = 1;
obj.m = test;
test(); // 0 当前this 指向 window
obj.m(); // 1 当前this 指向 对象 obj
test.call(obj); // 1 显示修改 this 的指向(由window 修改 为 obj)

关于 apply、call、bind 三者的区别,我们后⾯再详细说

9.3 箭头函数

在 ES6 的语法中还提供了箭头函语法,让我们在代码书写时就能确定 this 的指向(编译时绑定) 举个例子

const obj = {
    sayThis: () => {
        console.log(this);
    }
};
obj.sayThis(); // window 因为 JavaScript 没有块作⽤域,所以在定义 sayThis 的时候,⾥⾯的 this 就绑到 window 上去了

const globalSay = obj.sayThis;
globalSay(); // window 浏览器中的 global 对象

虽然箭头函数的 this 能够在编译的时候就确定了 this 的指向,但也需要注意⼀些潜在的坑 下⾯举个例⼦:

  • 绑定事件监听:
const button = document.getElementById('mngb');
button.addEventListener('click', ()=> {
    console.log(this === window) // true
    this.innerHTML = 'clicked button'
})

上述可以看到,我们其实是想要 this 为点击的 button ,但此时 this 指向了 window

  • 包括在原型上添加⽅法时候,此时 this 指向 window
Cat.prototype.sayName = () => {
    console.log(this === window) //true
    return this.name
}
const cat = new Cat('mm');
cat.sayName()
  • 同样的,箭头函数不能作为构建函数

9.4 优先级

9.4.1 隐式绑定 与 显示绑定

function foo() {
    console.log( this.a );
}
var obj1 = {
    a: 2,
    foo: foo
};
var obj2 = {
    a: 3,
    foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2

显然,显示绑定的优先级更⾼

9.4.2 new 与 隐式绑定

function foo(something) {
    this.a = something;
}
var obj1 = {
    foo: foo
};
var obj2 = {};
obj1.foo(2);
console.log( obj1.a ); // 2

obj1.foo.call( obj2, 3 );
console.log( obj2.a ); // 3

var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4

可以看到,new绑定的优先级>隐式绑定

9.4.3 new 与 显示绑定

因为 new 和 apply、call ⽆法⼀起使⽤,但硬绑定也是显式绑定的⼀种,可以替换测试

function foo(something) {
    this.a = something;
}

var obj1 = {};
var bar = foo.bind( obj1 );

bar( 2 );
console.log( obj1.a ); // 2

var baz = new bar( 3 );
console.log( obj1.a ); // 2
console.log( baz.a ); // 3

new 修改了绑定调⽤ bar() 中的 this

综上,new绑定优先级 > 显示绑定优先级 > 隐式绑定优先级 > 默认绑定优先级

10. new 操作符的原理

image.png

10.1 new 是什么

在JavaScript中,new 操作符用于创建一个给定 构造函数实例对象 例子:

function Person(name, age){
    this.name = name;
    this.age = age;
}

Person.prototype.sayName = function () {
    console.log(this.name)
}

const person1 = new Person('Tom', 20)
console.log(person1) // Person {name: "Tom", age: 20}
t.sayName() // 'Tom'

从上述代码可知:

  1. new 通过构造函数 Person 创建出来的实例可以访问到构造函数中的属性(即 name age
  2. new 通过构造函数 Person 创建出来的实例可以访问到构造函数原型链中的属性(即实例与构造函数通过原型链连接了起来)

10.2 流程

从上述介绍中,new 关键字主要做了以下工作:

  1. 创建一个新的对象(obj)
  2. 将对象与构造函数通过原型链链接起来
  3. 将构造函数中的 this 绑定到新建的对象(obj) 上
  4. 根据构造函数返回类型判断,如果是原始值则被忽略,如果是返回对象,需要正常处理

举个例子:

function Person(name, age){
    this.name = name;
    this.age = age;
}

Person.prototype.sayName = function () {
    console.log(this.name)
}

const person1 = new Person('Tom', 20)
console.log(person1) // Person {name: "Tom", age: 20}
t.sayName() // 'Tom'

流程图如下:

image.png

10.3 手写 new 操作符

function myNew(Func, ...args) {
    // 1. 创建一个空的对象
    const obj = {};
    // 2. 将新对象的[[prototype]]指向 Func 的 prototype
    obj.__proto__ = Func.prototype;
    // 3. 将 Func 构造函数的this 设置为新创建的对象,并执行
    let result = Func.apply(obj, args)
    // 4. 根据返回值判断返回内容
    return result instanceof Object ? result : obj;
}

// 测试
function Person(name, age){
    this.name = name;
    this.age = age;
}

Person.prototype.sayName = function () {
    console.log(this.name)
}
const person1 = myNew(Person, 'Tom', 20);
console.log(person1) // Person {name: "Tom", age: 20}
t.sayName() // 'Tom'

11. bind、call、apply

image.png

11.1 作用

是改变函数执⾏时的上下⽂,简⽽⾔之就是改变函数运⾏时的 this 指向

那么什么情况下需要改变 this 的指向呢?下⾯举个例⼦

var name = "lucy";
var obj = {
    name: "martin",
    say: function () {
        console.log(this.name);
    }
};
obj.say(); // martin,this 指向 obj 对象
setTimeout(obj.say,0); // lucy,this 指向 window 对象

从上⾯可以看到,正常情况 say ⽅法输出 martin, 但是我们把 say 放在 setTimeout ⽅法中,在定时器中是 作为回调函数 来执⾏的,因此 回到主栈执⾏时是在全局执⾏上下⽂的环境中执⾏的 ,这时候 this 指向 window ,所以输出 lucy 我们实际需要的是 this 指向 obj 对象,这时候就需要该改变 this 指向了

setTimeout(obj.say.bind(obj),0); //martin,this指向obj对象

11.2 区别

11.2.1 apply

apply 接受两个参数, 第⼀个参数是 this 的指向 ,第⼆个参数是函数接受的参数,以 数组 的形式传⼊.

改变 this 指向后原函数会⽴即执⾏,且此⽅法只是临时改变 this 指向⼀次

function fn(...args){
    console.log(this,args);
}
let obj = {
    myname:"张三"
}
fn.apply(obj,[1,2]); // this会变成传⼊的obj,传⼊的参数必须是⼀个数组;
fn(1,2) // this指向window

当第⼀个参数为 null 、 undefined 的时候,默认指向window (在浏览器中)

fn.apply(null,[1,2]); // this指向window
fn.apply(undefined,[1,2]); // this指向window

11.2.2 call

call ⽅法的第⼀个参数也是 this 的指向,后⾯传⼊的是⼀个参数列表。

跟 apply ⼀样,改变 this 指向后原函数会⽴即执⾏,且此⽅法只是临时改变this 指向⼀次

function fn(...args){
    console.log(this,args);
}
let obj = {
    myname:"张三"
}
fn.call(obj,1,2); // this会变成传⼊的obj;
fn(1,2) // this指向window

同样的,当第⼀个参数为 null 、 undefined 的时候,默认指向 window (在浏览器中)

11.2.3 bind

bind⽅法和call很相似,第⼀参数也是 this 的指向,后⾯传⼊的也是⼀个参数列表( 但是这个参数列表可以分多次传⼊ )

改变 this 指向后 不会⽴即执⾏ ,⽽是 返回⼀个永久改变 this 指向的函数

function fn(...args){
    console.log(this,args);
}
let obj = {
    myname:"张三"
}

const bindFn = fn.bind(obj); // this 也会变成传⼊的obj ,bind不是⽴即执⾏需要执
⾏⼀次

bindFn(1,2) // this指向obj
fn(1,2) // this指向window

11.2.4 小结

区别在于:

  1. 三者都可以改变函数的 this 指向
  2. 三者第一个参数都是 this 要指向的对象,如果没有这个参数或者这个参数为 undefined 或 null,则默认指向window
  3. 三者都可以传参,但 apply 是数组,而 call 是参数列表,且 apply 和 call 是一次性传入参数,而 bind 可以分为多次传入
  4. bind 是 返回绑定 this 之后的函数, apply、call 则是立即执行

11.3 实现 bind

分为三部分:

  1. 修改this指向
  2. 动态传递参数
fn.bind(obj, 1, 2)()
fn.bind(obj, 1)(2)
  1. 兼容 new 关键字
Function.prototype.myBind = function(context) {
    // 0. 判断调用对象是否为函数
    if (typeof this !== 'function') {
        throw new TypeError('Error')
    }
    // 0.1. 解构(获取)参数, 
    const args = [...arguments].slice(1),
        fn = this; // 0.2 获取到原始的函数并赋值给fn
    return function Fn() {
        return fn.apply(
            this instanceof Fn ? new fn(...arguments) // 3. 兼容 new 关键字
            : context // 1. 修改 this 指向 传入的对象
            , args.concat(...arguments)); // 2. 动态传递参数
    }
}

12. JavaScript中执行上下文和执行栈是什么

image.png

12.1 执行上下文

简单的来说,执⾏上下⽂是⼀种对 Javascript 代码执⾏环境的抽象概念,也就是说只要有 Javascript 代码运⾏,那么它就⼀定是运⾏在执⾏上下⽂中

分为三种:

  • 全局执⾏上下⽂:只有⼀个,浏览器中的全局对象就是 window 对象, this 指向这个全局对象
  • 函数执⾏上下⽂:存在⽆数个,只有在函数被调⽤的时候才会被创建,每次调⽤函数都会创建⼀个新的执⾏上下⽂
  • eval 函数执⾏上下⽂: 指的是运⾏在 eval 函数中的代码,很少⽤⽽且不建议使⽤

image.png

紫⾊框住的部分为全局上下⽂,蓝⾊和橘⾊框起来的是不同的函数上下⽂。只有全局上下⽂(的变量)能被其他任何上下⽂访问

可以有任意多个函数上下⽂,每次调⽤函数创建⼀个新的上下⽂,会创建⼀个私有作⽤域,函数内部声明的任何变量都不能在当前函数作⽤域外部直接访问

12.2 执行上下文声明周期

创建阶段 → 执⾏阶段 → 回收阶段

12.2.1 创建阶段

创建阶段即当函数被调⽤,但未执⾏任何其内部代码之前 创建阶段做了三件事:

  • 确定 this 的值, 也被称为 This Binding。
  • 词法环境(LexicalEnvironment)组件被创建
  • 变量环境(VariableEnvironment)组件被创建

伪代码:

ExecutionContext = {
    ThisBinding = <this value>, // 确定this
    LexicalEnvironment = { ... }, // 词法环境
    VariableEnvironment = { ... }, // 变量环境
}
12.2.1.1 This Binding

确定 this 的值我们前⾯讲到, this 的值是在执⾏的时候才能确认,定义的时候不能确认

12.2.1.2 词法环境

词法环境由两部分组成

  • 全局环境:是一个没有外部环境的词法环境,其外部环境引用为 null,有一个全局对象,this 的值指向这个全局对象
  • 函数环境:用户在函数中定义的变量被存储在环境记录中,包含了 arguments 对象,外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境

伪代码如下:


GlobalExectionContext = { // 全局执⾏上下⽂
    LexicalEnvironment: { // 词法环境
        EnvironmentRecord: { // 环境记录
            Type: "Object", // 全局环境
            // 标识符绑定在这⾥
        },
        outer: <null> // 对外部环境的引⽤
    }
}

FunctionExectionContext = { // 函数执⾏上下⽂
    LexicalEnvironment: { // 词法环境
        EnvironmentRecord: { // 环境记录
            Type: "Declarative", // 函数环境
            // 标识符绑定在这⾥ 
        },
        // 对外部环境的引⽤
        outer: <Global or outer function environment reference>
    }
}
12.2.1.3 变量环境

变量环境也是⼀个词法环境,因此它具有上⾯定义的词法环境的所有属性

在 ES6 中,词法环境和变量环境的区别在于前者⽤于存储函数声明和变量( let 和 const )绑定,⽽后者仅⽤于存储变量( var )绑定 举个例子:

let a = 20;
const b = 30;
var c;

function multiply(e, f) {
    var g = 20;
    return e * f * g;
}
c = multiply(20, 30);

// 执行上下文如下:
GlobalExectionContext = { // 全局执⾏上下⽂
    ThisBinding: <Global object>,
    LexicalEnvironment: { // 词法环境
        EnvironmentRecord: { // 环境记录
            Type: "Object", // 全局环境
            // 标识符绑定在这⾥
            a: < uninitialized >,
            b: < uninitialized >,
            multiply: < uninitialized >
        }
        outer: <null> // 对外部环境的引⽤
    },
    VariableEnvironment: { // 变量环境
        EnvironmentRecord: {
            Type: "Object",
            // 标识符绑定在这⾥
            c: undefined,
        },
        outer: <null>
    }
}

FunctionExectionContext = { // 函数执⾏上下⽂
    ThisBinding: <Global object>,
    LexicalEnvironment: { // 词法环境
        EnvironmentRecord: { // 环境记录
            Type: "Declarative", // 函数环境
            // 标识符绑定在这⾥ 
            Arguments: {0: 20, 1: 30, length: 2},
        },
        // 对外部环境的引⽤
        outer: <GlobalLexicalEnvironment>
    },
    VariableEnvironment: { // 变量环境
        EnvironmentRecord: {
            Type: "Declarative",
            // 标识符绑定在这⾥
            g: undefined,
        },
        outer: <GlobalLexicalEnvironment>
    }
}

留意上⾯的代码, let 和 const 定义的变量 a 和b。在创建阶段没有被赋值,但 var 声明的变量从在创建阶段被赋值为 undefined。

这是因为,创建阶段,会在代码中扫描变量和函数声明,然后将函数声明存储在环境中

但变量会被初始化为 undefined ( var 声明的情况下)和保持 uninitialized (未初始化状态)(使⽤ let 和 const 声明的情况下)

这就是变量提升的实际原因。

12.2.2 执行阶段

在这阶段,执⾏变量赋值、代码执⾏

如果 Javascript 引擎在源代码中声明的实际位置找不到变量的值,那么将为其分配 undefined值

12.2.3 回收阶段

执⾏上下⽂出栈等待虚拟机回收执⾏上下⽂

12.3 执行栈

执⾏栈,也叫调⽤栈,具有后进先出(LIFO)结构,⽤于存储在代码执⾏期间创建的所有执⾏上下⽂

image.png 当 Javascript 引擎开始执⾏你第⼀⾏脚本代码的时候,它就会创建⼀个全局执⾏上下⽂然后将它压 到执⾏栈中

每当引擎碰到⼀个函数的时候,它就会创建⼀个函数执⾏上下⽂,然后将这个执⾏上下⽂压到执⾏栈中 引擎会执⾏位于执⾏栈栈顶的执⾏上下⽂(⼀般是函数执⾏上下⽂),当该函数执⾏结束后,对应的执⾏上 下⽂就会被弹出,然后控制流程到达执⾏栈的下⼀个执⾏上下⽂

举个例⼦:

let a = 'Hello World!';
function first() {
    console.log('Inside first function');
    second();
    console.log('Again inside first function');
}
function second() {
    console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');

image.png 分析:

  1. 创建全局上下文,并压入执行栈
  2. 函数 first 函数被调用,创建函数执行上下文并压入栈
  3. 执行 first 函数时遇到 函数 second,再创建一个函数执行上下文并压入栈
  4. second 函数执行完毕,对应的函数执行上下文被推出执行栈,执行下一个执行上下文 函数 first
  5. 函数 first 函数执行完毕,对应的函数执行上下文也被推出栈,然后执行全局上下文
  6. 所有代码执行完毕,全局上下文业火被推出栈,程序结束。

13. JavaScript中的事件模型

image.png

13.1 事件与事件流

javascript 中的事件,可以理解就是在 HTML ⽂档或者浏览器中发⽣的⼀种交互操作,使得⽹⻚具备互动性, 常⻅的有加载事件、⿏标事件、⾃定义事件等

由于 DOM 是⼀个树结构,如果在⽗⼦节点绑定事件时候,当触发⼦节点的时候,就存在⼀个顺序问题,这就涉及到了事件流的概念

事件流都会经历三个阶段:

  • 事件捕获阶段(capture)
  • 处于目标阶段(target)
  • 事件冒泡阶段(bubbling)

image.png

13.1.1 冒泡

事件冒泡是⼀种从下往上的传播⽅式,由最具体的元素(触发节点)然后逐渐向上传播到最不具体的那个节点,也就是 DOM 中最⾼层的⽗节点 举例:

<!DOCTYPE html>
<html lang="en">
    <head>
    <meta charset="UTF-8">
        <title>Event Bubbling</title>
    </head>
    <body>
        <button id="clickMe">Click Me</button>
    </body>
</html>;
// 给 button 和他的父元素添加点击事件
var button = document.getElementById('clickMe');

button.onclick = function() {
    console.log('1.Button');
};

document.body.onclick = function() {
    console.log('2.body');
};

document.onclick = function() {
    console.log('3.document');
};

window.onclick = function() {
    console.log('4.window');
};

// 点击按钮, 输出如下
1.button
2.body
3.document
4.window

点击事件⾸先在 button 元素上发⽣,然后逐级向上传播

事件捕获与事件冒泡相反,事件最开始由不太具体的节点最早接受事件, ⽽最具体的节点(触发节点)最后接受事件

13.2 事件模型

分为三种:

  • 原始事件模型(DOM 0级)
  • 标准事件模型(DOM 1级)
  • IE事件模型 (基本不用)

13.2.1 原始事件模型

事件绑定监听函数⽐较简单, 有两种⽅式:

  • HTML代码中直接绑定
<input type="button" onclick="fun()">
  • 通过JS代码绑定
var btn = document.getElementById('.btn');
btn.onclick = fun;

13.2.1.1 原始事件模型的特性

  1. 绑定速度快

DOM0 级事件具有很好的跨浏览器优势,会以最快的速度绑定,但由于绑定速度太快,可能⻚⾯还未完全加载出来,以⾄于事件可能⽆法正常运⾏。

  1. 只支持冒泡,不支持捕获
  2. 同一个类型的事件只能绑定一次
<input type="button" id="btn" onclick="fun1()">
var btn = document.getElementById('.btn'); 
btn.onclick = fun2;

如上,当希望为同⼀个元素绑定多个同类型事件的时候(上⾯的这个 btn 元素绑定2个点击事件),是不被允许的,后绑定的事件会覆盖之前的事件

删除 DOM0 级事件处理程序只要将对应事件属性置为 null 即可

btn.onclick = null;

13.2.2 标准事件模型

在该事件模型中,⼀次事件共有三个过程:

  1. 事件捕获阶段:事件从 document ⼀直向下传播到⽬标元素, 依次检查经过的节点是否绑定了事件
// 添加监听事件
addEventListener(eventType, handler, useCapture)

// 移除监听事件
removeEventListener(eventType, handler, useCapture) 

// eventType: zhiding事件类型(不要加on)
// handler: 事件处理函数
// useCapture: 是⼀个 boolean ⽤于指定是否在捕获阶段进⾏处理,⼀般设置为 false 与IE浏览器保持⼀致
// 举个例子:
var btn = document.getElementById('.btn');
btn.addEventListener(‘click’, showMessage, false);
btn.removeEventListener(‘click’, showMessage, false);
  1. 事件处理阶段:事件到达⽬标元素, 触发⽬标元素的监听函数监听函数,如果有则执⾏
  2. 事件冒泡阶段:事件从⽬标元素冒泡到 document , 依次检查经过的节点是否绑定了事件监听函数,如果有则执⾏
13.2.2.1 特性
  1. 可以在⼀个 DOM 元素上绑定多个事件处理器,各⾃并不会冲突
btn.addEventListener(‘click’, showMessage1, false);
btn.addEventListener(‘click’, showMessage2, false);
btn.addEventListener(‘click’, showMessage3, false);
  1. 执行时机:当 useCapture 设置为 true 就表示在捕获过程中执行,反之在冒泡过程中执行处理函数
<div id='div'>
    <p id='p'>
        <span id='span'>Click Me!</span>
    </p >
</div>
var div = document.getElementById('div');
var p = document.getElementById('p');
function onClickFn (event) {
    var tagName = event.currentTarget.tagName;
    var phase = event.eventPhase; // 返回⼀个代表当前执⾏阶段的整数值。1为捕获阶段、2为事件对象触发阶段、3为冒泡阶段
    console.log(tagName, phase);
}
div.addEventListener('click', onClickFn, false);
p.addEventListener('click', onClickFn, false);

// 点击按钮
// 输出
// P 3
// DIV 3
// 由于冒泡的特性,裹在⾥层的 p 率先做出响应

//如果把第三个参数都改为 true
div.addEventListener('click', onClickFn, true);
p.addEventListener('click', onClickFn, true);// 输出
// DIV 1
// P 1
// 两者都是在捕获阶段响应事件,所以 div ⽐ p标签先做出响应

13.2.3 IE事件模型

IE事件模型共有两个过程:

  • 事件处理阶段:事件到达⽬标元素, 触发⽬标元素的监听函数。
  • 事件冒泡阶段:事件从⽬标元素冒泡到 document , 依次检查经过的节点是否绑定了事件监听函数,如果有则执⾏
// 绑定监听函数的方式如下:
attachEvent(eventType, handler)

// 移除方式如下
detachEvent(eventType, handler)

14. 事件代理

image.png

14.1 什么是事件代理

通俗地来讲,就是把⼀个元素响应事件( click 、 keydown ......)的函数委托到另⼀个元素。

前⾯讲到,事件流的都会经过三个阶段: 捕获阶段 -> ⽬标阶段 -> 冒泡阶段,⽽事件代理就是在冒泡阶段完成

事件代理会把⼀个或者⼀组元素的事件委托到它的⽗层或者更外层元素上,真正绑定事件的是外层元 素,⽽不是⽬标元素

当事件响应到⽬标元素上时,会通过事件冒泡机制从⽽触发它的外层元素的绑定事件上,然后在外层元素上去执⾏函数

14.2 应用场景

  • 场景1: 如果我们有⼀个列表,列表之中有⼤量的列表项,我们需要在点击列表项的时候响应⼀个事件。
<ul id="list">
    <li>item 1</li>
    <li>item 2</li>
    <li>item 3</li>
    ......

    <li>item n</li>
</ul>
// 给⽗层元素绑定事件
document.getElementById('list').addEventListener('click', function (e) {
    // 兼容性处理
    var event = e || window.event;
    var target = event.target || event.srcElement;
    // 判断是否匹配⽬标元素
    if (target.nodeName.toLocaleLowerCase === 'li') {
        console.log('the content is: ', target.innerHTML);
    }
});
  • 场景2:⽤户能够随时动态的增加或者去除列表项元素.

14.3 总结

适合事件代理的事件有: click , mousedown ,mouseup , keydown , keyup ,keypress

事件代理的优点:

  1. 减少整个⻚⾯所需的内存,提升整体性能
  2. 动态绑定、减少重复工作

局限性:

  1. focus 、blur 这些事件没有事件冒泡机制,所以⽆法进⾏代理绑定事件
  2. mousemove 、 mouseout 这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗⾼,因此也是不适合于事件代理的

15. 闭包

image.png

15.1 什么闭包

一个函数和对其周围状态(词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包

也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。

在 JavaScript 中,每当创建⼀个函数,闭包就会在函数创建的同时被创建出来,作为函数内部与外部连接起来的⼀座桥梁

举个例子:

function init() {
    var name = "Mozilla"; // name 是⼀个被 init 创建的局部变量
    function displayName() { // displayName() 是内部函数,⼀个闭包
        alert(name); // 使⽤了⽗函数中声明的变量
    }
    displayName();
}

init();

displayName() 没有⾃⼰的局部变量。然⽽,由于闭包的特性,它可以访问到外部函数的变量

15.2 使用场景

  • 创建私有变量
  • 延长变量的生命生命周期

⼀般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引⽤,即便创建 时所在的执⾏上下⽂被销毁,但创建时所在词法环境依然存在,以达到延⻓变量的⽣命周期的⽬的

举个例子:

// 在页面上添加一些可以调整字号的按钮
function makeSizer(size) {
    return function() {
        document.body.style.fontSize = size + 'px';
    };
}

var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);

document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16

15.2.1 柯里化函数

柯⾥化的⽬的在于 避免频繁调⽤具有相同参数 的 函数 的同时,⼜ 能够轻松的重⽤

// 假设我们有⼀个求⻓⽅形⾯积的函数
function getArea(width, height) {
    return width * height
}

// 如果我们碰到的⻓⽅形的宽⽼是10
const area1 = getArea(10, 20)
const area2 = getArea(10, 30)
const area3 = getArea(10, 40)

// 我们可以使⽤闭包柯⾥化这个计算⾯积的函数
function getArea(width) {
    return height => {
        return width * height
    }
}
const getTenWidthArea = getArea(10)
// 之后碰到宽度为10的⻓⽅形就可以这样计算⾯积
const area1 = getTenWidthArea(20)
// ⽽且如果遇到宽度偶尔变化也可以轻松复⽤
const getTwentyWidthArea = getArea(20)

15.2.2 使用闭包模拟所有方法

在 JavaScript 中,没有⽀持声明私有变量,但我们可以使⽤闭包来模拟私有⽅法

举个例⼦:

var makeCounter = (function() {
    var privateCounter = 0;
    function changeBy(val) {
        privateCounter += val;
    }
    return {
        increment: function() {
            changeBy(1);
       },
        decrement: function() {
            changeBy(-1);
        },
        value: function() {
            return privateCounter;
        }
    }
})();

var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */

Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */

Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */

上述通过使⽤闭包来定义公共函数,并令其可以访问私有函数和变量,这种⽅式也叫模块⽅式 两个计数器 Counter1 和 Counter2 是维护它们各⾃的独⽴性的,每次调⽤其中⼀个计数器时, 通过改变这个变量的值,会改变这个闭包的词法环境,不会影响另⼀个闭包中的变量

15.2.3 其他

例如计数器、延迟调⽤、回调等闭包的应⽤,其核⼼思想还是创建私有变量和延⻓变量的⽣命周期

15.3 注意事项

如果不是某些特定任务需要使⽤闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗⽅⾯对脚本性能具有负⾯影响 例如,在创建新的对象或者类时,⽅法通常应该关联于对象的原型,⽽不是定义到对象的构造器中。原因在于每个对象的创建,⽅法都会被重新赋值

function MyObject(name, message) {
    this.name = name.toString();
    this.message = message.toString();
    
    this.getName = function() {
        return this.name;
    };

    this.getMessage = function() {
        return this.message;
    };

}

// 上述代码中,我们并没有利用闭包的好处,因此可以避免使用闭包

function MyObject(name, message) {
    this.name = name.toString();
    this.message = message.toString();
}

MyObject.prototype.getName = function() {
    return this.name;
};

MyObject.prototype.getMessage = function() {
    return this.message;
};

16. JavaScript中的类型转换机制

image.png

16.1 概述

上面说到, JS 中共有6种基本数据类型:boolean、number、string、undefined、null和symbol,以及引用数据类型 object

但是我们在声明的时候只有⼀种数据类型,只有到运⾏期间才会确定当前类型

let x = y ? 1 : a;

上述代码中,x 的值在编译阶段是无法获取的,只有等到程序运行时才能知道。 虽然变量的类型是不能确定的,但是各种运算符对数据类型是有要求的,如果运算子的类型和预期不符合,就会触发类型转换机制

常见的转换机制:

  • 强制转换(显示转换)
  • 自动转换(隐式转换)

16.2 显示转换

显示转换,即我们很清楚可以看到这⾥发⽣了类型的转变,常⻅的⽅法有:

  • Number()
  • parseInt()
  • String()
  • Boolean()

16.2.1 Number()

将任意类型转换为数值

规则:

原始值转换结果
undefinedNaN
null0
true1
false0
string根据语法和转换规则来转换
symbolThrow a TypeError Exception
object先调用 toPrimitive 再调用 toNumber
举例:
Number(324) // 324

// 字符串:如果可以被解析为数值,则转换为相应的数值
Number('324') // 324

// 字符串:如果不可以被解析为数值,返回 NaN
Number('324abc') // NaN

// 空字符串转为0
Number('') // 0


// 布尔值:true 转成 1,false 转成 0
Number(true) // 1
Number(false) // 0

// undefined:转成 NaN
Number(undefined) // NaN

// null:转成0
Number(null) // 0

// 对象:通常转换成NaN(除了只包含单个数值的数组)
Number({a: 1}) // NaN
Number([1, 2, 3]) // NaN
Number([5]) // 5

从上⾯可以看到, Number 转换的时候是很严格的,只要有⼀个字符⽆法转成数值,整个字符串就会被转为 NaN

16.2.2 parseInt()

parseInt 相⽐ Number ,就没那么严格了, parseInt 函数逐个解析字符,遇到不能转换的字符就停下来

parseInt('32a3') //32

16.2.3 String()

规则:

原始值转换结果
undefined'undefined'
null'null'
boolean'true' or 'false'
number对应的字符串类型
stringstring
symbolthore a TypeError Exception
object先调用 toPrimitive 再调用 toNumber
举个例子:
// 数值:转为相应的字符串
String(1) // "1"

//字符串:转换后还是原来的值
String("a") // "a"

//布尔值:true转为字符串"true",false转为字符串"false"
String(true) // "true"

//undefined:转为字符串"undefined"
String(undefined) // "undefined"

//null:转为字符串"null"
String(null) // "null"

//对象
String({a: 1}) // "[object Object]"
String([1, 2, 3]) // "1,2,3"

16.2.4 Boolean()

规则:

数据类型truefalse
booleantruefalse
string非空字符串""(空字符串)
number非零数值(包括无穷值)0,NaN
object任意对象null
undefinedN/A(不存在)undefined

举个例子:

Boolean(undefined) // false

Boolean(null) // false

Boolean(0) // false
Boolean(NaN) // false

Boolean('') // false

Boolean({}) // true
Boolean([]) // true
Boolean(new Boolean(false)) // true

16.3 隐式转换

发生隐式转换的场景:

  1. 运算符(==、!=、 >、 <)、if、 while 需要布尔值的地方
  2. 算术运算(+ - * / %)

16.3.1 自动转换为布尔值

在需要布尔值的地⽅,就会将⾮布尔值的参数⾃动转为布尔值,系统内部会调⽤ Boolean 函数

可以得出个⼩结(会被转换成false):

  • undefined
  • null
  • false
  • +0
  • -0
  • NaN
  • ""

其他都将被转换为true。

16.3.2 自动转换成字符串

遇到 预期为字符串 的地⽅,就会将⾮字符串的值⾃动转为字符串

具体规则是:先将引用数据类型的值转为基本数据类型的值,再将基本数据类型的值转为字符串

常发生在 + 运算中,⼀旦存在字符串,则会进行字符串拼接操作

'5' + 1 // '51'
'5' + true // "5true"
'5' + false // "5false"
'5' + {} // "5[object Object]"
'5' + [] // "5"
'5' + function (){} // "5function (){}"
'5' + undefined // "5undefined"
'5' + null // "5null"

16.3.3 自动转换成数值、

除了 + 有可能把运算⼦转为字符串,其他运算符都会把运算⼦⾃动转成数值

'5' - '2' // 3
'5' * '2' // 10
true - 1 // 0
false - 1 // -1
'1' - 1 // 0
'5' * [] // 0
false / '5' // 0
'abc' - 1 // NaN
null + 1 // 1
undefined + 1 // NaN

null 转为数值时,值为 0 。undefined 转为数值时,值为 NaN

17.深拷贝和浅拷贝

image.png

17.1 数据类型存储

前⾯⽂章我们讲到, JavaScript 中存在两⼤数据类型:

  • 基本类型
  • 引⽤类型

基本类型数据保存在在栈内存中

引⽤类型数据保存在堆内存中,引⽤数据类型的变量是⼀个指向堆内存中实际对象的引⽤,存在栈中

17.2 浅拷贝

浅拷贝:指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝

如果属性是基本类型,拷贝的就是基本类型的值。

如果属性是引用类型,拷贝的就是内存地址。

即:浅拷贝是拷贝一层,深层次的引用类型则共享内容地址

下面简单实现一个浅拷贝:

function shallowClone(obj) {
    const newObj = {};
    for(let prop in obj) {
        if(obj.hasOwnProperty(prop)){
            newObj[prop] = obj[prop];
        }
    }
    return newObj;
}

在 JS 中,存在浅拷贝的现象有:

  1. Object.assign
  2. Array.prototype.slice() and Array.prototype.concat()
  3. 使用拓展运算符的复制

17.2.1 Object.assign

var obj = {
    age: 18,
    nature: ['smart', 'good'],
    names: {
        name1: 'fx',
        name2: 'xka'
    },
    love: function () {
        console.log('fx is a great girl')
    }
}
var newObj = Object.assign({}, fxObj);

17.2.2 slice()

const fxArr = ["One", "Two", "Three"]
const fxArrs = fxArr.slice(0)

fxArrs[1] = "love";

console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]

17.2.3 concat()

const fxArr = ["One", "Two", "Three"]
const fxArrs = fxArr.concat()

fxArrs[1] = "love";

console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]

17.2.4 拓展运算符

const fxArr = ["One", "Two", "Three"]
const fxArrs = [...fxArr]

fxArrs[1] = "love";

console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]

17.3 深拷贝

深拷贝 开辟一个新的栈,两个对象属性完全相同,但是对应两个不同的地址,修改一个对象的属性们不会改变另一个对象的属性。

常见的深拷贝方式有:

  • _.cloneDeep() (lodash)
  • jQuery.extend()
  • JSON.stringify()
  • 手写循环递归

17.3.1 _.cloenDeep()

const _ = require('lodash');
const obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};

const obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false

17.3.2 jQuery.extend()

const $ = require('jquery');
const obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};

const obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f);// false

17.3.3 JSON.stringify()

const obj2=JSON.parse(JSON.stringify(obj1));

但是这种⽅式存在弊端,会忽略 undefined 、 symbol 和 函数.

const obj = {
    name: 'A',
    name1: undefined,
    name3: function() {},
    name4: Symbol('A')
}

const obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2); // {name: "A"}

17.3.4 循环递归

function deepClone(obj, hash = new WeakMap()) {
    if (obj === null) return obj; // 如果是null或者undefined我就不进⾏拷⻉操作
    if (obj instanceof Date) return new Date(obj);
    if (obj instanceof RegExp) return new RegExp(obj);

    // 可能是对象或者普通的值 如果是函数的话是不需要深拷⻉
    if (typeof obj !== "object") return obj;
    
    // 是对象的话就要进⾏深拷⻉
    if (hash.get(obj)) return hash.get(obj);
    
    let cloneObj = new obj.constructor();
    // 找到的是所属类原型上的constructor,⽽原型上的 constructor指向的是当前类本身
    hash.set(obj, cloneObj);
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            // 实现⼀个递归拷⻉
            cloneObj[key] = deepClone(obj[key], hash);
        }
    }
    return cloneObj;
}

17.4 区别

image.png

由上图可知,浅拷贝和深拷贝都创建出一个新的对象,但是复制对象属性的时候,行为就不一样了。

浅拷贝只复制属性指向某个对象的指针,而不是复制对象本身,新旧对象还是共享一块内存,修改新对象的属性会影响原对象。

// 浅拷⻉
const obj1 = {
    name : 'init',
    arr : [1,[2,3],4],
};

const obj3 = shallowClone(obj1) // ⼀个浅拷⻉⽅法
obj3.name = "update";
obj3.arr[1] = [5,6,7] ; // 新旧对象还是共享同⼀块内存

console.log('obj1',obj1) // obj1 { name: 'init', arr: [ 1, [ 5, 6, 7 ], 4 ] }
console.log('obj3',obj3) // obj3 { name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] }

但深拷贝会另外创造一个一模一样的对象,新对象跟源对象不共享内存,修改新对象不会改到源对象

// 深拷⻉
const obj1 = {
    name : 'init',
    arr : [1,[2,3],4],
};

const obj4=deepClone(obj1) // ⼀个深拷⻉⽅法
obj4.name = "update";
obj4.arr[1] = [5,6,7] ; // 新对象跟原对象不共享内存
console.log('obj1',obj1) // obj1 { name: 'init', arr: [ 1, [ 2, 3 ], 4 ] }
console.log('obj4',obj4) // obj4 { name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] }

17.5 小结

前提为拷⻉类型为引⽤类型的情况下:

  • 浅拷⻉是拷⻉⼀层,属性为对象时,浅拷⻉是复制,两个对象指向同⼀个地址
  • 深拷⻉是递归拷⻉深层次,属性为对象时,深拷⻉是新开栈,两个对象指向不同的地址

18. 函数缓存及其应用场景

image.png

18.1 什么是函数缓存

函数缓存,就是将函数运算过的结果进行缓存

本质上就是用空间(缓存存储)换时间(计算过程)

常用于缓存数据计算结果和缓存对象

const add = (a,b) => a+b;
const calc = memoize(add); // 函数缓存
calc(10,20);// 30
calc(10,20);// 30 缓存

18.2 如何实现

实现函数缓存主要依靠 闭包柯⾥化⾼阶函数 ,这⾥再简单复习下:

18.2.1 闭包

闭包可以理解成:函数 + 函数体可访问变量总和

(function() {
    var a = 1;
    function add() {
        const b = 2
        let sum = b + a
        console.log(sum); // 3
    }
    add()
})()

add 函数本身,以及其内部可访问的变量,即 a=1,这两个组合在一起就形成了闭包。

18.2.2 柯里化

把接受多个参数的函数转换成接受⼀个单⼀参数的函数

// ⾮函数柯⾥化
var add = function (x,y) {
    return x+y;
}

add(3,4) //7

// 函数柯⾥化
var add2 = function (x) {
    //**返回函数**
    return function (y) {
        return x+y;
    }
}

add2(3)(4) //7

将⼀个⼆元函数拆分成两个⼀元函数

18.2.3 高阶函数

通过 接收其他函数作为参数返回其他函数的函数

function foo(){
    var a = 2;
    function bar() {
        console.log(a);
    }
    return bar;
}

var baz = foo();
baz();//2

函数 foo 如何返回另一个函数 bar ,baz 现在持有对 foo 中定义的 bar 函数的引⽤。由于闭包特性, a 的值能够得到

下⾯再看看如何 实现函数缓存 ,实现原理也很简单,把参数和对应的结果数据存在⼀个对象中,调⽤时判断参数对应的数据是否存在,存在就返回对应的结果数据,否则就返回计算结果

const memoize = function (func, content) {
    let cache = Object.create(null) // 在当前函数作用域定义了一个空对象,用于缓存结果
    content = content || this
    return (...key) => {// 运⽤柯⾥化返回⼀个函数,返回的函数由于闭包特性,可以访问到 cache
        // 然后判断输⼊参数是不是在 cache 的中。
        // 如果已经存在,直接返回 cache 的内容,
        // 如果没有存在,使⽤函数 func 对输⼊参数求值,然后把结果存储在 cache 中
        if (!cache[key]) {
            cache[key] = func.apply(content, key)
        }
        return cache[key]
    }
}
// 使用
const calc = memoize(add);
const num1 = calc(100, 200)
const num2 = calc(100, 200) // 缓存得到的结果

18.3 应用场景

虽然使⽤缓存效率是⾮常⾼的,但并不是所有场景都适⽤,因此千万不要极端的将所有函数都添加缓存 以下⼏种情况下,适合使⽤缓存:

  • 对于昂贵的函数调⽤,执⾏复杂计算的函数
  • 对于具有有限且⾼度重复输⼊范围的函数
  • 对于具有重复输⼊值的递归函数
  • 对于纯函数,即每次使⽤特定输⼊调⽤时返回相同输出的函数

19. 常见的字符串操作

image.png

19.1 字符串常见操作方法

我们也可将字符串常⽤的操作⽅法归纳为增、删、改、查。

需要知道字符串的特点是⼀旦创建了,就不可变

19.1.1 增

这⾥增的意思并不是说直接增添内容,⽽是创建字符串的⼀个副本,再进⾏操作

除了常⽤ + 以及 ${} 进⾏字符串拼接之外,还可通过 concat

19.1.1.1 concat

用于将一个或多个字符串拼接成一个新字符串

let stringValue = "hello ";
let result = stringValue.concat("world");

console.log(result); // "hello world"
console.log(stringValue); // "hello"

19.1.2 删

这⾥的删的意思并不是说删除原字符串的内容,⽽是创建字符串的⼀个副本,再进⾏操作

常⻅的有:

  • slice()
  • substr()
  • substring()

这三个⽅法都返回调⽤它们的字符串的⼀个⼦字符串,⽽且都接收⼀或两个参数。

let stringValue = "hello world";

console.log(stringValue.slice(3)); // "lo world"
console.log(stringValue.substring(3)); // "lo world"
console.log(stringValue.substr(3)); // "lo world"
console.log(stringValue.slice(3, 7)); // "lo w"
console.log(stringValue.substring(3,7)); // "lo w"
console.log(stringValue.substr(3, 7)); // "lo worl"

19.1.3 改

这⾥改的意思也不是改变原字符串,⽽是创建字符串的⼀个副本,再进⾏操作

常⻅的有:

  • trim()、trimLeft()、trimRight()
  • repeat()
  • padStart()、padEnd()
  • toLowerCase()、 toUpperCase()
19.1.3.1 trim() trimLeft() trimRight()

删除前、后或者前后所有空字符串,再返回新的字符串

let stringValue = " hello world ";
let trimmedStringValue = stringValue.trim();

console.log(stringValue); // " hello world "
console.log(trimmedStringValue); // "hello world"
19.1.3.2 repeat()

接收⼀个整数参数,表示要将字符串复制多少次,然后返回拼接所有副本后的结果

let stringValue = "na ";

let copyResult = stringValue.repeat(2) // na na
19.1.3.3 padStart()、padEnd()

复制字符串,如果⼩于指定⻓度,则在相应⼀边填充字符,直⾄满⾜⻓度条件

let stringValue = "foo";

console.log(stringValue.padStart(6)); // "   foo"
console.log(stringValue.padStart(9, ".")); // "......foo"
19.1.3.4 toLowerCase()、 toUpperCase()

⼤⼩写转化

let stringValue = "hello world";

console.log(stringValue.toUpperCase()); // "HELLO WORLD"
console.log(stringValue.toLowerCase()); // "hello world"

19.1.4 查

除了通过索引的⽅式获取字符串的值,还可通过:

  • charAt()
  • indexOf()
  • startWith()
  • includes()
19.1.4.1 charAt()

返回给定索引位置的字符,由传给方法的整数参数指定

let message = "abcde";

console.log(message.charAt(2)); // "c"
19.1.4.2 indexOf()

从字符串开头去搜索出入的字符串、并返回位置(如果没有找到,则返回 -1)

let stringValue = "hello world";

console.log(stringValue.indexOf("o")); // 4
19.1.4.3 startWith() inclueds()

从字符串中去搜索出入的字符串、并返回一个表示是否包含的布尔值

let message = "foobarbaz";

console.log(message.startsWith("foo")); // true
console.log(message.startsWith("bar")); // false
console.log(message.includes("bar")); // true
console.log(message.includes("qux")); // false

19.2 转换方法

19.2.1 split

把字符串按照指定的分隔符,拆分成数组中的每一项

let str = "12+23+34"
let arr = str.split("+") // [12,23,34]

19.3 模板匹配方法

针对正则表达式,字符串设计了几个方法

  • match()
  • search()
  • replace()

19.3.1 match()

接收一个参数,可以设计一个正则表达式字符串,也可以是一个RegExp对象,返回数组

let text = "cat, bat, sat, fat";
let pattern = /.at/;

let matches = text.match(pattern);
console.log(matches[0]); // "cat"

19.3.2 search()

接收⼀个参数,可以是⼀个正则表达式字符串,也可以是⼀个 RegExp 对象,找到则返回匹配索引,否则返回 -1

let text = "cat, bat, sat, fat";
let pos = text.search(/at/);

console.log(pos); // 1

19.3.3 replace()

接收两个参数,第⼀个参数为匹配的内容,第⼆个参数为替换的元素(可⽤函数)

let text = "cat, bat, sat, fat";
let result = text.replace("at", "ond");

console.log(result); // "cond, bat, sat, fat"

20.常见的数组操作

image.png

20.1 操作方法

数组基本操作可以归纳为 增、删、改、查,需要留意的是哪些⽅法会对原数组产⽣影响,哪些⽅法不会

下⾯对数组常⽤的操作⽅法做⼀个归纳

20.1.1 增

下⾯前三种是对原数组产⽣影响的增添⽅法,第四种则不会对原数组产⽣影响

  • push()
  • unshift()
  • splice()
  • concat()
20.1.1.1 push()

push() ⽅法接收任意数量的参数,并将它们添加到数组末尾,返回数组的最新⻓度

let colors = []; // 创建⼀个数组
let count = colors.push("red", "green"); // 推⼊两项

console.log(count) // 2
20.1.1.2 unshift()

unshift()在数组开头添加任意多个值,然后返回新的数组⻓度

let colors = new Array(); // 创建⼀个数组
let count = colors.unshift("red", "green"); // 从数组开头推⼊两项
alert(count); // 2
20.1.1.3 splice()

传⼊三个参数,分别是开始位置、0(要删除的元素数量)、插⼊的元素,返回空数组

let colors = ["red", "green", "blue"];
let removed = colors.splice(1, 0, "yellow", "orange")
console.log(colors) // red,yellow,orange,green,blue
console.log(removed) // []
20.1.1.4 concat()

⾸先会创建⼀个当前数组的副本; 然后再把它的参数添加到副本末尾; 最后返回这个新构建的数组。

不会影响原始数组

let colors = ["red", "green", "blue"];
let colors2 = colors.concat("yellow", ["black", "brown"]);
console.log(colors); // ["red", "green","blue"]
console.log(colors2); // ["red", "green", "blue", "yellow", "black", "brown"]

20.1.2 删

下⾯三种都会影响原数组,最后⼀项不影响原数组:

  • pop()
  • shift()
  • splice()
  • slice()
20.1.2.1 pop()

pop() ⽅法⽤于删除数组的最后⼀项,同时减少数组的 length 值,返回被删除的项

let colors = ["red", "green"]
let item = colors.pop(); // 取得最后⼀项
console.log(item) // green
console.log(colors.length) // 1
20.1.2.2 shift()

shift() ⽅法⽤于删除数组的第⼀项,同时减少数组的 length 值,返回被删除的项

let colors = ["red", "green"]
let item = colors.shift(); // 取得第⼀项
console.log(item) // red
console.log(colors.length) // 1
20.1.2.3 splice()

传⼊两个参数,分别是开始位置,删除元素的数量,返回包含删除元素的数组

let colors = ["red", "green", "blue"];
let removed = colors.splice(0,1); // 删除第⼀项
console.log(colors); // [green,blue]
console.log(removed); // [red],只有⼀个元素的数组
20.1.2.4 slice()

slice() ⽤于创建⼀个包含原有数组中⼀个或多个元素的新数组,不会影响原始数组

let colors = ["red", "green", "blue", "yellow", "purple"];
let colors2 = colors.slice(1);
let colors3 = colors.slice(1, 4);
console.log(colors) // red,green,blue,yellow,purple
concole.log(colors2); // green,blue,yellow,purple
concole.log(colors3); // green,blue,yellow

20.1.3 改

即修改原来数组的内容,常⽤ splice

20.1.3.1 splice()

传⼊三个参数,分别是开始位置,要删除元素的数量,要插⼊的任意多个元素,返回删除元素的数组,对原数组产⽣影响

let colors = ["red", "green", "blue"];
let removed = colors.splice(1, 1, "red", "purple"); // 插⼊两个值,删除⼀个元素
console.log(colors); // red,red,purple,blue
console.log(removed); // green,只有⼀个元素的数组

20.1.4 查

即查找元素,返回元素坐标或者元素值

  • indexOf()
  • includes()
  • find()
20.1.4.1 indexOf()

返回要查找的元素在数组中的位置,如果没找到则返回 -1

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.indexOf(4) // 3
20.1.4.2 includes()

返回要查找的元素在数组中的位置,找到返回 true ,否则 false

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.includes(4) // true
20.1.4.3 find()

返回第⼀个匹配的元素

const people = [
    {
        name: "Matt",
        age: 27
    },
    {
        name: "Nicholas",
        age: 29
    }
];

people.find((element, index, array) => element.age < 28) // // {name: "Matt", age: 27}

20.2 排序方法

数组有两个⽅法可以⽤来对元素重新排序:

  • reverse()
  • sort()
20.2.1 reverse()

顾名思义,将数组元素⽅向反转

let values = [1, 2, 3, 4, 5];

values.reverse();

alert(values); // 5,4,3,2,1
20.2.2 sort()

sort()⽅法接受⼀个⽐较函数,⽤于判断哪个值应该排在前⾯

function compare(value1, value2) {
    if (value1 < value2) {
        return -1; 
    } else if (value1 > value2) {
        return 1;
    } else {
        return 0;
    }
}

let values = [0, 1, 5, 10, 15];
values.sort(compare);
alert(values); // 0,1,5,10,15

20.3 转换方法

常见的转换方法:

  • join()

20.3.1 join()

join() ⽅法接收⼀个参数,即字符串分隔符, 返回包含所有项的字符串

let colors = ["red", "green", "blue"];

alert(colors.join(",")); // red,green,blue
alert(colors.join("||")); // red||green||blue

20.4 迭代方法(数组的遍历)

常⽤来迭代数组的⽅法(都不改变原数组)有如下:

  • some()
  • every()
  • forEach()
  • filter()
  • map()

20.4.1 some()

对数组每⼀项都运⾏传⼊的测试函数,如果⾄少有1个元素返回 true ,则这个⽅法返回 true

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let someResult = numbers.some((item, index, array) => item > 2);
console.log(someResult) // true

20.4.2 every()

对数组每⼀项都运⾏传⼊的测试函数,如果所有元素都返回 true ,则这个⽅法返回 true

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let everyResult = numbers.every((item, index, array) => item > 2);
console.log(everyResult) // false

20.4.3 forEach()

对数组每⼀项都运⾏传⼊的函数,没有返回值

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.forEach((item, index, array) => {
    // 执⾏某些操作
});

20.4.4 filter()

对数组每⼀项都运⾏传⼊的函数,函数返回 true 的项会组成数组之后返回

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let filterResult = numbers.filter((item, index, array) => item > 2);
console.log(filterResult); // 3,4,5,4,3

20.4.5 map()

对数组每⼀项都运⾏传⼊的函数,返回由每次函数调⽤的结果构成的数组

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let mapResult = numbers.map((item, index, array) => item * 2);
console.log(mapResult) // 2,4,6,8,10,8,6,4,2

21.事件循环

image.png

21.什么是事件循环

首先, JS 是一门单线程的语言,意味着同一时间内只能做一件事,但这并不意味着单线程就是阻塞,而实现单线程非阻塞的方法就是事件循环。

在JS中,所有的任务可以分为:

  • 同步任务(立即执行的任务,同步任务一般会直接进入到主线程中执行)
  • 异步任务(异步执行的任务,比如 ajax 网络请求、setTimeout 定时函数等)

同步任务和异步任务的运行流程图如下:

image.png 由上图可知:

  • 同步任务进入主线程,即主执行栈,异步任务进入任务队列
  • 主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入 主线程执行。
  • 上述过程的不断重复称为 事件循环.

21.2 宏任务与微任务

如果将任务划分为同步任务和异步任务并不是那么的准确,举个例⼦:

console.log(1)
setTimeout(()=>{
    console.log(2)
}, 0)

new Promise((resolve, reject)=>{
    console.log('new Promise')
    resolve()
}).then(()=>{
    console.log('then')
})

console.log(3)

如果按照上⾯流程图来分析代码,我们会得到下⾯的执⾏步骤:

  • console.log(1) ,同步任务,主线程中执⾏
  • setTimeout() ,异步任务,放到 Event Table ,0 毫秒后 console.log(2) 回调推⼊ Event Queue 中
  • new Promise ,同步任务,主线程直接执⾏
  • .then ,异步任务,放到 Event Table
  • console.log(3) ,同步任务,主线程执⾏

所以按照分析,它的结果应该是 1 => 'new Promise' => 3 => 2 => 'then'

但是实际结果是: 1 => 'new Promise' => 3 => 'then' => 2

出现分歧的原因在于异步任务执⾏顺序,事件队列其实是⼀个“先进先出”的数据结构,排在前⾯的事件会优先被主线程读取

例⼦中 setTimeout 回调事件是先进⼊队列中的,按理说应该先于.then 中的执⾏,但是结果却偏偏相反

原因在于异步任务还可以细分为微任务与宏任务

21.2.1 微任务

⼀个需要异步执⾏的函数,执⾏时机是在主函数执⾏结束之后、当前宏任务结束之前

常⻅的微任务有:

  • Promise.then
  • MutaionObserver
  • Object.observe(已废弃;Proxy 对象替代)
  • process.nextTick(Node.js)

21.2.2 宏任务

宏任务的时间粒度⽐较⼤,执⾏的时间间隔是不能精确控制的,对⼀些⾼实时性的需求就不太符合

常⻅的宏任务有:

  • script (可以理解为外层同步代码)
  • setTimeout/setInterval
  • UI rendering/UI事件
  • postMessage、MessageChannel
  • setImmediate、I/O(Node.js)

这时候,事件循环,宏任务,微任务的关系如图所示:

image.png 按照这个流程,执行机制是:

  1. 执行一个宏任务,如果遇到微任务就将它放到微任务的事件队列中
  2. 当前宏任务执行完毕后,会查看微任务的事件队列,然后将里面的所有微任务依次执行完毕

回到上面的题目:

console.log(1)
setTimeout(()=>{
    console.log(2)
}, 0)

new Promise((resolve, reject)=>{
    console.log('new Promise')
    resolve()
}).then(()=>{
    console.log('then')
})

console.log(3)

// 流程如下:
// 遇到 console.log(1) ,直接打印 1
// 遇到定时器,属于新的宏任务,留着后⾯执⾏
// 遇到 new Promise,这个是直接执⾏的,打印 'new Promise'
// .then 属于微任务,放⼊微任务队列,后⾯再执⾏
// 遇到 console.log(3) 直接打印 3
// 好了本轮宏任务执⾏完毕,现在去微任务列表查看是否有微任务,发现 .then 的回调,执⾏它,打印 'then'
// 当⼀次宏任务执⾏完,再去执⾏新的宏任务,这⾥就剩⼀个定时器的宏任务了,执⾏它,打印 2

21.3 async 与 await

async 是异步的意思, await 则可以理解为 async wait。所以可以理解 async 就是用来声明一个异步方法的,而 await 是用来等待异步方法执行

21.3.1 async

async 函数返回⼀个 promise 对象,下⾯两种⽅法是等效的

function f() {
    return Promise.resolve('TEST');
}

// asyncF is equivalent to f!
async function asyncF() {
    return 'TEST';
}

21.3.2 await

正常情况下, await 命令后⾯是⼀个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值

async function f(){
// 等同于
// return 123
    return await 123
}

f().then(v => console.log(v)) // 123

不管 await 后⾯跟着的是什么, await 都会阻塞后⾯的代码

async function fn1 (){
    console.log(1)
    await fn2()
    console.log(2) // 阻塞
}

async function fn2 (){
    console.log('fn2')
}

fn1()
console.log(3)

上面的例子中, await 会阻塞下面的代码(即加入微任务队列),先执行 async 外面的同步代码,同步代码执行完毕,再回到 async 函数中,再执行之前阻塞的代码

所以上述输出结果为:1 fn2 3 2

21.4 流程分析

async function async1() {
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}

async function async2() {
    console.log('async2')
}

console.log('script start')

setTimeout(function () {
    console.log('settimeout')
})

async1()

new Promise(function (resolve) {
    console.log('promise1')
    resolve()
}).then(function () {
    console.log('promise2')
})

console.log('script end')

分析过程:

  1. 执⾏整段代码,遇到 console.log('script start') 直接打印结果,输出 script start

  2. 遇到定时器了,它是宏任务,先放着不执⾏

  3. 遇到 async1() ,执⾏ async1 函数,先打印async1 start ,下⾯遇到 await 怎么办?先执⾏ async2 ,打印 async2 ,然后阻塞下⾯代码(即加⼊微任务列表),跳出去执⾏同步代码

  4. 跳到 new Promise 这⾥,直接执⾏,打印 promise1 ,下⾯遇到.then() ,它是微任务,放到微任务列表等待执⾏

  5. 最后⼀⾏直接打印 script end ,现在同步代码执⾏完了,开始执⾏微任务,即 await 下⾯的代码,打印 async1 end

  6. 继续执⾏下⼀个微任务,即执⾏ then 的回调,打印 promise2

  7. 上⼀个宏任务所有事都做完了,开始下⼀个宏任务,就是定时器,打印 settimeout

所以最后的结果是: script start 、 async1 start 、 async2 、promise1 、 script end 、 async1 end 、 promise2 、 settimeout

22. JS 中的本地存储

image.png

22.1 本地存储的方式

javaScript 本地缓存的⽅法我们主要讲述以下四种:

  • cookie
  • sessionStorage
  • localStorage
  • indexedDB

22.1.1 cookie

Cookie, 类型为「小型文本文件」,指某些网站为了辨别用户身份而储存用户本地终端上的数据。是为了解决 HTTP 无状态导致的问题。

作为一段一般不超过 4KB 的小型文本数据,由一个Name(名称)、一个Value(值)和其他几个用于控制Cookie 有效期、安全性、使用范围的可选属性组成。

但是Cookie 在 每次请求中都会被发送,如果不适用 HTTPS 并对其加密,其保存的信息很容易被窃取,导致安全风险。 举个例子: 在一些使用 Cookie 保存登录状态的网站上,如果Cookie被盗取,他人很容易利用你的Cookie来假扮成你登录网站。

Cookie 常用的属性:

  1. Expires 用于设置 Cookie的过期时间
Expires=Wed, 21 Oct 2015 07:28:00 GMT
  1. Max-Age 用于设置在 Cookie 失效之前需要经过的秒数(优先级比 Expires 高)
Max-Age = 604800
  1. Domain 指定 Cookie 可以送达的主机名
  2. Path 执行一个 URL 路径,这个路径必须出现在要请求的资源的路径中才可以发送 Cookie首部
Path=/docs # /docs/Web/ 下的资源会带 Cookie ⾸部

  1. 标记为 Secure 的 Cookie 只应通过被HTTPS 协议加密过的请求发送给服务端

通过上述,我们可以看到 cookie ⼜开始的作⽤并不是为了缓存⽽设计出来,只是借⽤了 cookie 的特性实现缓存

关于Cookie的使用如下:

document.cookie = '名字=值';

关于 cookie 的修改,⾸先要确定 domain 和path 属性都是相同的才可以,其中有⼀个不同得时候都会创建出⼀个新的 cookie

Set-Cookie:name=aa; domain=aa.net; path=/ # 服务端设置
document.cookie =name=bb; domain=aa.net; path=/ # 客户端设置;

最后 cookie 的删除,最常⽤的⽅法就是给 cookie 设置⼀个过期的事件,这样cookie 过期后会被浏览器删除

22.1.3 localStorage

HTML5 新⽅法,IE8及以上浏览器都兼容

22.1.3.1 特点
  • 生命周期:持久化的本地存储,除非主动删除数据,否则数据永远不会过期
  • 存储的信息在同一域中是共享的
  • 当本页操作(新增、修改、删除)了 localStorage 的时候,本页面不会触发 storage事件,但别的页面会触发 storage 事件。
  • 大小:5M(跟浏览器厂商有关系)
  • localStorage 本质上是对字符串的读取,如果存储内容多的话会消耗内存空间,会导致页面变卡。
  • 受同源策略的限制

下面是 localStorage 的使用:

  • 设置:
localStorage.setItem('username','cfangxu');
  • 获取:
localStorage.getItem('username'); // cfangxu
  • 获取键名:
localStorage.key(0); // 获取第一个键名
  • 删除:
localStorage.removeItem('username'); 
  • 一次性清楚所有存储:
localStorage.clear(); 

缺点 :

  1. 无法像 Cookie 一样设置过期时间
  2. 只能存入字符串,无法直接存储对象
localStorage.setItem('key', {name: 'value'});
console.log(localStorage.getItem('key')); // '[object, Object]'

22.1.4 sessionStorage

sessionStorage 和 localStorage 使⽤⽅法基本⼀致,唯⼀不同的是⽣命周期,⼀旦⻚⾯(会话)关闭, sessionStorage 将会删除数据

22.1.5 扩展的前端存储方式(IndexedDB)

indexedDB 是一种低级 API,用于客户端存储大量接过话数据(包括 文件/blobs)

该API使用索引来实现对该数据的高性能搜索

虽然 Web Storage 对于存储较少量的数据很有用,但对于存储更大量的接过话数据来说,这种方法不太有用。IndexedDB 提供了一个解决方案。

22.1.5.1 优点
  • 储存量理论上没有上限
  • 所有操作都是异步的,相比 localStorage 同步操作性能更高,尤其是数据量较大时
  • 原生支持 存储 JS 对象
  • 是个正经的数据库,意味着数据库能干的事它都能干
22.1.5.2 缺点
  • 操作非常繁琐
  • 本身有一定门槛

关于 indexedDB 的使用基本使用步骤如下:

  1. 打开数据库并且开始一个事务
  2. 创建一个 object store
  3. 构建一个请求来执行一些数据库操作:增加或提取数据等。
  4. 通过监听正确类型的DOM 事件已等待操作完成
  5. 在操作结果上进行一些操作(可以在 request 对象中找到)

关于使⽤ indexdb 的使⽤会⽐较繁琐,⼤家可以通过使⽤ Godb.js 库进⾏缓存,最⼤化的降低操作 难度

22.2 区别

关于 cookie 、 sessionStorage 、 localStorage 三者的区别主要如下:

  1. 存储⼤⼩:

    1. cookie 数据⼤⼩不能超过4k,
    2. sessionStorage 和 localStorage 虽然也有存储⼤⼩的限制,但⽐ cookie ⼤得多,可以达到5M或更⼤
  2. 有效时间:

    1. localStorage 存储持久数据,浏览器关闭后数据不丢失除⾮主动删除数据;
    2. sessionStorage 数据在当前浏览器窗⼝关闭后⾃动删除;
    3. cookie 设置的cookie 过期时间之前⼀直有效,即使窗⼝或浏览器关闭
  3. 数据与服务器之间的交互⽅式,

    1. cookie 的数据会⾃动的传递到服务器,服务器端也可以写 cookie 到客户端;
    2. sessionStorage 和localStorage 不会⾃动把数据发给服务器,仅在本地保存

22.3 应用场景

针对不同场景的使用选择:

  • 标记⽤户与跟踪⽤户⾏为的情况,推荐使⽤ cookie
  • 适合⻓期保存在本地的数据(令牌),推荐使⽤ localStorage
  • 敏感账号⼀次性登录,推荐使⽤ sessionStorage
  • 存储⼤量数据的情况、在线⽂档(富⽂本编辑器)保存编辑历史的情况,推荐使⽤ indexedDB

23. 大文件上传如何做断点续传

image.png

23.1 什么是断点续传

不管怎样简单的需求,在量级达到⼀定层次时,都会变得异常复杂

⽂件上传简单,⽂件变⼤就复杂

上传⼤⽂件时,以下⼏个变量会影响我们的⽤户体验:

  • 服务器处理数据的能力
  • 请求超时
  • 网络波动

上传时间会变⻓,⾼频次⽂件上传失败,失败后⼜需要重新上传等等

为了解决上述问题,我们需要对⼤⽂件上传单独处理

这⾥涉及到 分⽚上传断点续传 两个概念

23.1.1 分片上传

分⽚上传,就是将所要上传的⽂件,按照⼀定的⼤⼩,将整个⽂件分隔成多个数据块(Part)来进⾏分⽚上传 image.png

上传完之后再由服务端对所有上传的⽂件进行汇总整合成原始的⽂件

大致流程如下:

  1. 将需要上传的⽂件按照⼀定的分割规则,分割成相同⼤⼩的数据块;
  2. 初始化⼀个分⽚上传任务,返回本次分⽚上传唯⼀标识;
  3. 按照⼀定的策略(串⾏或并⾏)发送各个分⽚数据块;
  4. 发送完成后,服务端根据判断数据上传是否完整,如果完整,则进⾏数据块合成得到原始⽂件

23.1.2 断点续传

断点续传指的是在下载或上传时,将下载或上传任务⼈为的划分为⼏个部分

每⼀个部分采⽤⼀个线程进⾏上传或下载,如果碰到⽹络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,⽽没有必要从头开始上传下载。⽤户可以节省时间,提⾼速度

⼀般实现⽅式有两种:

  • 服务器端返回,告知从哪开始
  • 浏览器端⾃⾏处理

上传过程中将⽂件在服务器写为临时⽂件,等全部写完了(⽂件上传完),将此临时⽂件重命名为正式⽂件即可

如果中途上传中断过,下次上传的时候根据当前临时⽂件⼤⼩,作为在客户端读取⽂件的偏移量,从此位置继续读取⽂件数据块,上传到服务器从此偏移量继续写⼊⽂件即可

23.2 实现思路

整体思路⽐较简单,拿到⽂件,保存⽂件唯⼀性标识,切割⽂件,分段上传,每次上传⼀段,根据唯⼀性标识判断⽂件上传进度,直到⽂件的全部⽚段上传完毕

image.png

下面写一下实现的伪代码:

  • 读取文件内容:
// 1. 读取文件内容
const input = document.querySelector('input');
input.addEventListener('change', function() {
    var file = this.files[0];
});

// 2. 可以使用 md5 实现文件的唯一性
const md5Code = md5(file)

// 3. 开始对文件进行分割
var reader = new FileReader()
reader.readAsArrayBuffer(file)
reader.addEventListener('load', function(e) {
    // 每10M 切割一段,这里只做一个切割演示,实际切割需要循环切割
    var slice = e.target.result.slice(0, 10 * 1024 * 1024);
}

// h5 上传一个(一片)
const formdata = new FormData()
formdata.append('0', slice)
// 这里是有一个坑的,部分设备无法获取文件名称和文件乐行,这个在最后给出解决方案
formdata.append('filename', file.filename)
var xhr = new XMLHttpRequest();
xhr.addEventListener('load', function() {
    // xhr.responseText
)};
xhr.open('POST', '');
xhr.send(formdata);
xhr.addEventListener('progress', updateProgress);
xhr.upload.addEventListener('progress', updateProgress);
function updateProgress(event) {
    if (event.lengthComputable) {
        // 进度条
    }
}

这里给出常见的图片和视频的文件类型判断:

function checkFileType(type, file, back) {
    /**
    * type png jpg mp4 ...
    * file input.change=> this.files[0]
    * back callback(boolean)
    */

    var args = arguments;
    if (args.length != 3) {
        back(0);
    }

    var type = args[0]; // type = '(png|jpg)' , 'png'
    var file = args[1];
    var back = typeof args[2] == 'function' ? args[2] : function() {};

    if (file.type == '') {
    // 如果系统⽆法获取⽂件类型,则读取⼆进制流,对⼆进制进⾏解析⽂件类型
        var imgType = [
            'ff d8 ff', //jpg
            '89 50 4e', //png

            '0 0 0 14 66 74 79 70 69 73 6F 6D', //mp4
            '0 0 0 18 66 74 79 70 33 67 70 35', //mp4
            '0 0 0 0 66 74 79 70 33 67 70 35', //mp4
            '0 0 0 0 66 74 79 70 4D 53 4E 56', //mp4
            '0 0 0 0 66 74 79 70 69 73 6F 6D', //mp4

            '0 0 0 18 66 74 79 70 6D 70 34 32', //m4v
            '0 0 0 0 66 74 79 70 6D 70 34 32', //m4v

            '0 0 0 14 66 74 79 70 71 74 20 20', //mov
            '0 0 0 0 66 74 79 70 71 74 20 20', //mov
            '0 0 0 0 6D 6F 6F 76', //mov

            '4F 67 67 53 0 02', //ogg
            '1A 45 DF A3', //ogg

            '52 49 46 46 x x x x 41 56 49 20', //avi (RIFF fileSize fileTy pe LIST)(52 49 46 46,DC 6C 57 09,41 56 49 20,4C 49 53 54)

        ];

        var typeName = [
            'jpg',
            'png',
            'mp4',
            'mp4',
            'mp4',
            'mp4',
            'mp4',
            'm4v',
            'm4v',
            'mov',
            'mov',
            'mov',
            'ogg',
            'ogg',
            'avi',
        ];

        var sliceSize = /png|jpg|jpeg/.test(type) ? 3 : 12;
        var reader = new FileReader();
        reader.readAsArrayBuffer(file);
        reader.addEventListener("load", function(e) {
            var slice = e.target.result.slice(0, sliceSize);
            reader = null;
        
            if (slice && slice.byteLength == sliceSize) {
                var view = new Uint8Array(slice);
                var arr = [];
                view.forEach(function(v) {
                    arr.push(v.toString(16));
                });
                view = null;
                var idx = arr.join(' ').indexOf(imgType);
                if (idx > -1) {
                    back(typeName[idx]);
                } else {
                    arr = arr.map(function(v) {
                        if (i > 3 && i < 8) {
                            return 'x';
                        }
                        return v;
                    });
                    var idx = arr.join(' ').indexOf(imgType);
                    if (idx > -1) {
                        back(typeName[idx]);
                    } else {
                        back(false);
                    }
                }
            } else {
                back(false);
            }
        });
    } else {
        var type = file.name.match(/\.(\w+)$/)[1];
        back(type);
    }
}

使用方法:

checkFileType('(mov|mp4|avi)',file,function(fileType){
    // fileType = mp4,
    // 如果file的类型不在枚举之列,则返回false
});
// 上面上传文件的一步,可以改成:
formdata.append('filename', md5code+'.'+fileType);

有了切割上传后,也就有了⽂件唯⼀标识信息,断点续传变成了后台的⼀个⼩⼩的逻辑判断

后端主要做的内容为:根据前端传给后台的 md5 值,到服务器磁盘查找是否有之前未完成的⽂件合并信息(也就是未完成的半成品⽂件切⽚),取到之后根据上传切⽚的数量,返回数据告诉前端开始从第⼏节上传

如果想要暂停切⽚的上传,可以使⽤ XMLHttpRequest 的 abort ⽅法

23.3 使用场景

  • ⼤⽂件加速上传:当⽂件⼤⼩超过预期⼤⼩时,使⽤分⽚上传可实现并⾏上传多个 Part, 以加快上传速度
  • ⽹络环境较差:建议使⽤分⽚上传。当出现上传失败的时候,仅需重传失败的Part
  • 流式上传:可以在需要上传的⽂件⼤⼩还不确定的情况下开始上传。这种场景在视频监控等⾏业应⽤中⽐较常⻅

23.4

当前的伪代码,只是提供⼀个简单的思路,想要把事情做到极致,我们还需要考虑到更多场景,⽐如:

  • 切⽚上传失败怎么办
  • 上传过程中刷新⻚⾯怎么办
  • 如何进⾏并⾏上传
  • 切⽚什么时候按数量切,什么时候按⼤⼩切
  • 如何结合 Web Worker 处理⼤⽂件上传
  • 如何实现秒传

24.ajax

image.png

24.1 ajax 是什么

AJAX 全程 Async JavaScript and XML 即 异步的 JavaScript 和 XML,是一种创建交互式网页应用的网页开发技术,可以在不重新加载整个网页的情况下,与服务器交换数据,并且更新部分网页

Ajax 的原理简单来说 通过 XmlHttpRequest 对象来向服务器发送异步骑牛,从服务器获得数据,然后用JavaScript 来操作 DOM 而更新页面

流程图如下: image.png

举个例子: 领导想找⼩李汇报⼀下⼯作,就委托秘书去叫⼩李,⾃⼰就接着做其他事情,直到秘书告诉他⼩李已经 到了,最后⼩李跟领导汇报⼯作

Ajax 请求数据流程与“领导想找⼩李汇报⼀下⼯作”类似,上述秘书就相当于 XMLHttpRequest 对 象,领导相当于浏览器,响应数据相当于⼩李

浏览器可以发送 HTTP 请求后,接着做其他事情,等收到 XHR 返回来的数据再进⾏操作

24.2 实现过程

实现 Ajax 异步交互需要服务器逻辑进行配合,完成以下步骤:

  1. 创建Ajax 的核心对象 XMLHttpRequest 对象
  2. 通过 XMLHttpRequest 对象的 open() 方法与服务器建立连接
  3. 构建请求所需的数据内容,并通过 XMLHttpRequest 对象的 send() 方法 发送给服务器端
  4. 通过 XMLHttpRequest 对象提供的 onreadystatechange 事件监听服务器端的通信状态
  5. 接收并处理服务器端向客户端响应的数据结果
  6. 将处理结果更新到 HTML 页面中

24.2.1 创建XMLHttpRequest对象

通过 XMLHttpRequest() 构造函数⽤于初始化⼀个 XMLHttpRequest 实例对象

const xhr = new XMLHttpRequest();

24.2.2 与服务器建立连接

通过 XMLHttpRequest 对象的 open() 方法与服务器建立连接

xhr.open(method, url, [async][, user][, password])

参数说明:

  • method :表示当前的请求⽅式,常⻅的有 GET 、 POST
  • url :服务端地址
  • async :布尔值,表示是否异步执⾏操作,默认为 true
  • user : 可选的⽤户名⽤于认证⽤途;默认为null
  • password : 可选的密码⽤于认证⽤途,默认为null

24.2.3 给服务端发送数据

通过 XMLHttpRequest 对象的 send() 方法 发送给服务器端

xhr.send([body])

body : 在 XHR 请求中要发送的数据体,如果不传递数据则为null

如果使⽤ GET 请求发送数据的时候,需要注意如下:

  • 将请求数据添加到 open() ⽅法中的 url 地址中
  • 发送请求数据中的 send() ⽅法中参数设置为 null

24.2.4 绑定onreadystatechange 事件

onreadystatechange 事件⽤于监听服务器端的通信状态,主要监听的属性为 XMLHttpRequest.readyState

关于 XMLHttpRequest.readyState 属性有五个状态,如下图显示

状态描述
0UNSENT(未打开)open()方法还未被调用
1OPENED(未发送)send()方法还未被调用
2HANDERS_RRECEIVED(已获取响应头)send()方法已经被调用,响应头和响应状态已经返回
3LOADING(正在下载响应体)响应体下载中;responseText中已经获取部分数据
4DONE(请求完成)整个请求过程已完毕

只要 readyState 属性值⼀变化,就会触发⼀次 readystatechange 事件

XMLHttpRequest.responseText 属性⽤于接收服务器端的响应结果

const request = new XMLHttpRequest()

request.onreadystatechange = function(e){
    if(request.readyState === 4){ // 整个请求过程完毕
        if(request.status >= 200 && request.status <= 300){
            console.log(request.responseText) // 服务端返回的结果
        }else if(request.status >=400){
            console.log("错误信息:" + request.status)
        }
    }
}

request.open('POST','http://xxxx')

request.send()

24.3 封装

通过上⾯对 XMLHttpRequest 对象的了解,下⾯来封装⼀个简单的 ajax 请求

//封装⼀个ajax请求
function ajax(options) {
    //创建XMLHttpRequest对象
    const xhr = new XMLHttpRequest()

    //初始化参数的内容
    options = options || {}
    options.type = (options.type || 'GET').toUpperCase()
    options.dataType = options.dataType || 'json'
    const params = options.data

    //发送请求
    if (options.type === 'GET') {
        xhr.open('GET', options.url + '?' + params, true)
        xhr.send(null)
    } else if (options.type === 'POST') {
        xhr.open('POST', options.url, true)
        xhr.send(params)
    }
    
    //接收请求
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {
            let status = xhr.status
            if (status >= 200 && status < 300) {
                options.success && options.success(xhr.responseText, xhr.responseXML)
            } else {
                options.fail && options.fail(status)
            }
        }
    }
}

// 使用方式
ajax({
    type: 'post',
    dataType: 'json',
    data: {},
    url: 'https://xxxx',
    success: function(text,xml){//请求成功后的回调函数
        console.log(text)
    },
    fail: function(status){////请求失败后的回调函数
        console.log(status)
    }
})

25.防抖与节流

image.png

25.1 防抖与节流分别是什么

本质上是优化⾼频率执⾏代码的⼀种⼿段

如:浏览器的 resize 、 scroll 、 keypress 、 mousemove 等事件在触发时,会不断地调⽤绑定在事件上的回调函数,极⼤地浪费资源,降低前端性能

为了优化体验,需要对这类事件进⾏调⽤次数的限制,对此我们就可以采⽤ 防抖(debounce) 和 节流 (throttle) 的⽅式来减少调⽤频率

25.1.1 定义

  • 节流:n秒内置运行一次,若在n秒内重复触发,只有一次生效
  • 防抖:n秒后再执行该事件,若在 n 秒内被重复触发,则重新计时

Version:0.9 StartHTML:0000000105 EndHTML:0000001654 StartFragment:0000000141 EndFragment:0000001614

⼀个经典的⽐喻:

想象每天上班⼤厦底下的电梯。把电梯完成⼀次运送,类⽐为⼀次函数的执⾏和响应

假设电梯有两种运⾏策略 debounce 和 throttle ,超时设定为15秒,不考虑容量限制

电梯第⼀个⼈进来后,15秒后准时运送⼀次,这是节流

电梯第⼀个⼈进来后,等待15秒。如果过程中⼜有⼈进来,15秒等待重新计时,直到15秒后开始运送,这是防抖

25.1.2 代码实现

25.1.2.1 节流

完成节流可以使⽤时间戳与定时器的写法

  • 时间戳写法:事件会立即执行,停止触发后没有办法再次执行
function throttled1(fn, delay = 500) {
    let oldtime = Date.now()
    return function (...args) {
        let newtime = Date.now()
        if (newtime - oldtime >= delay) {
            fn.apply(null, args)
            oldtime = Date.now()
        }
    }
}
  • 定时器写法:delay 毫秒后第一次执行,第二次事件体制触发后依然会再一次执行
function throttled2(fn, delay = 500) {
    let timer = null
    return function (...args) {
        if (!timer) {
            timer = setTimeout(() => {
                fn.apply(this, args)
                timer = null
            }, delay);
        }
    }
}
  • 时间戳与定时器结合,实现更加精确的节流:
function throttled(fn, delay) {
    let timer = null
    let starttime = Date.now()
    return function () {
        let curTime = Date.now() // 当前时间
        let remaining = delay - (curTime - starttime) // 从上⼀次到现在,还剩下多少多余时间
        let context = this
        let args = arguments
        clearTimeout(timer)
        if (remaining <= 0) {
            fn.apply(context, args)
            starttime = Date.now()
        } else {
            timer = setTimeout(fn, remaining);
        }
    }
}
25.1.2.2 防抖

简易版本:

function debounce(func, wait) {
    let timeout;
    return function () {
        let context = this; // 保存this指向
        let args = arguments; // 拿到event对象
        clearTimeout(timeout)
        timeout = setTimeout(function(){
            func.apply(context, args)
        }, wait);
    }
}

防抖如果需要⽴即执⾏,可加⼊第三个参数⽤于判断,实现如下:

function debounce(func, wait, immediate) {
    let timeout;
    return function () {
        let context = this;
        let args = arguments;
        if (timeout) clearTimeout(timeout); // timeout 不为null
        if (immediate) {
            let callNow = !timeout; // 第⼀次会⽴即执⾏,以后只有事件执⾏后才会再次触发
            timeout = setTimeout(function () {
                timeout = null;
            }, wait)
            if (callNow) {
                func.apply(context, args)
            }
        }
        else {
            timeout = setTimeout(function () {
                func.apply(context, args)
            }, wait);
        }
    }
}

25.2 区别

相同点:

  • 都可以通过使⽤ setTimeout 实现
  • ⽬的都是,降低回调执⾏频率。节省计算资源

不同点:

  • 函数防抖,在⼀段连续操作结束后,处理回调,利⽤ clearTimeout 和 setTimeout 实现。函数节流,在⼀段连续操作中,每⼀段时间只执⾏⼀次,频率较⾼的事件中使⽤来提⾼性能
  • 函数防抖关注⼀定时间连续触发的事件,只在最后执⾏⼀次,⽽函数节流⼀段时间内只执⾏⼀次

例如,都设置时间频率为500ms,在2秒时间内,频繁触发函数,节流,每隔 500ms 就执⾏⼀次。防抖,则不管调动多少次⽅法,在2s后,只会执⾏⼀次

image.png

25.3 应用场景

防抖在连续的事件,只需触发⼀次回调的场景有:

  • 搜索框搜索输⼊。只需⽤户最后⼀次输⼊完,再发送请求
  • ⼿机号、邮箱验证输⼊检测
  • 窗⼝⼤⼩ resize 。只需窗⼝调整完成后,计算窗⼝⼤⼩。防⽌重复渲染。

节流在间隔⼀段时间执⾏⼀次回调的场景有:

  • 滚动加载,加载更多或滚到底部监听
  • 搜索框,搜索联想功能

26 如何判断一个元素是否在可视区域

image.png

26.1 用途

可视区域,即我们浏览网页的设备肉眼可见的区域,如下图:

image.png 在⽇常开发中,我们经常需要判断⽬标元素是否在视窗之内或者和视窗的距离⼩于⼀个值(例如 100px),从⽽实现⼀些常⽤的功能,例如:

  • 图⽚的懒加载
  • 列表的⽆限滚动
  • 计算⼴告元素的曝光情况
  • 可点击链接的预加载

26.2 实现方式

判断⼀个元素是否在可视区域,我们常⽤的有三种办法:

  • offsetTop、scrollTop
  • getBoundingClientRect
  • Intersection Observer

26.2.1 offsetTop、scrollTop

  • offsetTop:元素的上外边框到包含元素的上内边框之间的像素距离,其他offset 属性如下图所示: image.png 下⾯再来了解下 clientWidth 、 clientHeight :

  • clientWidth :元素内容区宽度加上左右内边距宽度,即 clientWidth = content + padding(left + right)

  • clientHeight :元素内容区⾼度加上上下内边距⾼度,即 clientHeight = content + padding(top + bottom)

这⾥可以看到 client 元素都不包括外边距

Version:0.9 StartHTML:0000000105 EndHTML:0000002707 StartFragment:0000000141 EndFragment:0000002667

最后,关于 scroll 系列的属性如下:

  • scrollWidth 和 scrollHeight 主要⽤于确定元素内容的实际⼤⼩
  • scrollLeft 和 scrollTop 属性既可以确定元素当前滚动的状态,也可以设置元素的滚动位置
    • 垂直滚动 scrollTop > 0
    • ⽔平滚动 scrollLeft > 0
  • 将元素的 scrollLeft 和scrollTop 设置为 0,可以重置元素的滚动位置
26.2.1.1 注意

上述属性都是 只读的,每次访问都要重新开发

下⾯再看看如何实现判断(元素是否在可视窗口中):

公式如下:

// el.offsetTop - document.documentElement.scrollTop <= viewPortHeight
// 代码实现
function isInViewPortOfOne (el) {
    // viewPortHeight 兼容所有浏览器写法
    const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
    const offsetTop = el.offsetTop
    const scrollTop = document.documentElement.scrollTop
    const top = offsetTop - scrollTop
    return top <= viewPortHeight
}

26.2.2 getBoundingClientRect

返回值是⼀个 DOMRect 对象,拥有 left , top , right , bottom , x , y , width , 和 height 属性

const target = document.querySelector('.target');
const clientRect = target.getBoundingClientRect();
console.log(clientRect);
// {
// bottom: 556.21875,
// height: 393.59375,
// left: 333,
// right: 1017,
// top: 162.625,
// width: 684
// }

属性对应的关系图如下:

image.png 当⻚⾯发⽣滚动的时候, top 与 left 属性值都会随之改变

如果⼀个元素在视窗之内的话,那么它⼀定满⾜下⾯四个条件:

  • top ⼤于等于 0
  • left ⼤于等于 0
  • bottom ⼩于等于视窗⾼度
  • right ⼩于等于视窗宽度

实现代码:

function isInViewPort(element) {
    const viewWidth = window.innerWidth || document.documentElement.clientWidth;
    const viewHeight = window.innerHeight || document.documentElement.clientHeight;
    const {
        top,
        right,
        bottom,
        left,
    } = element.getBoundingClientRect();
    return (
        top >= 0 &&
        left >= 0 &&
        right <= viewWidth &&
        bottom <= viewHeight
    );
}

26.2.3 Intersection Observer

Intersection Observer 即重叠观察者,从这个命名就可以看出它⽤于判断两个元素是否重叠,因为不⽤进⾏事件的监听,性能⽅⾯相⽐ getBoundingClientRect 会好很多

使⽤步骤主要分为两步:创建观察者和传⼊被观察者

26.2.3.1 创建观察者
const options = {
    // 表示重叠⾯积占被观察者的⽐例,从 0 - 1 取值,
    // 1 表示完全被包含
    threshold: 1.0,
    root:document.querySelector('#scrollArea') // 必须是⽬标元素的⽗级元素
};

const callback = (entries, observer) => { ....}
const observer = new IntersectionObserver(callback, options);

通过 new IntersectionObserver 创建了观察者 observer ,传⼊的参数 callback 在重叠⽐例超过 threshold 时会被执⾏

// 上段代码中被省略的 callback
const callback = function(entries, observer) {
    entries.forEach(entry => {
        entry.time; // 触发的时间
        entry.rootBounds; // 根元素的位置矩形,这种情况下为视窗位置
        entry.boundingClientRect; // 被观察者的位置举⾏
        entry.intersectionRect; // 重叠区域的位置矩形
        entry.intersectionRatio; // 重叠区域占被观察者⾯积的⽐例(被观察者不是矩形时也按照矩形计算)
        entry.target; // 被观察者
    });
};
26.2.3.2 传⼊被观察者

通过 observer.observe(target) 这⼀⾏代码即可简单的注册被观察者

const target = document.querySelector('.target');
observer.observe(target);

26.2.4 案例分析

实现:创建了⼀个⼗万个节点的⻓列表,当节点滚⼊到视窗中时,背景就会从红⾊变为⻩⾊

<div class="container"></div>
.container {
    display: flex;
    flex-wrap: wrap;
}

.target {
    margin: 5px;
    width: 20px;
    height: 20px;
    background: red;
}
// 往 container 中插入 1000 个元素
const $container = $(".container");

// 插⼊ 100000 个 <div class="target"></div>
function createTargets() {
    const htmlString = new Array(100000).fill('<div class="target"></div>').join("");
    $container.html(htmlString);
}
  1. 首先使用 getBoundingClientRect 方法判断元素是否在可是区域
function isInViewPort(el) {
    const viewWidth = window.innerWidth || document.documentElement.clientWidth;
    const viewHeight = window.innerHeight || document.documentElement.clientHeight;
    
    const {top, bottom, left, right} = el.getBoundingClientRect();
    
    return top >= 0 && left >= 0 && bottom <= viewHeight && right <= viewWidth;
}
  1. 然后开始监听 scroll 事件,判断页面上哪些元素在可视区域,如果在可视区域中则将背景颜色设置为 yellow
$(window).on('scroll', () => {
    $targets.each(index, el) => {
        if (isInViewPort(el) => {
            $(el).css('background-color', 'yellow')
        }
    }
}

通过上述⽅式,可以看到可视区域颜⾊会变成⻩⾊了,但是可以明显看到有卡顿的现象,原因在于我们 绑定了 scroll 事件, scroll 事件伴随了⼤量的计算,会造成资源⽅⾯的浪费

下⾯通过 Intersection Observer 的形式同样实现相同的功能

  1. 首先创建一个观察者
const observer = new IntersectionObserver(getYellow, { threshold: 1.0 })

function getYellow(entries, observer){
    entries.forEach(entry => {
        $(entry.target).css('background-color', 'yellow')
    }
}
  1. 传入被观察者
$targets.each((index, element) => {
    observer.observe(element);
});

可以看到功能同样完成,并且⻚⾯不会出现卡顿的情况