前端学习笔记

217 阅读25分钟

JS基础知识

参考:juejin.cn/book/684473…

JS的7种数据类型 参考文章

这7种又分为两种,原始类型和对象类型。

原始类型(Primitive)

问:原始类型都有哪些?
答:总共有6种,boolean, number, string, null, undefined, symbol(不可枚举,独一无二可当私有属性)
在es10新增第7种类型BigInt
延申1: 原始类型存储的是值,没有函数可以调用。
undefined.toString(),1.toString() 会报错误
延申1-1: 而'1'.toString() 会打印 "1"
原因:因为原始类型(String primitives)被转换成对象类型(String object)。 创建string的方法有3种,

1. var a = "first way"
2. var b = String('second way')
3. var c = new String('third way')
//也可以通过以下方法
4. var d = a + ''

第一个和第二个方法会创建原始类型,而第三种会创建对象类型。

typeof a // 'string'
typeof b // 'string'
typeof c // 'object'
typeof d // 'string'

但是通过原始类型也可以调用对象类型的方法,此时浏览器自动把原始类型转换成对象类型。 当使用eval方法的时候原始类型被当作source code所以会返回运算结果,而对象类型会被当做对象,直接返回对象。

var a = '12 + 12'
eval(a) // 24
var b = new String('12 + 12')
eval(b) // "12 + 12"

如果想用对象类型正确使用eval该怎么办? 对象类型的StringvalueOf方法直接返回原始类型的string

eval(b.valueOf()) //24

以上同样适用与numberboolean

延申2:number里的浮点数相关, 0.1 + 0.2 !== 0.3

延申3:null既然是原始类型,为什么typeof null会输出object。 答: JS的Bug导致的。

延申4: 不可变性。 比如字符串的各种方法slice, substr, trim,toLowerCase, +=''等都是重新分配栈内存,产生新字符串,而不会去改变原来的值。而对象类型是可变的。比如数组的某些方法pop(返回删除的元素),push(返回长度),shift(返回删除的元素),unshift(返回长度),reverse,sort,splice都是改变原数组,某些比如slice是不会改变原数组的。

对象类型(Object)

  1. 对象类型和原始类型的区别:内存空间分为栈内存和堆内存。栈内存存大小固定且较小的值。所以原始类型存在栈中,堆内存存大小不固定以及较大的数据,且需要通过地址来找到堆内存里的存储位置,所以对象类型的地址存在栈内存,而值存在堆。

内存分配图

不管是原始类型还是对象类型都遵守以下规则

  • 赋值:给新变量在栈内存重新分配新的空间,新变量名对应的值是复制过来的值(如果是原始类型就是值,如果是对象类型就是地址)。
  • 比较: 比较栈内存的值(粉色区域),所以对于原始类型比较的是值,对象类型比较的是地址。
  • 参数传递:先复制一份栈内存里的值(粉色区域)再传给函数。
var a = {'a' : 1}
var b = {'a' : 1} //b存新地址,这个地址上存着全新的对象
a === b // false (粉色区域的地址不同)

var e = a  //给e分配新的空间,a的地址复制一份,存到e里
e === a //true(粉色区域的地址相同)

var c = 'a'
var d = 'a'
c === d // true(粉色区域的值相同)

var f = d
f === d // true (粉色区域的值相同)

类型转换

  1. 转换为布尔值:除了undefined, null, false, NaN, '', 0, -0 其它都转为true
  2. 转换为数字:
  • 字符串:数字转数字,其他转NaN
  • 数组: 空数组0,一个元素转成一个数字,其他NaN
  • 数组以外的引用类型,undefined=> NaN
  • null => 0
  • Symbol 报错
  1. 转换为字符串
  • 对象 => '[object Object]'
  • 其他:'原内容'

对象转原始类型

在js中对象转原始类型,必然会调用toPrimitive()内部函数。

函数形式:

toPrimitive(input, preferedType)

其中第一个参数是要转换的值,第二个参数是要转换的类型。

转number内部执行步骤:

  1. 判断input值类型是不是原始值,是直接返回值,否第2步
  2. 调用valueOf(),判断结果是不是原始值,是直接返回值,否则第3步
  3. 调用toString(),判断结果是不是原始值,是直接返回值否则抛出错误

转string类型先调用toString再调用valueOf

注意:如果省略第二个参数,此时,日期会被认为是转成字符串,其他就被转成Number

举例:

[] + [] // ""
[] + {} //"[object Object]"
{} + [] // 0 ,因为{}被认为是代码块,代码等于 +[],直接转换为0

加法运算符

  • 运算中其中一方为字符串,那么另一方也转换为字符串
  • 如果一方不是字符串或数字,那么就把它转换为数字或字符串

其他运算符,只要其中一方是数字,另外一方转成数字

比较运算符

  • 如果是对象,调用toPrimitive
  • 如果是字符串,就通过unicode字符索引来比较

==和===

==类型转换后比较,===不会类型转换直接比较

==步骤:

  1. 两边类型相同直接比较,否则转换类型
  2. 双边是null和undefined返回true
  3. 双边是string和number,字符串转number
  4. 有一边是boolean,boolean转为number
  5. 有一边是object,object转为原始类型
[] == ![] //true

步骤:
[] == ![] -> [] == false -> [] == 0 -> "" == 0 -> 0 == 0 -> true

闭包

函数里面包着函数,里面的函数可以访问到外面的函数的变量。

浅拷贝

  • Object.assign(拷贝属性值,如果属性值是对象拷贝地址)
  • ... 展开运算符

深拷贝

  • JSON.parse(JSON.stringigy(object)) -> 缺点: 忽略undefined,symbol,不能序列化函数,以及不能解决循环引用对象(方案:用lodash的cloneDeep代替)
  • 面试手写深拷贝
function deepCopy(obj) {
    if(typeof obj === 'object') {
        var result = Array.isArray(obj) ? [] : {}
        for(let key in obj) {
            result[key] = (typeof obj[key] === 'object')? deepCopy(obj[key]) : obj[key]    
        }
    } else {
       var result = obj
    }
    return result
}

原型

原型关系图

JS执行上下文和执行栈参考链接

  1. 什么是执行上下文:JS代码被解析和执行时所在的环境。
  2. 执行上下文有3种:
  • 全局执行上下文 : 不在任何函数里,就是在解析全局代码时创建的。做两件事: 1)创建全局对象(window对象),2)this指向全局对象
  • 函数执行上下文:每次调用函数时会创建新的函数执行上下文。
  • Eval函数执行上下文:运行eval函数种也会创建。
  1. 执行栈:LIFO(后进先出),存所有执行上下文。

  2. 执行上下文的两个阶段:创建阶段, 执行阶段。

  • 创建阶段:
    • 确定this的值
    • 词法环境(Lexical Environment)创建-只存储函数以及变量let和const,
      • 1)环境记录(存储变量和函数声明的实际位置,函数环境额外还包括arguments对象)
      • 2)对外部环境的引用(全局执行上下文里没有,函数环境里是包含自己的外部函数环境或者全局环境)
    • 变量环境(VariableEnvironment)创建: 与词法环境相同,只是存的变量不同,变量环境仅用于var绑定
let a = 20
cosnt 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: <func>
        }
        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>
    }
    
}
  • 执行阶段:完成对所有变量的分配,最后执行代码

延申1:var变量提升,let/cont而不会->原因:创建阶段代码被扫描并解析变量和函数声明,其中函数声明存储在环境中,而变量var会被设置为undefined,而let/const会保持未初始化。 延申2:函数声明和函数表达式的提升,原理同上。而且函数声明提升优先于变量提升。就算变量声明在后也无法覆盖前面声明的函数

ES6

  1. var,let,const区别:
  • let,const在全局作用域下使用,变量不会挂在到window上。
  • let和const存在暂时性死区,不能提前使用变量。
  • const不能再次赋值
  1. 原型继承

    function Parent(val) {
        this.val = val
    }
    Parent.prototype.getValue = function() {
        console.log(this.val)
    }
    
    function Child(val) {
        Parent.call(this, value)
    }
    
    Child.prototype = Object.create(Parent.prototype, {
    constructor: {
      value: Child,  
      enumerable: false,
      writable: true,
      configurable: true
    }
    })
    
    const child = new Child(1)
    

    Class继承

    好处:

    1. 一定要new调用不然会报错
    2. 原型上的方法默认不可枚举的
    3. 很方便的实现继承
    class A extends B { //注意不是对象不能加逗号,也不用加冒号以及function关键字 
         static a() {}//挂在自己本身的函数
         constructor(){
             super()//记得继承的时候在子类的构造函数调用父类的构造函数super()等于B.call(this)
             super.add()//直接调用父类的方法
         }//自己当成构造函数
         method1(){}//挂在prototype(原型)上的方法
         get length(){}//getter/setter也都是挂在原型上的
         set length(val){}
    }
    

模块化

  1. 为什么使用模块化? 答:提供复用性, 提高代码维护性

  2. 实现模块化的方式
    1)CommonJS-Node使用的
    用法:

//add.js,想导出的文件
export.add = function add(a, b) {
    return a + b
}

//use.js , 在哪里导出
var add = require(./add.js)
return add(1, 2)

require实现

function require(path) {
    var fileContent = readFile(path)
    var f = new Function('export', 'module', fileContent)
    var export = {}
    var module = {}
    module.export = export
    f(export, module)
    return module.export
}

2)ES Module 是原生实现的模块化方案

//引入
import xxx from './a.js'
//导出
export default function(){}

, 与CommomJS的区别:

  • CommonJS支持动态导入 require(${path})
  • CommonJS同步导入, 因为是在服务端使用,所有文件同步导入也影响不大, 而ESModule是用于浏览器的,如果也是同步导入对渲染有很大影响
  • CommonJS导出是值拷贝, ESModule导出的是地址
  • ESModule会编译成require/exports

promise

一个promise对象代表一个异步操作的结果

异步结果不可能马上传到外部,需要等异步程序结束之后把结果传给回调函数来通知外部。而外部之前是通过传来的数据形式来区分自己被传成功值还是失败值。比如callback(err)只传一个参数和callback(null,data)传两个参数。 promise的话差不多以上原理只是
规则1:直接传两个回调函数,resolve和reject到时候谁被调用就表明其结果是什么
规则2:获取promise结果的方法,直接调用promise的then方法,如果此promise成功就把成功值传到第一个函数(也就是then的第一个参数是函数,而这个函数接受到的参数就是上一个promise成功值),否则传第二个函数(第二个参数也是函数,它接收的参数就是上一个promise的失败原因)。等待then里面确认结果后根据return值最终返回一个新的promise。

p2 = p1.then(f1, f2)

规则3:运行代码时先创建p2可是没有确定最终结果,等p1的f1或者f2调用完之后如果结果是
3-1)抛出错误: p2的确定值是失败的
3-2)返回值(除了promise以外的任何值): p2的确定值是成功的,注意:如果没有值return自动返回undefined,结果也算成功
3-3)返回promise: p2的确定值需要等到这个promise的值确定下来,结果取

注意:以下两种形式完全不同

p1.then(f1,f2)
p1.then(f3,f4)
p1.then(f5,f6)
不同与
p1.then(f1,f2)
  .then(f3,f4)
  .then(f5,f6)

上面一段表示全是挂在p1,只要p1确定结果挂在自己身上的所有异步函数全部同时执行, 下面一段表示每一个then都是挂在新的promise上。 3个then是直接在调用栈执行,执行完then,把异步函数存起来,马上导出尚未确定的promise,再执行下一个then。几乎then都是同步执行的。 之后当前一个promise确定状态后后面的异步函数也将逐步执行使后面的promise也确定状态。

手写promise

  class myPromise {   
    constructor(f){
    //起初new一个promise的时候接受一个异步函数,给此函数传resolve,reject两个参数(异步函数),初始化状态如下几个
      this.state = 'pending'
      this.value = null
      this.reason = null
      this.resolvedCallbacks = []
      this.rejectedCallbacks = []
      //等到异步函数出来结果之后通知resolve,并把结果传给resolve
      //resolve收到结果之后改promise的内部状态并执行挂在自己身上的所有异步操作
      const resolve = (value) => {
        setTimeout(() => {
          if(this.state === 'pending'){
            this.state = 'resolved'
            this.value = value
            this.resolvedCallbacks.map(cb => this.value = cb(this.value))
          }
        }, 0)
      } 
      //reject跟resolve一样
      const reject = (reason) => {
        setTimeout(() => {
          if(this.state === 'pending'){
            this.state = 'rejected'
            this.reason = reason
            this.rejectedCallbacks.map(cb => this.reason = cb(this.reason))
          }
        }, 0)
      } 
      //给异步函数传两个参数并执行此异步函数
      try{
        f(resolve, reject)
      } catch(e){
        reject(e)
      }
   }  
   //能取到promise内部value或者reason得方法就是then
   //then的第一个函数参数接收value为参数,第二个函数参数接受reason为参数
   //不管是在第一个函数还是在第二个函数如果成功就是成功,如果失败就是失败。等到确定结果封装成promise之后返回,所以每一次then之后的结果是全新的promise 这里没有实现
    then(onFullfilled, onRejected){
    //以防then的两个参数不是函数,封装成透传函数后再执行
      onFullfilled = typeof onFullfilled === 'function'? onFullfilled : v => v
      onRejected = typeof onRejected === 'function'? onRejected : r => {throw r}
    
      this.resolvedCallbacks.push(onFullfilled)
      this.rejectedCallbacks.push(onRejected)
    
      return this
    } 
	
}

实现Promise.resolve()

Promise.resolve = function(val) {
    return new Promise((resolve) => resolve(val)
}

实现Promise.reject()

Promise.reject = function(val) {
    return new Promise((resolve, reject) => {
        reject(val)
    })
}

实现catch

Promise.prototype.catch = function(onRejected) {
    return this.then(null, onRejected)
}

实现Promise.all:接受一个以promise组成的数组,返回一个promise,这个promise的值是由所有promise的结果按顺序组成的数组

Promise.all = function(promises) {
  return new Promise((resolve, reject) => {
    var result = []
    var resolvedCount = 0
    for(let i = 0 ; i < promises.length ; i++){
      Promsie.resolve(promises[i]).then(val => {
        result[i] = val
        resolvedCount++
        if(resolvedCount == promises.length){
          resolve(result)
        }
      }, reason => {
        reject(reason)
      })
    }
      
  })
}

实现Promise.race:数组里面第一个成功或失败的值为最终结果

Promise.race = function(values) {
  return new Promise((resolve, reject) => {
    for(let i = 0 ; i < values.length; i++){
      Promise.resolve(values[i]).then(val => {
        resolve(val)
      }, reason => {
        reject(reason)
      })
    }
  })
}

####Promises/A+ 标准看了一遍记不住

  1. 术语
    1.1. 'promise'是一个对象或者函数,有一个then方法并且满足A+标准
    1.2. 'thenable'是一个有then方法的对象或者函数
    1.3. 'value'是任意的js值包括undefined,thenable或promise
    1.4. 'exception'被thorw抛出来的异常
    1.5. 'reason'为什么promise失败的理由

  2. 需要满足的标准
    2.1. promises状态
    promise必须在三个状态中一个,pending,fulfilled, rejected。
    如果在pending可以转移到fullfilled或者rejected
    如果在fullfilled不可以转移到其他状态,而且值也不能改变
    如果在rejected不可以转移到其他状态,而且其原因也不能再改变
    注意:这里的不可变指的是对象还是那个对象就行了,对象的属性变化是可以的。
    2.2. then方法
    一个promise需要提供一个then方法用来访问当前或者最终的value或者reason。
    then方法接受两个参数

      promise.then(onFulfilled, onRejected)
    

    2.2.1 两个参数都是可选的而且必须是函数,如果参数不是function,就会被忽略掉

    2.2.2 如果onFulfilled是一个函数
    等promise的状态变成fulfilled之后被调用,并且被传promise的value为第一个参数。
    在fulfilled(成功)之前不能被调用,也不能被调用多次。
    2.2.3 如果onRejected是一个函数

    等promise的状态变成rejected之后被调用,并且被传promise的reason为第一个参数。
    在rejected(失败)之前不能被调用,也不能被调用多次。

    2.2.4 onFulfilled和onRejected函数只有在执行上下文栈里面仅包含平台代码的时候才能被执行,也就是被异步调用,类似于setTimeOut(,0)调用。 构造函数是立刻调用。

    2.2.5 onFulfilled和onRejected是以函数的形式调用所以this是没有值的。

    2.2.6 在同一个promise可以多次调用then

    当promise状态成功之后所有对应成功的异步函数会按照挂上去的顺序调用。 当promise状态失败之后所有对应失败的异步函数会按照挂上去的顺序调用。

    2.2.7 then必须返回一个promise

        promise2 = promise1.then(onFulfilled, onRejected)//如果没传参数promise2可以取跟promise1一样的状态
    

    2.2.7.1 如果onFulfilled或onRejected返回一个值x,运行Promise Resolution Procedure(promise解决过程)[[Resolve]](promise2, x)
    2.2.7.2 如果onFulfilled或onRejected返回异常e,promise2必须以e为理由失败。
    2.2.7.3 如果onFulfilled
    不是函数而promise1恰好是成功的,那么promise2就会以promise1的值为成功值。
    2.2.7.4 如果onRejected 不是函数而promise1恰好是失败的,那么promise2就会以promise1的失败原因为自己失败原因。

    2.3 Promise Resolution Procedure(promise解决过程) Resolve就是函数,接受promise和value为参数。标记为[[Resolve]](promise, x). 如果x是thenable就取x的状态,否则以x为value成功。 之所以取thenable是为了让不同的promise之间可以相互交互。

    2.3.1 如果promise和x指向同一个对象,promise就以TypeError为reason失败。
    2.3.2 如果x是promise,就取x的状态
    2.3.3 如果x是一个对象或函数
    2.3.3.1 取x的then方法
    2.3.3.2 如果取出then的过程中就已经报错的话直接失败
    2.3.3.3 如果then是一个函数,call这个函数的this为x,并且第一个参数为resolvePromise,第二个参数为rejectPromise。为什么以call调用then是因为有可能then本身是getter,第一次取出和第二次取出有可能不一样。比如如下

         var a = 1
         Object.defineProperty(window, 'x', {
             get: function(){
                 return a++
             }
         })
         
         if(x == 1 && x == 2 && x == 3) {
             console.log(true)
         }
    

    2.3.3.3.1 如果resolvePromise被值y调用,执行[Resolve]
    2.3.3.3.2 如果rejectPromise以r为失败, promise也以r为原因失败。
    2.3.3.3.3 如果resolvePromise,rejectPromise都被调用或者多次调用,只有第一次调用有效,后面的全部忽略。
    2.3.3.3.4 如果调用then的时候抛出错误
    2.3.3.4.1 如果resolvePromise,rejectPromise被调用过忽略即可
    2.3.3.4.2 否则以捕捉到的e为理由失败
    2.3.3.4 如果then不是函数,就以x成功promise
    2.3.4 如果x不是对象或函数,直接以x为值让promise成功。

        function ResolvePromise(promise, x, resolve, reject) {
            if (x === promise) {
                reject( new TypeError())
                return
            }
            
            if(x instanceof MyPromise) {
                x.then(resolve, reject)
                return
            }
            if(x && typeof x == 'object' || typeof x == 'function'){
                let then
                try {
                  then = x.then  
                } catch(e) {
                    reject(e)
                    return
                }
                if(typeof then === 'function') {
                    let called = false
                    try {
                      then.call(x, function resolvePromise(y){
                        if(!called) {
                            called = true
                            ResolvePromise(promise, y, resolve, reject)
                        }
                    }, function rejectPromise(r){
                        if(!called) {
                            called = true
                            reject(r)
                        }
                    })  
                    } catch(e) {
                       if(!called){
                           reject(e)
                       } 
                    }
                } else {
                    resolve(x)
                }
            } else {
                resolve(x)
            }
        }

async await

promise和生成器结合

function get(url) {
  return new Promsie(resolve => {
    var xhr = new XMLHttpRequest()
    xhr.open('get',url)
    xhr.onload = () => resolve(xhr.respenseText)
    xhr.send()
  })
}

function * foo() {
  var x = yield get('/')  // 下次调用then时传的参数当成yield的结果赋值给x
  console.log(x)
  var y = yield get('/one')  // 下次调用then时传的参数当成yield的结果赋值给y
  console.log(y)
  var z = yield get('/two')  // 下次调用then时传的参数当成yield的结果赋值给z
  console.log(z)
}

iter = foo()//返回生成器函数 foo{<suspended>}
obj = iter.next() //返回 {value:Promise, done:false}
obj.value.then(val => iter.next(val)) //上一次的promise结束之后调用then,并把自己的结果当参数传给生成器函数调用

//封装成函数(把生成器函数(这个生成器函数每次返回一个promise)从头执行到尾,最后返回一个promise让其继续可以调用)
function run(generatorFunction) {
  return new Promise((resolve, reject) => {
    var iter = generatorFunction()
    var generated = iter.next()
    step()
    function step() {
      if(!generated.done) {
        generated.value.then(val => {  //generated.value是yield返回的promise
          try{
            generated = iter.next(val)
          } catch(e) {
            reject(e)
          }
          step()
        }, reason => {
          try {
            generated = iter.throw(reason)//从yeild抛出去
          } catch(e) {
            reject(e)
          }
          step()
        })
      } else {
        Promise.resolve(generated.value).then(resolve, reject)
      }
    }
  })
}

run函数被浏览器已经实现好了就是async await

function squareAsync(x) {
  return new Promise(resolve => {
    setTimeout(() => resolve(x * x), 1000 + Math.random() * 2000)
  })
}

async function foo(n) { //总是返回一个promise
  var a = await squareAsync(n) // await等待异步函数(一般写promise)执行完,再把结果赋值给a
  console.log(a)
}

foo(5) // 结果: 返回promise并打印25
foo(5).then(val => console.log(val)) //结果: 返回promise,打印25,打印undefined
//原因:因为foo函数没有返回结果

Event Loop 事件循环

1. 首先执行同步代码
2. 执行过程中每遇到异步代码分微任务和宏任务各自放到自己的任务队列中, 区别:执行微任务的时候如果又有微任务会直接执行,而宏任务中出现的异步函数会安排到下一轮
3. 当执行完所有同步代码后,从微任务队列中取出可执行的微任务到执行栈执行
4. 之后如果需要渲染页面
5. 之后执行宏任务中的异步代码

微任务包括: process.nextTick(node专属,早于promise.then执行), promise的then, MutationObserver(监控dom树的变化)
宏任务包括: script, setTimeout, setInterval,setImmediate, I/O, UI rendering

在node定义event loop

使得Node.js可以在单线程执行非阻塞I/O操作(非阻塞: 遇到异步操作调一下就离开,把自己的异步操作卸载给操作系统)。 等系统执行完异步操作后通知Node.js 可以把回调放到poll队列去执行。

步骤:

  1. INIT (当Node.js开始初始event loop)
  2. INPUT SCRIPT (执行初始输入代码)
  3. 每遇到异步代码往各个事件循环的不同阶段(每个阶段都是先进先出的队列)安排函数
 - timers:执行被setTimeout() 【注意如果设置0自动被改成1毫秒】 和setInterval()设定的到时间的函数.
 - pending callbacks:被延迟到下一圈的I/O回调函数,poll阶段来不及执行的函数
 - idle, prepare: 内部系统
 - poll:  第一件是计算在这个阶段可以停顿多久以及等待I/O;
 第二件是执行除了setTimeout,setInterval, setImmediate,close以外的所有回调函数。
 - check: setImmediate()的回调函
 - close callbacks:  close的回调函数 
 - 额外:nextTick()的函数不管当前是什么阶段,走下一个阶段之前先执行。也就是每个阶段间隙之间执行。特殊:如果nextTick里又安排nextTick无限安排会导致程序一直卡在间隙里。其它异步函数是安排在下一轮。nextTick优先于promise.then。 
  1. 开始进入事件循环。从timer开始执行自己阶段里被安排的到时间的函数,执行时如果又遇到异步代码继续往各个阶段安排函数。把自己阶段里的所有函数都执行完继续走下一个阶段。
  2. 一直循环执行,等到所有阶段都没有被安排函数就会退出node

浏览器基础知识

事件机制

  1. addEventListener: 代码在事件发生时响应- 事件处理器 。 是全局属性,可在windo也可在各个dom元素使用

  2. 给dom元素绑定处理器的方法 1)用onclick : 每次只能捆绑一个事件

    // 里面只能写函数调用表达式 2)addEventListener: 可添加任意数量的事件 以上方法冲突时,会执行第二个捆绑的事件 3) 相应的还有 removeEventListener : 移除事件的方法 注意: 需要传同一个函数,而不是名字和内容相同的函数, 所以匿名函数不能执行事件

  3. 事件对象event: 事件触发时为事件处理函数传一个参数就是event对象(里面包括很多属性) 比如: which : 哪个鼠标 1-left 2-middle 3- right detail: 连击次数 type:”click“/”mousedown“ target == srcElement : 点击的元素targetting元素 clientX clientY : 点击的位置相对视口的左边和上面的位置 等等还有很多 注意: event就算不单独传参数,函数自动从全局event读取event 另外有this参数, 哪个元素调用this就是哪个元素本身。 记住: this不能用箭头函数,只能用function

//获取点击的位置相对元素是多少\网站:D3 =》 画图标的库

  function mousePosition(node) {
    var elPos = node.getBoundingClientRect()
    var mouseClientPositionX = window.event.clientX
    var mouseClientPositionY = window.event.clientY
    return {
      x: mouseClientPositionX - elPos.x,
      y: mouseClientPositionY - elPos.y
    }
  }
  var div = document.querySelector('div')

  div.addEventListener('click', function() {
    var pos = mousePosition(this)
  })

  1. e.target => 点击哪个元素它就变成那个元素,事件最初开始的元素
      • 额外记住matches函数,用选择器的方法灵活匹配元素标签 if (event.target.matches('div button'))

应用: 撒大网 , 给最外层绑定事件, 里面的元素谁被点击,最外层函数的参数就变成那个元素 专业名词: 事件代理, 事件委托 , Event Delegate

<body>
<div>
 <button>I`m No.1</button>
 <button>I`m No.2</button>
 <button>I`m No.3</button>
 <button>I`m No.4</button>
 <button>I`m No.5</button>
 <button>I`m No.6</button>
 <button>I`m No.7</button>
 <button>I`m No.8</button>
 <button>I`m No.9</button>
 <button>I`m No.10</button>  
</div>

<script>
//方法 1
var buttons = document.querySelectorAll('button')
// for(var i = 0 ; i < buttons.length; i++) {  //外层作用域的i早就变成le10
//   buttons[i].addEventListener('click', function() { //点击完后i已经遍历完毕
//     console.log(i)  这个函数里面没有i,要去外面的作用域找i
//   }) 
// }
//以上注释部分,不管按哪个案件总是log出10 ,所以用let声明


//   for(let i = 0 ; i < buttons.length; i++) {  
//   buttons[i].addEventListener('click', function() { 
//     console.log(i)  
//   }) 
// }

//用新函数创建一个内部作用域,i是形参,调用时传i
// for(var i = 0 ; i < buttons.length; i++) { 
//   (function(i) { // 这里的i是形参,也可改成别的
//     buttons[i].addEventListener('click', function() { 
//       console.log(i)  
//     }) 
//   }(i)) //这是传参i
// }

// 方法2:
var div = document.querySelector('div') //获取要绑定的元素

div.addEventListener('click', function(event) { //开始绑定事件
// if(event.target.tagName === 'BUTTON')  //获取target元素,元素的标签名字用tagName获取,注意:::tagName返回大写字母
  if (event.target.matches('div button')) // 改成选择器样式
   var btns = Array.from(div.children) // Array.from 浅复制类数组对象变成数组, 获取某个元素的子元素们的方法.children
   console.log(btns.indexOf(event.target)) //把按键的下标返回
})
</script>

3) e.currentTarget 能获取现在事件进行到哪里了

  1. 事件冒泡 propagation
    1) 冒泡阶段: BTN -> p -> div -> body 全部绑定事件, 点击btn往外触发他们自己的事件。 就算没有绑定事件,事件冒泡阶段是总是有。
    2)总共有三个阶段:一)从外到内捕获阶段Capture 二)目标(执行/点击)阶段targetting 三) 再从内到外 冒泡 propagation阶段是捕获还是冒泡阶段, 谁先绑定的先执行谁
    3) 绑定方法: div.addEventListener('click', function() {console.log()}, true) 不传第三个参数true的话是给冒泡阶段绑定的, 如果传true说明是在捕捉阶段绑定的
    4) 为每一次点击事件浏览器创建一件对象,不管是在什么阶段,都共用一个事件对象event
    5) stopPropagation : 阻止到下一个元素,那个被设定停止的元素还是把自己元素内部的所有绑定事件执行完的; 单词是传播的意思,跟冒泡没关系
  1. e.stopImmediatepropagation: 直接马上停止,不会依旧执行自己内部函数

举例:

 <div onclick = "console.log('div propagation')">div
   <p>
     <button> click me </button>
   </p>
 </div>
//propagation阶段绑定
 var p = document.querySelector('p')  // 每次记得先获取元素node
 var button = document.querySelector('button')
 p.addEventListener('click', function() {
   console.log('p propagation')
 })
 button.addEventListener('click', function() {
   console.log('button propagation')
 })

//capture阶段绑定
 var div = document.querySelector('div')
  div.addEventListener('click', function() {
  console.log('div capture')
  }, true)
   p.addEventListener('click', function() {
   console.log('p capture')
 }, true)
   button.addEventListener('click', function() {
   console.log('button capture')
 }, true)


 // =>如下顺序输出
// "div capture"
// "p capture"
// "button propagation" 因为target不在乎什么阶段,只根据谁先绑定来执行
// "button capture"
// "p propagation"
// "div propagation"

//绑定双击事件
 var div = document.querySelector('div')
  div.addEventListener('dblclick', function() {
  console.log('div capture dblclick')
  }, true)
   p.addEventListener('dblclick', function() {
   console.log('p capture dblclick')
 }, true)
   button.addEventListener('dblclick', function() {
   console.log('button capture dblclick')
 }, true)

 
 div.addEventListener('dblclick', function() {
  console.log('div  propagation dblclick')
  })
 p.addEventListener('dblclick', function() {
   console.log('p propagation dblclick')
 })
 button.addEventListener('dblclick', function() {
   console.log('button propagation dblclick')
 })

//如下输出: 双击触发三件事, 第一次点击 第二次点击(重复第一次) 以及双击事件
// "div capture"
// "p capture"
// "button propagation"
// "button capture"
// "p propagation"
// "div propagation"
// "div capture"
// "p capture"
// "button propagation"
// "button capture"
// "p propagation"
// "div propagation"
// "div capture dblclick"
// "p capture dblclick"
// "button capture dblclick"
// "button propagation dblclick"
// "p propagation dblclick"
// "div  propagation dblclick"
  1. 实现matches函数
  //div.foo.bar p#id998 span
  function matches(node, selector) {
    var selects = selector.split(' ')
    var i = selects.length - 1
    if(matchesSingleComboSelector(node, selects[i])) {
      i--
      node = node.parentNode // 不是parent是parentNode
      while(node && i >= 0) {
        if(matchesSingleComboSelector(node, selects[i])) {
          i--
          node = node.parent
        } else {
          node = node.parent
        }
      }
      return i == -1
    } else {
      return false
    }
  }


  // 返回node是否匹配selector
  //其中selector为没有层级关系的选择器, 即无语法空格
  function matchesSingleComboSelector(node, selector) {
    var selects = selector.split(/(?=\.|\#)/g) //记得加括号的g,正则表达式不要加双引号
    // for(var i = 0; i < selects.length ; i++) { / /?
    //   matchesSingleSelector(node, selects[i]) 
    // } 以上错误,重新写
    return selects.every(s => {
      return matchesSingleSelector(node, s)
    })
  }

  //返回node是否匹配selector
  //其中selector只为一个非复合选择武器
  //如: div  .foo .bar #id
  function matchesSingleSelector(node, selector) {
    if(selector[0] == ".") {
      return  matchesClassSelector(node, selector)
    } else if(selector[0] == "#") {
      return matchesIdSelector(node, selector)
    } else {
      return matchesElementSelector(node, selector)
    }
  }

  function matchesClassSelector(node, selector) {
    return node.classList.contains(selector.slice(1)) //不是contain是contains
  }
  function matchesIdSelector(node, selector) {
    return node.id == selector.slice(1)
  }
  function matchesElementSelector(node, selector) {
    if(selector[0] == '*') {
      return true
    }
    return node.tagName === selector.toUpperCase()  // 记住:大写
  }

  1. default actions 默认行为
    0)contextmenu 右击
    1) 例如: a标签点击自动打开页面, 滚轮会自动滚动网页, 表单里按tab自动跳到下一项 点击表单提交/重置
    2) 大部分元素如果被绑定事件,就会早于它的默认行为执行的
    3) 阻止默认行为的方法:preventDefault
   <body>
   <a href = "https://www.baidu.com">打不开的百度</a>
   </body>
   <script>
     var a = document.querySelector('a')
     a.addEventListener('click', e => { //左键
       e.preventDefault()
     })
     window.addEventListener('contextmenu', e => { //右击
       e.preventDefault()
     })
     </script>

4)window.close, window.scroll,快捷键等默认行为是无法拦截的

关闭窗口之前询问onbeforeunload/beforeunload

 window.addEventListener('onbeforeunload', function(e) {
  e = e || window.event;
  // 兼容IE8和Firefox 4之前的版本
  if (e) {
    e.returnValue = '关闭提示';
  }

  // Chrome, Safari, Firefox 4+, Opera 12+ , IE 9+
  return '关闭提示';
 })

   window.addEventListener('beforeunload', function(e) {
  var confimationMessage = "\o/"
  (e || window.event).returnValue = confimationMessage
  return confimationMessage
 })