Skip to content

JavaScript

1. 数据类型

数据类型

  • 在JS中共有8种基础的数据类型,分别为:UndefinedNullBooleanNumberStringObjectSymbolBigInt
  • 其中SymbolBigIntES6新增的数据类型:
    • Symbol 代表独一无二的值,最多的用法是用来定义对象的唯一属性名
    • BigInt 可以表示任意大小的整数
javascript
let a = 100;
let b = a;
a = 200;
console.log(b); // 100
javascript
let a = { age: 20 };
let b = a;
b.age = 30;
console.log(a.age); // 30

数据类型的判断:

  1. typeof:能判断所有值类型,函数。不可对null、对象、数组进行精确判断,因为都返回object

    javascript
    console.log(typeof undefined); // undefined
    console.log(typeof 2); // number
    console.log(typeof true); // boolean
    console.log(typeof "str"); // string
    console.log(typeof Symbol("foo")); // symbol
    console.log(typeof 2172141653n); // bigint
    console.log(typeof function () {}); // function
    // 不能判别
    console.log(typeof []); // object
    console.log(typeof {}); // object
    console.log(typeof null); // object
  2. instanceof:能判断对象类型,不能判断基本数据类型,其内部运行机制是判断在其原型链中能否找到该类型的原型

    javascript
    class People {}
    class Student extends People {}
    const s = new Student();
    console.log(s instanceof People); // true
    console.log(s instanceof Student); // true
  3. Object.prototype.toString.call():所有原始数据类型都是能判断的,还有Error对象Date对象等

    javascript
    Object.prototype.toString.call(2); // "[object Number]"
    Object.prototype.toString.call(""); // "[object String]"
    Object.prototype.toString.call(true); // "[object Boolean]"
    Object.prototype.toString.call(undefined); // "[object Undefined]"
    Object.prototype.toString.call(null); // "[object Null]"
    Object.prototype.toString.call(Math); // "[object Math]"
    Object.prototype.toString.call({}); // "[object Object]"
    Object.prototype.toString.call([]); // "[object Array]"
    Object.prototype.toString.call(function () {}); // "[object Function]"
  4. 如何判断变量是否为数组

    javascript
    Array.isArray(arr); // true
    arr.__proto__ === Array.prototype; // true
    arr instanceof Array; // true
    Object.prototype.toString.call(arr); // "[object Array]"

2. 原型和原型链

原型和原型链

  • 原型:每个对象拥有一个原型对象,通过 __proto__ 指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向null,这就是原型链

  • 原型链:由相互关联的原型组成的链状结构就是原型链

    javascript
    function Person(name){
      this.name = name
    }
    let p1 = new Person('Tom')
    p1.__proto__ === Person.prototype  //true
    Person.prototype.__proto__ === Object.prototype  //true
    Object.prototype.__proto__ === null  //true
    Person.__proto__ === Function.prototype //true
    Object.__proto__ === Function.prototype //true

3. 作用域与作用域链

作用域与作用域链

  • 作用域:
    • 规定了如何查找变量,也就是确定当前执行代码对变量的访问权限
    • 作用域决定了代码区块中变量和其他资源的可见性(全局作用域函数作用域块级作用域
  • 作用域链:
    • 当在Javascript中使用一个变量的时候,首先Javascript引擎会尝试在当前作用域下去寻找该变量
    • 如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域
    • 如果在全局作用域里仍然找不到该变量,它就会在全局范围内隐式声明该变量(非严格模式下)或是直接报错
javascript
// 作用域
function showMessage(){
  let message = 'hello'
}
console.log(message) // message is not defined
// 函数showMessage内部创建一个message变量,当我们在全局访问这个变量的时候,系统会报错
// 这就说明我们在全局是无法获取到(闭包除外)函数内部的变量
javascript
// 作用域链
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时找不到,向上一层作用域找,即全局作用域,还是找不到则报错

全局作用域

  • 任何不在函数中或是大括号中声明的变量,都是在全局作用域下
  • 全局作用域下声明的变量可以在程序的任意位置访问
javascript
var message = 'Hello World!'
function showMessage() {
  console.log(message);
}
showMessage() // Hello World!

函数作用域

  • 函数作用域也叫局部作用域,如果一个变量是在函数内部声明的它就在一个函数作用域下面
  • 这些变量只能在函数内部访问,不能在函数以外去访问
javascript
function showMessage() {
  var message = 'Hello World!'
  console.log(message)
}
showMessage() // Hello World!
console.log(message) // message is not defined

块级作用域

  • 在大括号中使用letconst声明的变量存在于块级作用域中
  • 在大括号之外不能访问这些变量
javascript
{
  let message1 = 'Hello World1!'
  var message2 = 'Hello World2!'
}
console.log(message2) //  Hello World2!
console.log(message1) //  message1 is not defined

词法作用域

  • 词法作用域,又叫静态作用域,变量被创建时就确定好了,而非执行阶段确定的
  • 也就是说我们写好代码时它的作用域就确定了,JavaScript遵循的就是词法作用域
javascript
var a = 1
function foo(){
    console.log(a)
}
function bar(){
    var a = 2
    foo()
}
bar() // 1

4. 闭包

闭包

  • JavaScript中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。可以在一个内层函数中访问到其外层函数的作用域
  • 闭包是指那些能够访问自由变量的函数。自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。闭包=函数+函数能够访问的自由变量
javascript
  // showName() 没有自己的局部变量。然而,由于闭包的特性,它可以访问到外部函数的变量
  function init(){
    let name = 'Tom'
    function showName(){
      alert(name) // Tom
    }
    showName()
  }
  init()

使用场景

  • 任何闭包的使用场景都离不开这两点:
    • 创建私有变量
    • 延长变量的生命周期

5. 事件循环

Even Loop

  • 浏览器中的事件循环:
    • JavaScript代码的执行过程中,除了依靠函数调用栈来搞定函数的执行顺序外,还依靠任务队列(task queue)来搞定另外一些代码的执行。整个执行过程,我们称为事件循环过程。一个线程中,事件循环是唯一的,但是任务队列可以拥有多个。任务队列又分为macro-task(宏任务)与micro-task(微任务)
  • macro-task(宏任务):
    • script(整体代码)
    • setTimeout
    • setInterval
    • setImmediate
    • I/O
    • UI render
  • micro-task(微任务):
    • process.nextTick
    • Promise
    • Async/Await(实际就是promise)
    • MutationObserver(html5新特性)
javascript
// demo.js
console.log(1)
async function async1() {
  await async2()
  console.log(2)
}
async function async2() {
  console.log(3)
}
async1()
setTimeout(function() {
  console.log(4)
}, 0)
new Promise(resolve => {
  console.log(5)
  resolve()
})
.then(()=>{
    console.log(6)
})
.then(()=> {
  console.log(7)
})
console.log(8)
// 执行顺序:1-3-5-8-2-6-7-4

分析:

  • 执行代码,输出1
  • 执行async1(),会调用async2(),然后输出3,此时将会保留async1函数的上下文
  • 跳出async1函数。
  • 遇到setTimeout,产生一个宏任务
  • 执行Promise,输出5。遇到then,产生第一个微任务
  • 继续执行代码,输出8
  • 代码逻辑执行完毕(当前宏任务执行完毕),开始执行当前宏任务产生的微任务队列,输出2,接着就是6
  • 产生一个新任务遇到then的微任务
  • 执行产生的微任务,输出7,当前微任务队列执行完毕。执行权回到async1
  • 执行await,实际上会产生一个promise返回,即:
javascript
let p1 = new Promise((resolve,reject){ resolve(undefined)})
  • 最后,执行下一个宏任务,即执行setTimeout,输出4

再分析:

  • 2种情况分析await后面跟的内容:
    1. 如果await后面直接跟的为一个变量,比如:await 1
      • 这种情况的话相当于直接把await后面的代码注册为一个微任务
      • 可以简单理解为promise.then(await下面的代码)
      • 然后跳出async1函数,执行其他代码
      • 当遇到promise函数的时候,会注册promise.then()函数到微任务队列
      • 注意此时微任务队列里面已经存在await后面的微任务,所以这种情况会先执行await后面的代码(console.log(2)),再执行async1函数后面注册的微任务代码( console.log(6), console.log(7))
    2. 如果await后面跟的是一个异步函数的调用,比如上面的代码,将代码改成这样:
javascript
console.log(1)
async function async1() {
  await async2()
  console.log(2)
}
async function async2() {
  console.log(3)
  return Promise.resolve().then(()=>{
    console.log(4)
  })
}
async1()
setTimeout(function() {
  console.log(5)
}, 0)
new Promise(resolve => {
  console.log(6)
  resolve()
})
.then(function() {
  console.log(7)
})
.then(function() {
  console.log(8)
})
console.log(9)
// 1-3-6-9-4-7-8-2-5

总结

  • 此时执行完await并不先把await后面的代码注册到微任务队列中去,
  • 而是执行完await之后,直接跳出async1函数,执行其他代码
  • 然后遇到promise的时候,把promise.then注册为微任务
  • 其他代码执行完毕后,需要回到async1函数去执行剩下的代码
  • 再把await后面的代码注册到微任务队列当中,注意此时微任务队列中是有之前注册的微任务的
  • 所以这种情况会先执行async1函数之外的微任务(p1,p2),然后才执行async1内注册的微任务
  • 可以理解为:这种情况下,await后面的代码会在本轮循环的最后被执行

6. 浏览器的垃圾回收机制(Garbage Collection)

标记清除算法

  • 标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)
  • 过程
    • 垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为 0
    • 然后从各个根对象开始遍历,把不是垃圾的节点改成 1
    • 清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间
    • 最后,把所有内存中对象标记修改为 0,等待下一轮垃圾回收
  • 优点
    • 实现比较简单,打标记也无非打与不打两种情况,这使得一位二进制位( 0 和 1 )就可以为其标记,非常简单
  • 缺点
    • 标记清除算法有一个很大的缺点,就是在清除之后,剩余的对象内存位置是不变的,也会导致空闲内存空间是不连续的,出现了内存碎片并且由于剩余空闲内存不是一整块,它是由不同大小内存组成的内存列表,这就牵扯出了内存分配的问题
      • 三种分配策略
        • First-fit,找到大于等于 size 的块立即返回
        • Best-fit,遍历整个空闲列表,返回大于等于 size 的最小分块
        • Worst-fit,遍历整个空闲列表,找到最大的分块,然后切成两部分,一部分 size 大小,并将该部分返回
      • 内存碎片化,空闲内存块是不连续的,容易出现很多空闲内存块,还可能会出现分配所需内存过大的对象时找不到合适的块
      • 分配速度慢,因为即便是使用 First-fit 策略,其操作仍是一个 O(n) 的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢
    • 针对标记清除算法的缺点引出标记整理(Mark-Compact)算法:
      • 它的标记阶段和标记清除算法没有什么不同,只是标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存

引用计数算法

  • 它把对象是否不再需要简化定义为对象有没有其他对象引用到它。如果没有引用指向该对象(引用计数为 0),对象将被垃圾回收机制回收
  • 过程
    • 当声明了一个变量并且将一个引用类型赋值给该变量的时候这个值的引用次数就为 1
    • 如果同一个值又被赋给另一个变量,那么引用数加 1
    • 如果该变量的值被其他的值覆盖了,则引用次数减 1
    • 当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值没法被访问了,回收空间,垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存

7. http 缓存

缓存

  • 什么是缓存?把一些不需要重新获取的内容再重新获取一次
  • 为什么需要缓存?网络请求相比于CPU的计算和页面渲染是非常非常慢的
  • 哪些资源可以被缓存?静态资源,比如 js,css,img

Cache-Control

  • Response Headers 中控制强制缓存的逻辑,例如:Cache-Control: max-age=3153600(单位是秒)
  • Cache-Control 有哪些值:
    • max-age:缓存最大过期时间
    • no-cache:可以在客户端存储资源,每次都必须去服务端做新鲜度校验,来决定从服务端获取新的资源(200)还是使用客户端缓存(304)
    • no-store:永远都不要在客户端存储资源,永远都去原始服务器去获取资源
Details

图片描述

协商缓存

  • 服务端缓存策略
  • 服务端判断客户端资源,是否和服务端资源一样
  • 一致则返回304,否则返回200和最新的资源
Details

图片描述

资源标识

  • Response Headers中,有两种:Last-ModifiedEtag
    • Last-Modified:资源的最后修改时间

      • 服务端拿到 if-Modified-Since 之后拿这个时间去和服务端资源最后修改时间做比较
      • 如果一致则返回304,不一致(也就是资源已经更新了)就返回200和新的资源及新的Last-Modified
      Details

      图片描述

    • Etag:资源的唯一标识(一个字符串,类似于人类的指纹)

      • 其实EtagLast-Modified一样的
      • 只不过 Etag 是服务端对资源按照一定方式(比如 contenthash)计算出来的唯一标识
      • 就像人类指纹一样,传给客户端之后,客户端再传过来时候,服务端会将其与现在的资源计算出来的唯一标识做比较
      • 一致则返回304,不一致就返回200和新的资源及新的Etag
      Details

      图片描述

比较

  • 优先使用Etag
  • Last-Modified只能精确到秒级
  • 如果资源被重复生成,而内容不变,则Etag更精确
总结

图片描述

8. 数组的方法

Array.prototype.push()

  • push()方法将一个或多个元素添加到数组的末尾,并返回该数组的新长度
javascript
const colors = ['red', 'green'];
const length = colors.push('blue');
console.log(colors) // ['red', 'green', 'blue']
console.log(length) //  3

Array.prototype.pop()

  • pop()方法从数组中删除最后一个元素,并返回该元素的值。此方法会更改数组的长度
javascript
const colors = ['red', 'green','blue']
const item = colors.pop()
console.log(item) //  'blue'
console.log(colors) // ['red', 'green']

Array.prototype.unshift()

  • unshift()方法将一个或多个元素添加到数组的开头,并返回该数组的新长度
javascript
const colors = ['red', 'green','blue']
const item = colors.shift()
console.log(item) //  'red'
console.log(colors) // ['green', 'blue']

Array.prototype.shift()

  • shift()方法从数组中删除第一个元素,并返回该元素的值。此方法更改数组的长度
javascript
const colors = ['red','green','blue']
const item = colors.shift()
console.log(item) //  red
console.log(colors) // ['red', 'green', 'blue']

Array.prototype.splice()

  • splice()方法通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容
  • 此方法会改变原数组
javascript
splice(start)
splice(start, deleteCount)
splice(start, deleteCount, item1)
splice(start, deleteCount, item1, item2, itemN)
 // 删除
const colors = ['red','green','blue']
const colors2 = colors.splice(0,1)
console.log(colors)  // ['green', 'blue']
console.log(colors2) // ['red']
 // 替换现有元素
const colors = ['red','green','blue']
const colors2 = colors.splice(0,1,'yellow')
console.log(colors)  // ['yellow', 'green', 'blue']
console.log(colors2) // ['red']
 // 原地添加新的元素
const colors = ['red','green','blue']
const colors2 = colors.splice(0,0,'yellow')
console.log(colors) // ['yellow', 'red', 'green', 'blue']
console.log(colors2) // [] // 如果没有删除元素,则返回空数组

Array.prototype.slice()

  • slice()方法返回一个新的数组对象
  • 这一对象是一个由beginend决定的原数组的浅拷贝包括begin,不包括end原始数组不会被改变
javascript
slice()
slice(start)
slice(start, end)
const colors = ['red','green','blue']
const colors2 = colors.slice()
const colors3 = colors.slice(1,5)
console.log(colors) //   ['red', 'green', 'blue']
console.log(colors2) //  ['red', 'green', 'blue']
console.log(colors3) //  ['green', 'blue']

Array.prototype.concat()

  • concat()方法用于合并两个或多个数组,此方法不会更改现有数组,而是返回一个新数组
javascript
const colors = ['red','green','blue']
const colors2 = ['yellow']
const colors3 = colors.concat(colors2)
console.log(colors) //   ['red', 'green', 'blue']
console.log(colors2) //  ['yellow']
console.log(colors3) //  ['red', 'green', 'blue', 'yellow']

Array.prototype.reverse()

  • reverse()方法将数组中元素的位置颠倒,并返回该数组。数组的第一个元素会变成最后一个,数组的最后一个元素变成第一个
  • 该方法会改变原数组
javascript
const colors = ['red','green','blue']
const colors2 = colors.reverse()
console.log(colors) //   ['blue', 'green', 'red']
console.log(colors2) //  ['blue', 'green', 'red']

Array.prototype.sort()

  • sort()方法用原地算法,原地算法对数组的元素进行排序,并返回数组
  • 默认排序顺序是在将元素转换为字符串,然后比较它们的UTF-16代码单元值序列时构建的
  • 由于它取决于具体实现,因此无法保证排序的时间和空间复杂性
javascript
sort()
// 用来指定按某种顺序进行排列的函数。
// 如果省略,元素按照转换为的字符串的各个字符的 Unicode 位点进行排序
sort(compareFn)
compareFn(a,b)
const colors = ['red','green','blue']
const colors2 = colors.sort()
console.log(colors) //   ['blue', 'green', 'red']
console.log(colors2) //  ['blue', 'green', 'red']

Array.prototype.join()

  • join()方法将一个数组(或一个类数组对象)的所有元素连接成一个字符串并返回这个字符串,用逗号或指定的分隔符字符串分隔
  • 如果数组只有一个元素,那么将返回该元素而不使用分隔符
javascript
const colors = ['red','green','blue']
const colors2 = colors.join()
const colors3 = colors.join("")
console.log(colors) // ['red', 'green', 'blue']
console.log(colors2) // 'red,green,blue'
console.log(colors3) // 'redgreenblue'

Array.prototype.filter()

  • filter()方法创建给定数组一部分的浅拷贝,其包含通过所提供函数实现的测试的所有元素
javascript
filter(callbackFn)
filter(callbackFn, thisArg)
callbackFn(element,index,array)
const colors = ['red','green','blue']
const colors2 = colors.filter((item)=>item.length >= 4)
console.log(colors)  //   ['red', 'green', 'blue']]
console.log(colors2) //  ['green', 'blue']

Array.prototype.forEach()

  • forEach()方法对数组的每个元素执行一次给定的函数,
  • forEach对原数组进行修改,返回值为undefined
javascript
forEach(callbackFn)
forEach(callbackFn, thisArg)
callbackFn(element,index,array)
const colors = ['red','green','blue']
const colors2 = colors.forEach((item,index)=>colors[index] = item +'!'  )
console.log(colors)  // ['red!', 'green!', 'blue!']
console.log(colors2) // undefined

Array.prototype.map()

  • map()方法创建一个新数组,这个新数组由原数组中的每个元素都调用一次提供的函数后的返回值组成
  • 返回一个新的数组
javascript
map(callbackFn)
map(callbackFn, thisArg)
callbackFn(currentValue,index,array)
const colors = ['red','green','blue']
const colors2 = colors.map((item)=> item +'!'  )
console.log(colors)  // ['red', 'green', 'blue']
console.log(colors2) // ['red!', 'green!', 'blue!']

Array.prototype.reduce()

  • reduce()方法对数组中的每个元素按序执行一个由您提供的reducer函数
  • 每一次运行reducer会将先前元素的计算结果作为参数传入,最后将其结果汇总为单个返回值
javascript
reduce(callbackFn)
reduce(callbackFn, initialValue)
callbackFn(previousValue,currentValue,index,array)
const arr = [1,2,3]
const arr2 = arr.reduce((pre,cur)=> pre + cur )
console.log(arr) // [1, 2, 3] 
console.log(arr2) // 6

Array.prototype.some()

  • reduce()方法测试数组中是不是至少有1个元素通过了被提供的函数测试
  • 它返回的是一个Boolean类型的值
  • 如果用一个空数组进行测试,在任何情况下它返回的都是False
javascript
reduce(callbackFn)
reduce(callbackFn, thisArg)
callbackFn(element,index,array)
const arr = [1,2,3,4,5]
const arr2 = arr.some((item)=> item % 2===0 )
console.log(arr) // [1, 2, 3, 4, 5] 
console.log(arr2) // true

Array.prototype.every()

  • every()方法测试一个数组内的所有元素是否都能通过指定函数的测试
  • 它返回的是一个Boolean类型的值
javascript
reduce(callbackFn)
reduce(callbackFn, thisArg)
callbackFn(element,index,array)
const arr = [1,2,3,4,5]
const arr2 = arr.every((item)=> item % 2===0 )
console.log(arr) // [1, 2, 3, 4, 5] 
console.log(arr2) // false

Array.prototype.indexOf()

  • indexOf()方法返回在数组中可以找到给定元素的第一个索引
  • 如果不存在,则返回-1
javascript
const colors = ['red','green','blue','red']
console.log(colors.indexOf('red')) // 0
console.log(colors.indexOf('red',1)) // 3
console.log(colors.indexOf('yellow')) // -1

Array.prototype.lastIndexOf()

  • lastIndexOf()方法返回指定元素(也即有效的JavaScript值或变量)在数组中的最后一个的索引
  • 如果不存在则返回 -1。从数组的后面向前查找,从fromIndex处开始
javascript
const colors = ['red','green','blue','red']
console.log(colors.lastIndexOf('red')) // 3
console.log(colors.indexOf('green')) // 1

Array.prototype.includes()

  • includes()方法用来判断一个数组是否包含一个指定的值
  • 如果包含则返回true,否则返回false
javascript
const colors = ['red','green','blue']
console.log(colors.includes('red')) // true
console.log(colors.includes('yellow')) // false
console.log(colors.includes()) // false

Array.prototype.find()

  • find()方法返回数组中满足提供的测试函数的第一个元素的值,否则返回 undefined
javascript
const arr = [1,2,3,4,5]
console.log(arr.find((item)=> item > 3)) // 4
console.log(arr.find((item)=> item > 5)) // undefined

Array.prototype.fill()

  • fill()方法用一个固定值填充一个数组中从起始索引(默认为0)到终止索引(默认为array.length)内的全部元素
  • 它返回修改后的数组
javascript
fill(value)
fill(value, start)
fill(value, start, end)
const arr = [1,2,3,4,5]
const arr1 = arr.fill(6)
const arr2 = arr.fill(7, 1)
const arr3 = arr.fill(8, 1, 3)
console.log(arr1) //  [6, 6, 6, 6, 6]
console.log(arr2) //  [6, 7, 7, 7, 7]
console.log(arr3) //  [6, 8, 8, 7, 7]

Array.prototype.flat()

  • flat()方法创建一个新的数组,并根据指定深度递归地将所有子数组元素拼接到新的数组中
javascript
const arr1 = [0, 1, 2, [3, 4], 5]
console.log(arr1.flat()) // [0, 1, 2, 3, 4, 5]

Array.from()

  • Array.from()静态方法从可迭代或类数组对象创建一个新的浅拷贝的数组实例
javascript
const arrayLike = {length:5}
const arr = Array.from(arrayLike)
console.log(Array.isArray(arr))  // true

Array.of()

  • Array.of()方法通过可变数量的参数创建一个新的Array实例,而不考虑参数的数量或类型
  • Array.of()Array()构造函数之间的区别在于对单个参数的处理
javascript
Array.of(7)// [7]
Array(7) // [empty × 7]
Array.of(1, 2, 3); // [1, 2, 3]
Array(1, 2, 3);    // [1, 2, 3]

9. 对象的方法

Object.assign()

  • Object.assign()方法将所有可枚举(Object.propertyIsEnumerable() 返回true)的自有(Object.hasOwnProperty()返回true)属性从一个或多个源对象复制到目标对象
  • 返回修改后的对象
javascript
Object.assign(target, ...sources)
const obj1 = {a:1,b:2}
const obj2 = {b:3,c:4}
const obj3 = Object.assign(obj1,obj2)
console.log(obj1) // { a: 1, b: 3, c: 4 }
console.log(obj2) // { b: 3, c: 4 }
console.log(obj3) // { a: 1, b: 3, c: 4 }
console.log(obj1 === obj3) // true

Object.create()

  • Object.create()方法用于创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype
  • 一个新对象,带着指定的原型对象及其属性
javascript
Object.create(proto)
Object.create(proto, propertiesObject)
const person = {
  isHuman: false,
  printIntroduction() {
    console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`)
    }
}
const p = Object.create(person)
p.name = 'Joy'
p.isHuman = true
p.printIntroduction() // "My name is Joy. Am I human? true"

Object.entries()

  • Object.entries()方法返回一个给定对象自身可枚举属性的键值对数组
javascript
const obj1 = {a:1,b:2}
const entries = Object.entries(obj1)
console.log(entries) // [ ['a', 1], ['b', 2]]

Object.hasOwn()

  • 如果指定的对象自身有指定的属性,则静态方法Object.hasOwn()返回true
  • 如果属性是继承的或者不存在,该方法返回false
  • 备注:Object.hasOwn()旨在取代Object.hasOwnProperty()
javascript
hasOwn(instance, prop)
const obj1 = {a:1,b:2}
console.log(Object.hasOwn(obj1,'a')) // true
console.log(Object.hasOwn(obj1,'toString')) // false
console.log(Object.hasOwn(obj1,'c')) // false

Object.is()

  • Object.is()方法判断两个值是否为同一个值
  • Object.is()方法判断两个值是否为同一个值,如果满足以下任意条件则两个值相等:
    • 都是undefined
    • 都是null
    • 都是true或都是false
    • 都是相同长度、相同字符、按相同顺序排列的字符串
    • 都是相同对象(意味着都是同一个对象的值引用)
    • 都是数字且
      • 都是+0
      • 都是-0
      • 都是NaN
      • 都是同一个值,非零且都不是NaN
javascript
Object.is(1, 1)                  // true
Object.is('abc', 'abc')          // true
Object.is('abc', 'edf')          // false
Object.is(null, null)            // true
Object.is(undefined, undefined)  // true
Object.is(window, window)        // true
Object.is([], [])                // false
const foo = { a: 1 }        
const bar = { a: 1 }
Object.is(foo, foo)              // true
Object.is(foo, bar)              // false
Object.is(0, -0)                 // false
Object.is(+0, -0)                // false
Object.is(-0, -0)                // true
Object.is(0n, -0n)               // true
Object.is(NaN, 0/0)              // true
Object.is(NaN, Number.NaN)       // true

10. JS 中的类型转换机制

概述

  • JS中有六种简单数据类型:undefinednullbooleanstringnumbersymbolobject
  • 但是我们在声明的时候只有一种数据类型,只有到运行期间才会确定当前类型
    javascript
    let x = y ? 1 : 2
  • 上面代码中,x的值在编译阶段是无法获取的,只有等到程序运行时才能知道
  • 虽然变量的数据类型是不确定的,但是各种运算符对数据类型是有要求的,如果运算子的类型与预期不符合,就会触发类型转换机制
  • 常见的类型转换有:
    • 强制转换(显示转换)
    • 自动转换(隐式转换)

显示转换

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

Number()

  • 将任意类型的值转化为数值
    原始值转换结果
    UndefinedNaN
    Null0
    Boolean1 、0
    String根据语法和转换规则来转换
    SymbolThrow a TypeError exception
    Object先调用toPrimitive,再调用toNumber
javascript
  // test
  Number(123) // 123
  Number('123') // 123
  Number('123abc') // NaN
  Number('') // 0
  Number(true) // 1
  Number(false) // 0
  Number(undefined) // NaN
  Number(null) // 0
  Number({}) // NaN
  Number([1,2,3]) // NaN
  Number([1]) // 1
  Number([]) // 0
  • Number转换的时候是很严格的,只要有一个字符无法转成数值,整个字符串就会被转为NaN

parseInt()

  • parseInt函数逐个解析字符,遇到不能转换的字符就停下来
javascript
parseInt('123abc123') // 123

Sting()

  • 可以将任意类型的值转化成字符串
    原始值转换结果
    Undefined'undefined'
    Null'null'
    Boolean'true'、'false'
    Numberstring
    Stringstring
    Symbolstring
    Object先调用toPrimitive,再调用toNumber
javascript
// test 
String(undefined) // 'undefined'
String(null) // 'null'
String(true) // 'true'
String(false) // 'false'
String(1) // '1'
String('abc') // 'abc'
String(Symbol('abc')) // 'Symbol(abc)'
String({}) // '[object Object]'

Boolean()

  • 可以将任意类型的值转为布尔值
    数据类型转换为true的值转换为false的值
    Booleantruefalse
    String非空字符串""
    Number非零数值(包括无穷值)0、NaN
    Object任意对象null
    UndefinedN/A(不存在)undefined
    NullN/A(不存在)null
javascript
// test
Boolean(undefined) // false
Boolean(null) // false
Boolean(0) // false
Boolean(NaN) // false
Boolean('') // false
Boolean({}) // true
Boolean([]) // true
Boolean(new Boolean(false)) // true

隐式转换

  • 比较运算(==!=><)、ifwhile需要布尔值地方
  • 算术运算(+-*/%
  • 除了上面的场景,还要求运算符两边的操作数不是同一类型

自动转换为布尔值

  • 在需要布尔值的地方,就会将非布尔值的参数自动转为布尔值,系统内部会调用Boolean函数
  • 可以得出个小结:
    • undefined
    • null
    • false
    • +0
    • -0
    • NaN
    • ""
  • 除了上面几种会被转化成false,其他都换被转化成true

自动转换成字符串

  • 先将复合类型的值转为原始类型的值,再将原始类型的值转为字符串
  • 常发生在+运算中,一旦存在字符串,则会进行字符串拼接操作
javascript
'1' + 1 // '11'
'1' + true // "1true"
'1' + false // "1false"
'1' + {} // "1[object Object]"
'1' + [] // "1"
'1' + function (){} // "1function (){}"
'1' + undefined // "1undefined"
'1' + null // "1null"

自动转换成数值

  • 除了+有可能把运算子转为字符串,其他运算符都会把运算子自动转成数值
javascript
'1' - '2' // -1
'1' * '2' // 2
true - 1  // 0
false - 1 // -1
'1' - 1   // 0
'1' * []    // 0
false / '1' // 0
'abc' - 1   // NaN
null + 1 // 1
undefined + 1 // NaN
// null转为数值后值为0 ,undefined转为数值后值为NaN

11. == 和 ===

等于操作符

  • 等于操作符用两个等于号(==)表示,如果操作数相等,则会返回true
  • 等于操作符(==)在比较中会先进行类型转换,再确定操作数是否相等
  • 遵循以下规则:
    • 如果任一操作数是布尔值,则将其转换为数值再比较是否相等
    javascript
      let result = (true == 1) // true
    • 如果一个操作数是字符串,另一个操作数是数值,则尝试将字符串转换为数值,再比较是否相等
    javascript
      let result = ("55" == 55) // true
    • 如果一个操作数是对象,另一个操作数不是,则调用对象的valueOf()方法取得其原始值,再根据前面的规则进行比较
    javascript
      let obj = {valueOf:function(){return 1}}
      let result = (obj == 1)  // true
    • 如果两个操作数都是对象,则比较它们是不是同一个对象,如果两个操作数都指向同一个对象,则相等操作符返回true
    javascript
      let obj1 = {name:"xxx"}
      let obj2 = {name:"xxx"}
      let result = (obj1 == obj2 ) // false
    • nullundefined相等
    javascript
      let result = (null == undefined )  // true
    • 如果有任一操作数是NaN,则相等操作符返回false
    javascript
      let result = (NaN == NaN )  // false

小结

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

全等操作符

  • 全等操作符由 3 个等于号(===)表示,只有两个操作数在不转换的前提下相等才返回true。即类型相同,值也需相同
javascript
let result1 = ("55" === 55) // false,不相等,因为数据类型不同
let result2 = (55 === 55) // true,相等,因为数据类型相同值也相同
  • undefinednull与自身严格相等
javascript
let result1 = (null === null)  //true
let result2 = (undefined === undefined)  //true

区别

  • 相等操作符(==)会做类型转换,再进行值的比较
  • 全等运算符不会做类型转换
    javascript
    let result1 = ("55" === 55) // false,不相等,因为数据类型不同
    let result2 = (55 === 55) // true,相等,因为数据类型相同值也相同
  • nullundefined比较,相等操作符(==)为true,全等为false
    javascript
    let result1 = (null == undefined ) // true
    let result2 = (null === undefined) // false

总结

  • 相等运算符隐藏的类型转换,会带来一些违反直觉的结果
    javascript
    '' == '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的情况的时候,我们一般使用相等操作符==
    javascript
    const obj = {}
    if(obj.x == null){
      console.log("1")  //执行
    }
    // 或者
    if(obj.x === null || obj.x === undefined) {
      // ...
    }
  • 使用相等操作符(==)的写法明显更加简洁了,所以除了在比较对象属性为null或者undefined的情况下,我们可以使用相等操作符(==),其他情况建议一律使用全等操作符(===

12. this

this

  • this关键字是函数运行时自动生成的一个内部对象,只能在函数内部使用,总指向调用它的对象
javascript
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一旦被确定了,就不可以再更改
javascript
var a = 10
var obj = {
  a: 20
}
function fn() {
  this = obj // 修改this,运行后会报错
  console.log(this.a)
}
fn()

绑定规则

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

默认绑定

  • 全局环境中定义person函数,内部使用this关键字
javascript
var name = 'Tom'
function person() {
    console.log(this.name)
}
person() // Tom
  • 上述代码输出Tom,原因是调用函数的对象在浏览器中位window,因此this指向window
  • 注意:
    • 严格模式下,不能将全局对象用于默认绑定,this会绑定到undefined
    • 只有函数运行在非严格模式下,默认绑定才能绑定到全局对象

隐式绑定

  • 函数还可以作为某个对象的方法调用,这时this就指这个上级对象
javascript
function showName2() {
  console.log(this.name)
}
var obj = {}
obj.name = 'Tom'
obj.showName1 = showName2
obj.showName1() // Tom
  • 下面的代码this的上一级对象为foofoo内部并没有name变量的定义,所以输出undefined
javascript
var obj = {
  name:"Tom",
  foo:{
    fn:function(){
      console.log(this.name)  // undefined
    }
  }
}
obj.foo.fn()
  • 下面的代码this指向的是window
  • this永远指向的是最后调用它的对象,虽然fn是对象foo的方法,但是fn赋值给bar时候并没有执行,所以最终指向window
javascript
var obj = {
  age:20,
  foo:{
    age:21,
    fn:function(){
        console.log(this.age) // undefined
        console.log(this)     // window
      }
    }
  }
var bar = obj.foo.fn
bar()

new绑定

  • 通过构建函数new关键字生成一个实例对象,此时this指向这个实例对象
    • new过程遇到return一个对象,此时this指向为返回的对象(return {})
    • 如果返回一个简单类型的时候,则this指向实例对象(return 1)
    • 如果返回是null虽然也是对象,但是此时new仍然指向实例对象(return null)
javascript
function foo() {
 this.name = 'Tom';
  }
var p = new foo();
p.name // Tom

显示绑定

  • apply()call()bind()是函数的一个方法,作用是改变函数的调用对象
  • 它的第一个参数就表示改变后的调用这个函数的对象。因此这时this指的就是这第一个参数
javascript
var name = '张三'
function showName() {
 console.log(this.name)
}
var obj = {}
obj.name = '李四'
obj.fn = showName
obj.fn.call(obj) // 李四
obj.fn.call(window) // 张三

箭头函数

  • 箭头函数不能作为构建函数
javascript
const obj = {
  sayThis: () => {
    console.log(this)
  }
}
obj.sayThis() // window 
// 因为 JavaScript 没有块作用域,
// 所以在定义 sayThis 的时候,里面的 this 就绑到 window 上去了
const globalSay = obj.sayThis;
globalSay()  // window 浏览器中的global对象
  • 虽然箭头函数的this能够在编译的时候就确定了this的指向,但也需要注意一些潜在的坑
javascript
// 绑定事件监听
const button = document.getElementById('id')
button.addEventListener('click', ()=> {
    console.log(this === window) // true
    this.innerHTML = 'clicked button'
    // 我们其实是想要this为点击的button,但此时this指向了window
})
  • 包括在原型上添加方法时候,此时this指向window
javascript
function Animals(name){
  this.name = name
  Animals.prototype.sayName = () => {
    console.log(this === window) //true
    console.log(this.name)  // Jerry
  }
}
const dog = new Animals('Jerry')
dog.sayName()

13. DOM 常见的操作有哪些

DOM

  • 文档对象模型(DOM)是HTMLXML文档的编程接口
  • 它提供了对文档的结构化的表述,并定义了一种方式可以使从程序中对该结构进行访问,从而改变文档的结构样式和内容
  • 任何HTMLXML文档都可以用DOM表示为一个由节点构成的层级结构

操作

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

创建节点

  • 创建新元素,接受一个参数,即要创建元素的标签名
javascript
// createElement
const divEle = document.createElement("div")
// createTextNode
const textEle = document.createTextNode("content")
// createAttribute
const dataAttribute = document.createAttribute('custom')

获取节点

  • 创建新元素,接受一个参数,即要创建元素的标签名
javascript
document.getElementById('id属性值')  // 返回拥有指定id的对象的引用
document.getElementsByClassName('class属性值')  // 返回拥有指定class的对象集合
document.getElementsByTagName('标签名')  // 返回拥有指定标签名的对象集合
document.getElementsByName('name属性值')  // 返回拥有指定名称的对象结合
document.documentElement  // 获取页面中的HTML标签
document.body // 获取页面中的BODY标签
document.all[''] //   获取页面中的所有元素节点的对象集合型
document/element.querySelector('CSS选择器')   //  仅返回第一个匹配的元素
document/element.querySelectorAll('CSS选择器')  //   返回所有匹配的元素

更新节点

  • innerHTML:不但可以修改一个DOM节点的文本内容,还可以直接通过HTML片段修改DOM节点内部的子树
javascript
// 获取<p id="p">...</p >
var p = document.getElementById('p');
// 设置文本为abc:
p.innerHTML = 'ABC'; // <p id="p">ABC</p >
// 设置HTML:
p.innerHTML = 'ABC <span style="color:red">RED</span> XYZ'
// <p>...</p >的内部结构已修改
  • innerText、textContent:自动对字符串进行HTML编码,保证无法设置任何HTML标签
  • 两者的区别在于读取属性时,innerText不返回隐藏元素的文本,而textContent返回所有文本
javascript
// 获取<p id="p-id">...</p >
var p = document.getElementById('p-id');
// 设置文本:
p.innerText = '<script>alert("Hi")</script>';
// HTML被自动编码,无法设置一个<script>节点:
// <p id="p-id">&lt;script&gt;alert("Hi")&lt;/script&gt;</p >

添加节点

  • appendChild:把一个子节点添加到父节点的最后一个子节点
javascript
// HTML结构 
<p id="js">JavaScript</p >
<div id="list">
  <p id="java">Java</p >
  <p id="python">Python</p >
  <p id="go">Go</p >
</div>
// js
const php = document.getElementById('php')
php.innerHTML = "Php"
const list = document.getElementById('list')
list.appendChild(php)
  • 动态添加
javascript
const list = document.getElementById('list')
const newNode = document.createElement('n')
newNode.id = 'newNode'
newNode.innerText = 'NewNode'
list.appendChild(newNode)

setAttribute

  • setAttribute:在指定元素中添加一个属性节点,如果元素中已有该属性改变属性值
javascript
const div = document.getElementById('id')
div.setAttribute('class', 'white')//第一个参数属性名,第二个参数属性值

删除节点

  • 删除一个节点,首先要获得该节点本身以及它的父节点
  • 调用父节点的removeChild把自己删掉
  • 删除后的节点虽然不在文档树中了,但其实它还在内存中,可以随时再次被添加到别的位置
javascript
// 拿到待删除节点:
const remove = document.getElementById('remove')
// 拿到父节点:
const parent = remove.parentElement
// 删除:
const removed = parent.removeChild(remove)
removed === remove // true

14. BOM 常见的操作有哪些

BOM

  • BOM (Browser Object Model)浏览器对象模型,提供了独立于内容与浏览器窗口进行交互的对象
  • 其作用就是跟浏览器做一些交互效果,比如如何进行页面的后退,前进,刷新,浏览器的窗口发生变化,滚动条的滚动
  • 以及获取客户的一些信息如:浏览器品牌版本,屏幕分辨率

Window

  • BOM的核心对象是window,它表示浏览器的一个实例在浏览器中
  • window对象有双重角色,即是浏览器窗口的一个接口,又是全局对象
  • 因此所有在全局作用域中声明的变量、函数都会变成window对象的属性和方法
javascript
var name = 'Tom'
function showName(){
  console.log(this.name)
}
console.log(window.name) // Tom
showName() // Tom
window.showName() // Tom
  • 关于窗口控制方法如下:
    • moveBy(x,y):从当前位置水平移动窗体x个像素,垂直移动窗体y个像素,x为负数,将向左移动窗体,y为负数,将向上移动窗体
    • moveTo(x,y):移动窗体左上角到相对于屏幕左上角的(x,y)点
    • resizeBy(w,h):相对窗体当前的大小,宽度调整w个像素,高度调整h个像素。如果参数为负值,将缩小窗体,反之扩大窗体
    • resizeTo(w,h):把窗体宽度调整为w个像素,高度调整为h个像素
    • scrollTo(x,y):如果有滚动条,将横向滚动条移动到相对于窗体宽度为x个像素的位置,将纵向滚动条移动到相对于窗体高度为y个像素的位置
    • scrollBy(x,y): 如果有滚动条,将横向滚动条向左移动x个像素,将纵向滚动条向下移动y个像素

location

属性名说明
hashurl中#后面的字符,没有则返回空串
host服务器名称和端口号
hostname域名,不带端口号
href完整url
pathname服务器下面的文件路径
porturl的端口号,没有则为空
protocol使用的协议
searchurl的查询字符串,通常为?后面的内容
  • 除了hash之外,只要修改location的一个属性,就会导致页面重新加载新URL
  • location.reload(),此方法可以重新刷新当前页面。这个方法会根据最有效的方式刷新页面
  • 如果页面自上一次请求以来没有改变过,页面就会从浏览器缓存中重新加载
  • 如果要强制从服务器中重新加载,传递一个参数true即可

navigator

  • navigator对象主要用来获取浏览器的属性,区分浏览器类型。属性较多,且兼容性比较复杂
  • 下表列出了navigator对象接口定义的属性和方法:
Details

picpic

screen

  • 保存的纯粹是客户端能力信息,也就是浏览器窗口外面的客户端显示器的信息,比如像素宽度和像素高度
Details

pic

history

  • history对象主要用来操作浏览器URL的历史记录,可以通过参数向前,向后,或者向指定URL跳转
  • 常用的属性如下:
    • history.go():接收一个整数数字或者字符串参数,向最近的一个记录中包含指定字符串的页面跳转
    • history.forward():向前跳转一个页面
    • history.back():向后跳转一个页面
    • history.length():获取历史记录数

15. JS 本地存储的方式有哪些

JavaScript 本地缓存:

  • Cookie
  • sessionStorage
  • localStorage
  • indexedDB

Cookie

  • Cookie,类型为「小型文本文件」,指某些网站为了辨别用户身份而储存在用户本地终端上的数据。是为了解决HTTP无状态导致的问题
  • 作为一段一般不超过4KB的小型文本数据,它由一个名称(Name)、一个值(Value)和其它几个用于控制Cookie有效期、安全性、使用范围的可选属性组成
  • 但是Cookie在每次请求中都会被发送,如果不使用HTTPS并对其加密,其保存的信息很容易被窃取,导致安全风险。举个例子,在一些使用 Cookie保持登录态的网站上,如果 Cookie被窃取,他人很容易利用你的Cookie来假扮成你登录网站
  • Cookie属性
    • Expires用于设置Cookie的过期时间
    • Max-Age用于设置在Cookie失效之前需要经过的秒数(优先级比Expires高)
    • Domain指定了Cookie可以送达的主机名
    • Path指定了一个 URL路径,这个路径必须出现在要请求的资源的路径中才可以发送Cookie首部
    • 标记为SecureCookie只应通过被HTTPS协议加密过的请求发送给服务端
shell
Expires=Wed, 12 Apr 2023 07:28:00 GMT
Max-Age=604800
Path=/docs   # /docs/Web/ 下的资源会带 Cookie 首部
  • Cookie使用
    • Cookie的修改,首先要确定domainpath属性都是相同的才可以,其中有一个不同得时候都会创建出一个新的Cookie
    • 最后Cookie的删除,最常用的方法就是给Cookie设置一个过期的事件,这样Cookie过期后会被浏览器删除
shell
document.cookie='key=value'
Set-cookie:name=aa; domain=aa.net; path=/  # 服务端设置
document.cookie=name=bb; domain=aa.net; path=/  # 客户端设置

localStorage

  • HTML5新方法,IE8及以上浏览器都兼容
  • localStorage的特点:
    • 生命周期:持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的
    • 存储的信息在同一域中是共享的
    • 当本页操作(新增、修改、删除)了localStorage的时候,本页面不会触发storage事件,但是别的页面会触发storage事件
    • 大小:5M(跟浏览器厂商有关系)
    • localStorage本质上是对字符串的读取,如果存储内容多的话会消耗内存空间,会导致页面变卡
    • 受同源策略的限制
  • localStorage的使用:
    javascript
    localStorage.setItem('username','hef') // 设置
    localStorage.getItem('username')       // 获取
    localStorage.key(0)                    // 获取键名
    localStorage.removeItem('username')    // 删除
    localStorage.clear()                   // 一次性清除所有存储
  • localStorage 也不是完美的,它有两个缺点:
    • 无法像Cookie一样设置过期时间
    • 只能存入字符串,无法直接存对象
    javascript
    localStorage.setItem('key', {name: 'value'})
    console.log(localStorage.getItem('key')) // '[object, Object]'

sessionStorage

  • sessionStoragelocalStorage使用方法基本一致
  • 唯一不同的是生命周期,一旦页面(会话)关闭,sessionStorage 将会删除数据

indexedDB

  • indexedDB是一种低级API,用于客户端存储大量结构化数据(包括, 文件/ blobs)。该API使用索引来实现对该数据的高性能搜索
  • 虽然Web Storage对于存储较少量的数据很有用,但对于存储更大量的结构化数据来说,这种方法不太有用。IndexedDB提供了一个解决方案
  • 优点:
    • 储存量理论上没有上限
    • 所有操作都是异步的,相比LocalStorage同步操作性能更高,尤其是数据量较大时
    • 原生支持储存JS的对象
    • 是个正经的数据库,意味着数据库能干的事它都能干
  • 缺点:
    • 操作非常繁琐
    • 本身有一定门槛
  • 基本使用步骤:
    • 打开数据库并且开始一个事务
    • 创建一个object store
    • 构建一个请求来执行一些数据库操作,像增加或提取数据等
    • 通过监听正确类型的DOM事件以等待操作完成
    • 操作结果上进行一些操作(可以在request对象中找到)

区别

  • 关于CookiesessionStoragelocalStorage三者的区别主要如下:
  • 存储大小:cookie数据大小不能超过4k,sessionStoragelocalStorage虽然也有存储大小的限制,但比Cookie大得多,可以达到5M或更大
  • 有效时间:localStorage存储持久数据,浏览器关闭后数据不丢失除非主动删除数据;sessionStorage数据在当前浏览器窗口关闭后自动删除;Cookie设置的Cookie过期时间之前一直有效,即使窗口或浏览器关闭
  • 数据与服务器之间的交互方式,Cookie的数据会自动的传递到服务器,服务器端也可以写Cookie到客户端;sessionStoragelocalStorage不会自动把数据发给服务器,仅在本地保存

应用场景

  • 针对不对场景的使用选择:
    • 标记用户与跟踪用户行为的情况,推荐使用Cookie
    • 适合长期保存在本地的数据(令牌),推荐使用localStorage
    • 敏感账号一次性登录,推荐使用sessionStorage
    • 存储大量数据的情况、在线文档(富文本编辑器)保存编辑历史的情况,推荐使用indexedDB

16. JS 中的事件模型

事件与事件流

  • Javascript中的事件,可以理解就是在HTML文档或者浏览器中发生的一种交互操作,使得网页具备互动性,常见的有加载事件、鼠标事件、自定义事件等
  • 由于DOM是一个树结构,如果在父子节点绑定事件时候,当触发子节点的时候,就存在一个顺序问题,这就涉及到了事件流的概念
  • 事件流都会经历三个阶段:
    • 事件捕获阶段(capture phase)
    • 处于目标阶段(target phase)
    • 事件冒泡阶段(bubbling phase) pic
  • 事件冒泡是一种从下往上的传播方式,由最具体的元素(触发节点)然后逐渐向上传播到最不具体的那个节点,也就是DOM中最高层的父节点
    javascript
    <!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和它的父元素,加入点击事件
    javascript
    let 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')
    }
  • 点击按钮,输出如下:
    javascript
    1.button
    2.body
    3.document
    4.window
  • 点击事件首先在button元素上发生,然后逐级向上传播
  • 事件捕获与事件冒泡相反,事件最开始由不太具体的节点最早接受事件, 而最具体的节点(触发节点)最后接受事件

事件模型

  • 事件模型可以分为三种:
    • 原始事件模型(DOM0级)
    • 标准事件模型(DOM2级)
    • IE事件模型(基本不用)

原始事件模型(DOM0级)

  • 事件绑定监听函数比较简单, 有两种方式:
    • HTML代码中直接绑定
      javascript
      <input type="button" onclick="fn()">
    • 通过JS代码绑定
      javascript
      let btn = document.getElementById('.btn');
      btn.onclick = fn
  • 特性:
    • 绑定速度快
      • DOM0级事件具有很好的跨浏览器优势,会以最快的速度绑定,但由于绑定速度太快,可能页面还未完全加载出来,以至于事件可能无法正常运行
        • 只支持冒泡,不支持捕获
        • 同一个类型的事件只能绑定一次
        javascript
        <input type="button" id="btn" onclick="fn1()">
        var btn = document.getElementById('.btn')
        btn.onclick = fn2
        • 如上,当希望为同一个元素绑定多个同类型事件的时候(上面的这个btn元素绑定2个点击事件),是不被允许的,后绑定的事件会覆盖之前的事件,删除 DOM0级事件处理程序只要将对应事件属性置为null即可
        javascript
        btn.onclick = null

标准事件模型

  • 在该事件模型中,一次事件共有三个过程:
    • 事件捕获阶段:事件从document一直向下传播到目标元素, 依次检查经过的节点是否绑定了事件监听函数,如果有则执行
    • 事件处理阶段:事件到达目标元素, 触发目标元素的监听函数
    • 事件冒泡阶段:事件从目标元素冒泡到document,依次检查经过的节点是否绑定了事件监听函数,如果有则执行
  • 事件绑定监听函数的方式如下:
    javascript
    addEventListener(eventType, handler, useCapture)
  • 事件移除监听函数的方式如下:
    javascript
    addEventListener(eventType, handler, useCapture)
  • 参数如下:
    • eventType指定事件类型(不要加on)
    • handler是事件处理函数
    • useCapture是一个boolean用于指定是否在捕获阶段进行处理,一般设置为falseIE浏览器保持一致
    • 举个例子:
      javascript
      var btn = document.getElementById('.btn')
      btn.addEventListener(‘click’, showMessage, false)
      btn.removeEventListener(‘click’, showMessage, false)
  • 特性:
    • 可以在一个DOM元素上绑定多个事件处理器,各自并不会冲突
      javascript
      btn.addEventListener(‘click’, showMessage1, false)
      btn.addEventListener(‘click’, showMessage2, false)
      btn.addEventListener(‘click’, showMessage3, false)
    • 执行时机:
      • 当第三个参数(useCapture)设置为true就在捕获过程中执行,反之在冒泡过程中执行处理函数
      • 下面举个例子:
        javascript
        <div id='div'>
          <p id='p'>
            <span id='span'>Click Me!</span>
          </p >
        </div>
      • 设置点击事件
        javascript
        var div = document.getElementById('div')
        var p = document.getElementById('p')
        function onClickFn (event) {
          var tagName = event.currentTarget.tagName
          var phase = event.eventPhase
          console.log(tagName, phase)
        }
        div.addEventListener('click', onClickFn, false)
        p.addEventListener('click', onClickFn, false)
      • 上述使用了eventPhase,返回一个代表当前执行阶段的整数值:
        • 1为捕获阶段
        • 2为事件对象触发阶段
        • 3为冒泡阶段
      • 点击Click Me!,输出如下:
        javascript
        P 3
        DIV 3
      • 可以看到,pdiv都是在冒泡阶段响应了事件,由于冒泡的特性,裹在里层的p率先做出响应
      • 如果把第三个参数都改为true
        javascript
        div.addEventListener('click', onClickFn, true)
        p.addEventListener('click', onClickFn, true)
      • 输出如下:
        javascript
        DIV 1
        P 1
      • 两者都是在捕获阶段响应事件,所以divp标签先做出响应

IE事件模型

  • IE事件模型共有两个过程:
    • 事件处理阶段:事件到达目标元素, 触发目标元素的监听函数
    • 事件冒泡阶段:事件从目标元素冒泡到document,依次检查经过的节点是否绑定了事件监听函数,如果有则执行
  • 事件绑定监听函数的方式如下:
    javascript
    attachEvent(eventType, handler)
  • 事件移除监听函数的方式如下:
    javascript
    detachEvent(eventType, handler)
  • 举个例子:
    javascript
    var btn = document.getElementById('.btn')
    btn.attachEvent(‘onclick’, showMessage)
    btn.detachEvent(‘onclick’, showMessage)

17. 事件代理(事件委托)

事件代理

  • 事件代理:就是把一个元素响应事件(clickkeydown......)的函数委托到另一个元素
  • 事件流的都会经过三个阶段:捕获阶段 -> 目标阶段 -> 冒泡阶段,而事件委托就是在冒泡阶段完成
  • 事件委托,会把一个或者一组元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素,而不是目标元素
  • 当事件响应到目标元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后在外层元素上去执行函数

应用场景

  • 如果我们有一个列表,列表之中有大量的列表项,我们需要在点击列表项的时候响应一个事件
    javascript
    <ul id="list">
      <li>item1</li>
      <li>item2</li>
      <li>item3</li>
    </ul>
  • 如果给每个列表项一一都绑定一个函数,那对于内存消耗是非常大的
    javascript
    // 获取目标元素
    let list = document.getElementsByTagName("li")
    // 循环遍历绑定事件
    for (let i = 0; i < list.length; i++) {
      list[i].onclick = function(e){
        console.log(e.target.innerHTML)
      }
    }
  • 这时候就可以事件委托,把点击事件绑定在父级元素ul上面,然后执行事件的时候再去匹配目标元素
    javascript
    // 给父层元素绑定事件
    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)
      }
    })
  • 还有一种场景是上述列表项并不多,我们给每个列表项都绑定了事件
  • 但是如果用户能够随时动态的增加或者去除列表项元素,那么在每一次改变的时候都需要重新给新增的元素绑定事件,给即将删去的元素解绑事件
  • 如果用了事件委托就没有这种麻烦了,因为事件是绑定在父层的,和目标元素的增减是没有关系的,执行到目标元素是在真正响应执行事件函数的过程中去匹配的
    javascript
    // 点击input可以动态添加元素
    <input type="button" name="" id="btn" value="添加" />
    <ul id="ul">
      <li>item1</li>
      <li>item2</li>
      <li>item3</li>
      <li>item4</li>
    </ul>
    // 使用事件委托
    const btn = document.getElementById("btn")
    const ul = document.getElementById("ul")
    const num = 4
    //事件委托,添加的子元素也有事件
    ul.onclick = (e) => {
      console.log(e.target.innerHTML)
    }
    //添加新节点
    btn.onclick = () => {
      num++
      const newLi = document.createElement('li')
      newLi.innerHTML = `item${num}`
      ul.appendChild(newLi)
    }
  • 可以看到,使用事件委托,在动态绑定事件的情况下是可以减少很多重复工作的

总结

  • 适合事件委托的事件有:clickmousedownmouseupkeydownkeyupkeypress
  • 从上面应用场景中,我们就可以看到使用事件委托存在两大优点:
    • 减少整个页面所需的内存,提升整体性能
    • 动态绑定,减少重复工作
  • 但是使用事件委托也是存在局限性:
    • focusblur这些事件没有事件冒泡机制,所以无法进行委托绑定事件
    • mousemovemouseout这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗高,因此也是不适合于事件委托的
  • 如果把所有事件都用事件代理,可能会出现事件误判,即本不该被触发的事件被绑定上了事件

18. 跨域

同源策略

  • 跨域本质是浏览器基于同源策略的一种安全手段
  • 同源策略(Sameoriginpolicy)是一种约定,它是浏览器最核心也最基本的安全功能
  • 所谓同源(即指在同一个域)具有以下三个相同点:
    • 协议相同(protocol
    • 主机相同(host
    • 端口相同(port
  • 反之非同源请求,也就是协议、端口、主机其中一项不相同的时候,这时候就会产生跨域

JSONP

  • 利用script标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的JSON数据。JSONP请求一定需要对方的服务器做支持才可以
  • JSONP的实现流程:
    • 声明一个回调函数,其函数名(如show)当做参数值,要传递给跨域请求数据的服务器,函数形参为要获取目标数据(服务器返回的data)
    • 创建一个script标签,把那个跨域的API数据接口地址,赋值给scriptsrc,还要在这个地址中向服务器传递该函数名(可以通过问号传参:?cb=cb
    • 服务器接收到请求后,需要进行特殊的处理:把传递进来的函数名和它需要给你的数据拼接成一个字符串,例如:传递进去的函数名是show,它准备好的数据是cb('hello')
    • 最后服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数cb(),对返回的数据进行操作
    html
    <button onclick="jsonpData();">跨域请求</button>
    javascript
    function jsonpData() {
      let script = document.createElement('script')
      script.src =
        'http://www.jxntv.cn/data/jmd-jxtv2.html?callback=list'
      document.body.appendChild(script)
    }
    function list(res) {
      console.log(res.data[0].list)
      document.body.removeChild(script)
    }
  • JSONP的优缺点:
    • 它不像XMLHttpRequest对象实现的Ajax请求那样受到同源策略的限制
    • 它的兼容性更好,在更加古老的浏览器中都可以运行,不需要XMLHttpRequestActiveX的支持;并且在请求完毕后可以通过调用callback的方式回传结果
    • 支持GET请求而不支持POST等其它类型的HTTP请求;它只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用的问题

CORS

  • CORS(Cross-Origin Resource Sharing,跨域资源共享)是一个系统,它由一系列传输的HTTP头组成,
  • 这些HTTP头决定浏览器是否阻止前端JavaScript代码获取跨域请求的响应
  • CORS实现起来非常方便,只需要增加一些 HTTP 头,让服务器能声明允许的访问来源,只要后端实现了CORS,就实现了跨域
  • koa框架举例,添加中间件,直接设置Access-Control-Allow-Origin响应头
    javascript
    app.use(async (ctx, next)=> {
    ctx.set('Access-Control-Allow-Origin', '*');
    ctx.set('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
    ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
    if (ctx.method == 'OPTIONS') {
      ctx.body = 200; 
    } else {
      await next();
      }
    })
  • PS:Access-Control-Allow-Origin设置为*其实意义不大,可以说是形同虚设在实际应用中,上线前我们会将Access-Control-Allow-Origin值设为我们目标host

Copyright © 2024 Fang He