一、变量类型和计算

1.值类型和引用类型的区别

值类型通过栈来存储:

引用类型是同时通过栈和堆存储:

常用引用类型:

2.typeof运算符

可以识别所有值类型,能判断函数,能识别引用类型(但不能再继续识别 直到object)

3.深拷贝

function deepClone(obj = {}) {
    if (typeof obj !== 'object' || obj == null) {
        // obj 是 null ,或者不是对象和数组,直接返回
        return obj
    }

    // 初始化返回结果
    let result
    if (obj instanceof Array) {
        result = []
    } else {
        result = {}
    }

    for (let key in obj) {
        // 保证 key 不是原型的属性
        if (obj.hasOwnProperty(key)) {
            // 递归调用!!!
            result[key] = deepClone(obj[key])
        }
    }

    // 返回结果
    return result
}
三个要点:
(1)判断值类型还是引用类型
(2)判断是数组还是对象
(3)递归

对递归理解的不好 需要多看一看

4.类型转换

4.1字符串拼接
4.2 ==运算符

== 会发生类型转换,使结果尽可能相等,为了避免 == 产生不理想的结果,尽量使用===
4.3 if语句和逻辑运算
truly 变量 和 falsely 变量

在 if 语句中 判断的就是truly 变量 和 falsely 变量

在逻辑判断中 判断的也是truly 变量 和 falsely 变量

二、原型和原型链

1. class实现继承

class 本质上的 function 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Animal {
  constructor (type) {
    this.type = type
  }
  walk () {
    console.log(`I am walking`)
  }
  static eat () {
    console.log(`I am eating`)
  }
}
 
class Dog extends Animal {
  constructor () {
    super('dog')
  }
  run () {
    console.log('I can run')
  }
}

2.理解JS原型

使用 instanceof 判断所属类型

a.每个class都有显示原型 prototype
b.每个实例都有隐式原型_proto_
c.实例的_proto_指向对应class的prototype

3.原型链

instanceof 就是顺着原型链 往上找

通过hasOwnProperty来验证某一个属性属于它自己,还是原型

4.手写简易JQuery

利用 class 思路 来写jQuery 进行DOM操作
class jQuery {
        /*先做DOM查询*/
    constructor(selector) {
        const result = document.querySelectorAll(selector)
        const length = result.length
        for (let i = 0; i < length; i++) {
            this[i] = result[i]/*和其他constructor一样*/
        }
        this.length = length
        this.selector = selector
                /*得到一个类数组*/
     get(index) {
+        return this[index]
+    }
+    each(fn) {/*遍历DOM*/
+        for (let i = 0; i < this.length; i++) {
+            const elem = this[i]
+            fn(elem)
+        }
+    }
+    on(type, fn) {
+        return this.each(elem => {
+            elem.addEventListener(type, fn, false)
+        })
+    }
+    // 扩展很多 DOM API
+}
+/*考虑扩展性*/
+/*插件形式 往原型链上添加方法*/
+jQuery.prototype.dialog = function (info) {
+    alert(info)
+}
+
+/*复写机制*/
/*写一个新的class 继承老的jQuery*/
+class myJQuery extends jQuery {
+    constructor(selector) {
+        super(selector)
+    }
+    // 扩展自己的方法
+    addClass(className) {
+
+    }
+    style(data) {
+        
+    }
+}

使用:

三、作用域和闭包

1.作用域和自由变量


(很显然 可以向内使用 不能向外使用)
作用域:全局作用域、函数作用域、块级作用域(let const)
自由变量:即在当前作用域没有被定义却被使用,就一层一层向外寻找,直到全局作用域

2.闭包

闭包是作用域应用的特殊情况:
  • 函数作为返回值返回  
  • 函数作为参数被传递
+// 函数作为返回值
+ function create() {
+    const a = 100
+     return function () {
+         console.log(a)
+     }
+ }
+
+ const fn = create()
+ const a = 200
+ fn() // 100
+
+// 函数作为参数被传递
+function print(fn) {
+    const a = 200
+    fn()
+}
+const a = 100
+function fn() {
+    console.log(a)
+}
+print(fn) // 100

闭包内所有自由变量的查找,是在函数定义的地方向上查找,而不是执行的地方向上查找

3.this

this的应用场景很多很复杂 但: this取什么值 是在函数执行时确定的 而不是定义时确定的

call bind 可以改变this 的绑定  但call是直接改变    bind是返回一个新的函数

注意上图左右对比:
左边:是由setTimeout直接触发这个函数 所以this指向window
右边:箭头函数的this  固定为上一作用域的this 

this应用场景小结:
  • 当作普通函数被调用:window
  • 使用call apply bind:传入什么绑定什么
  • 作为对象方法调用:返回对象本身
  • 在class中调用:返回class本身
  • 箭头函数:返回上一作用域的this

4.(重点)手写bind、apply、call

  • apply 用数组传参
  • call 需要分别传参
  • 与 bind 不同 call/apply 会立即执行函数
  • bind 是复制函数  会返回新函数
// 模拟 bind
Function.prototype.bind1 = function (...args) {

    // 获取要绑定的新this(数组第一项 并弹出)
    const t = args.shift()

    // 旧this  就是fn1.bind(...)中的fn1
    const self = this

    // 返回一个函数
    return function () {
        return self.apply(t, args)//就相当于 fn1.apply(t,args)
    }
}

function fn1(a, b, c) {
    console.log('this', this)
    console.log(a, b, c)
    return 'this is fn1'
}

const fn2 = fn1.bind1({x: 100}, 10, 20, 30)
const res = fn2()
console.log(res)
在这篇文章的基础上,自己写了一写:

要理解call的本质:使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。
可以把call函数理解为以下形式:
bar.call(foo); // 1

//等同于//

var foo = {
    value: 1,
    bar: function() {
        console.log(this.value)
    }
};

foo.bar(); // 1
即在foo中调用bar 从而实现改变this的指向
所以得到模拟的步骤:
  1. 将函数设为对象的属性  foo.fn=bar
  2. 执行该函数  foo.fn()
  3. 删除该函数  delete foo.fn
//手写call//
Function.prototype.call2 = function (...args) {
    const t = args.shift();
    t.fn = this;
    const result = t.fn(...args);
    delete t.fn;
    return result
}

// 测试一下
var foo = {
    value: 1
};

function bar(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value);
}

bar.call2(foo, 'kevin', 18);
// kevin
// 18
// 1
成功啦!
手写apply同理 但apply是传入一个数组
Function.prototype.apply = function (context,...args) {
    context.fn = this;
    var result;
    if (!arr) {
        result = context.fn();
    }
    else {
        result = context.fn(...args);
    }

    delete context.fn
    return result;
}
但就像文章里说的call 是 ES3 的方法,我们为了模拟实现一个 ES3 的方法,要用到ES6的方法不是很合适
我为了图方便就先用...args的方法了  有空还得好好领会一下文章里的方法

5.闭包的实际应用

可隐藏数据 只对外提供API
// 闭包隐藏数据,只提供API
function createCache() {
    const data = {} // 闭包中的数据,被隐藏,不被外界访问
    return {
        set: function (key, val) {
            data[key] = val
        },
        get: function (key) {
            return data[key]
        }
    }
}

const c = createCache()
c.set('a', 100)
console.log( c.get('a') )
//无法直接访问 data[a] 

6.点击数字弹出对应数字

let a
for (let i = 0; i < 10; i++) {
    a = document.createElement('a')
    a.innerHTML = i + '<br>'
    a.addEventListener('click', function (e) {
        e.preventDefault()
        alert(i)
    })
    document.body.appendChild(a)
}  //错误方法 每次弹出的都是10:
let a,i
for (i = 0; i < 10; i++) {
    a = document.createElement('a')
    a.innerHTML = i + '<br>'
    a.addEventListener('click', function (e) {
        e.preventDefault()
        alert(i)
    })
    document.body.appendChild(a)
}
必须把 i 定义为块级作用域 因为click不会被立刻执行 如果把 i 定义为全局作用域 那么等到执行click的时候,循环已经执行结束,此时i = 10 
但是作为块状作用域 每次执行时 都会形成一个新的块

四、异步问题

之前写的同步异步的小总结:https://blog.nowcoder.net/n/6e215afcffa44c57887870a54e1ccda3

重点题目:

(1)!!同步和异步的区别

JS是一种单线程语言,每次只能做一件事。JS和DOM渲染共用一个线程,因为JS可修改DOM结构
使用异步,不会阻塞代码执行,不会导致卡顿。使用同步,则会导致卡顿
(setTimeout DOM ajax 等并不是ES6的内容 而是浏览器定义的 所以是WebAPIs)
(异步事件如setTimeout ajax 和  DOM事件 都是基于event loop实现的 只是触发时间不同 触发事件由浏览器决定 
但DOM事件不能被单纯的看作是异步)
异步的运行机制为
所有同步任务都在主线程上执行,形成一个执行栈(Call stack),任务执行后会被移除
当异步任务进入主线程,异步任务会被移进WebAPIs中等待被执行时机
(比如点击事件的点击 定时器的定时 )
时机到了,就把异步任务塞进任务队列(callback queue)中
stack空了后  event loop自动触发 
event loop不断检查queue中有没有未执行的异步任务
如果有,就把该异步任务移动到call stack中执行
主线程不断重复上面的第三步
注:异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。例如ajax的success,complete,error也都指定了各自的回调函数,这些函数就会加入“任务队列”中,等待执行
!!!!!图中的event loop就是异步回调的实现原理 也是重点!!!!!!
重点:
JS和DOM共用一个线程,在JS执行时还要让DOM渲染,那么什么时候渲染DOM呢?
就是在call stack空闲时!!!
当call stack空闲时,首先尝试DOM渲染,再触发下一次的Event Loop
即:每执行完一次异步,先尝试渲染DOM ,再用event loop去拿新的异步任务

(2)使用异步的场景有哪些?

应用于一些需要等待的情况中,如网络请求(如ajax 图片加载) 和定时任务(如setTimeout)
ajax:

图片加载:

定时器:

(3)手写promise

为了解决回调地狱问题而提出了promise
使用promise是管道形式的 而不是嵌套形式的 
Promise包含pendingfulfilledrejected三种状态

pending 指初始等待状态,初始化 promise 时的状态 不会触发then catch
resolve(是个函数) 指已经解决,将 promise 状态设置为resolved 会触发then回调函数
reject (是个函数)指拒绝处理,将 promise 状态设置为rejected  会触发catch回调函数
状态转化是单行的,不可逆转。一个 promise 必须有一个 then 方法用于处理状态改变
then 和 catch都会返回一个promise对象,但返回的promise对象所处状态有以下几种情况:
// then() 一般正常返回 resolved 状态的 promise
Promise.resolve().then(() => {
    return 100
})

// then() 里抛出错误,会返回 rejected 状态的 promise
Promise.resolve().then(() => {
    throw new Error('err')
})

// catch() 不抛出错误,会返回 resolved 状态的 promise
Promise.reject().catch(() => {
    console.error('catch some error')
})

// catch() 抛出错误,会返回 rejected 状态的 promise
Promise.reject().catch(() => {
    console.error('catch some error')
    throw new Error('err')
})
then
正常返回resolved状态
如果里面有报错 返回rejected

catch
同样
正常返回resolved状态
如果里面有报错 返回rejected
之前忽略了catch的返回结果!这个要好好理解!!!
then 和 catch的返回promise结果是重点题目!
几道题目:
// 第一题
Promise.resolve().then(() => {
    console.log(1)
}).catch(() => {
    console.log(2)
}).then(() => {
    console.log(3)
})

// 第二题
Promise.resolve().then(() => { // 返回 rejected 状态的 promise
    console.log(1)
    throw new Error('erro1')
}).catch(() => { // 返回 resolved 状态的 promise
    console.log(2)
}).then(() => {
    console.log(3)
})

// 第三题
Promise.resolve().then(() => { // 返回 rejected 状态的 promise
    console.log(1)
    throw new Error('erro1')
}).catch(() => { // 返回 resolved 状态的 promise
    console.log(2)
}).catch(() => {
    console.log(3)
})
手写promise加载图片
function loadImg(src) {
    const p = new Promise(
                //new promise要传入一个函数 函数里的两个参数resolve reject也是函数//
        (resolve, reject) => {
            const img = document.createElement('img')
                        //img加载成功后的回调函数//
                        //我们用promise结果callback hell 但本质还是异步问题//
                       //所以promise内里的函数仍旧是异步函数//
            img.onload = () => {
                resolve(img)
            }
                       //img加载失败的回调函数//
            img.onerror = () => {
                const err = new Error(`图片加载失败 ${src}`)
                reject(err)
            }
            img.src = src
        }
    )
    return p
}
使用:
loadImg(url1).then(img1 => {
    console.log(img1.width)
    return img1 // return一个普通对象 此处return的img要传到下一个then里
}).then(img1 => {
    console.log(img1.height)
    return loadImg(url2) 
       //return一个promise实例  因此下一个then接受的就是这个promise返回的img2对象
}).then(img2 => {
    console.log(img2.width)
    return img2
}).then(img2 => {
    console.log(img2.height)
}).catch(ex => console.error(ex))
promise方法可以返回普通对象 也可以返回promise实例
(这一部分和ES6课程中的内容结合看看)

(4)async-await

promise归根结底还是基于回调函数,对回调函数进行链式调用
而async-await是更为优雅的异步方式,是同步语法,彻底的消灭了回调函数,但要和promise结合使用
  • 执行async函数,返回的是promise对象,如果函数内没返回 Promise ,则会自动封装 不用自己new了
async function fn2() {
    return new Promise.resolve()
}
console.log( fn2() )

async function fn1() {
    return 100  // 自动封装成promise 相当于 return Promise.resolve(100)

}
console.log( fn1() ) 
  • await相当于promise的then,用来解析函数内部的异步操作,后面一定是promise对象,如果不是可以自动封装
(体会await的用法时,要时刻想着,它相当于then!)
//如果不用await 输出顺序是 2——3——now it is done
async function firstAsync () {
  let promise = new Promise((resolve, reject) => {
    setTimeout(function () {
      resolve('now it is done')
    }, 1000)
  })
   promise.then(val=>{
    console.log(val)
   })
  console.log(2)
  return 3
}
firstAsync().then(val => {
  console.log(val)
})
	
//用await后 就能保证按照顺序执行 async function firstAsync () {   let promise = new Promise((resolve, reject) => {     setTimeout(function () {       resolve('now it is done')     }, 1000)   })   console.log(await promise)//要promise的状态为resolved才会继续往下执行   console.log(await 40)//自动封装 相等于await Promise.resolve(40)   console.log(2)   return Promise.resolve(3) } firstAsync().then(val => {   console.log(val) })
再举一个例子体会await
(async function () {
    const p2 = Promise.resolve(100)
    const res = await p2 //相当于then 拿到promise里的参数
    console.log(res) // 100
})()

(async function () {
    const res = await 100
    console.log(res) // 100
})()

(async function () {
    const p3 = Promise.reject('some err')
    const res = await p3 //rejected状态 不会执行
    console.log(res) // 不会执行
})()
  • 可用try...catch捕获rejected 状态,代替了promise的catch
(async function () {
    const p4 = Promise.reject('some err')
    try {
        const res = await p4
        console.log(res)
    } catch (ex) {
        console.error(ex)
    }
})()
小结:
async
Promise
await
处理 Promise 的resolved状态
相当于then
try...catch
处理Promise的rejected状态
相当于catch

(5)异步的本质

虽然async-await非常好用 但它归根结底时语法糖 还是要牢记异步的本质
async function async1 () {
  console.log('async1 start') //2 
  await async2()//undefined
    //await的后面都可以看做callback里的内容 即异步
    //类似event loop里的setTimeout(cb1)
  console.log('async1 end') //关键一步,将它放在队列中等待,最后执行
}

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

console.log('script start') //1
async1()
console.log('script end')//4 此时同步代码执行完 开始event loop 执行异步代码
输出结果:
script star——async1 start——async2——script end——async1 end
说白了,即,只要遇到了 await ,后面的代码都是异步,把他们都放在 callback queue 里,等待event loop

(6)异步集合遍历 for await of

可以使用 for await ...of 形式
function Gen (time) {
  return new Promise((resolve, reject) => {
    setTimeout(function () {
      resolve(time)
    }, time)
  })
}
  //用for await ...of遍历异步操作集合
 async function test () {
   let arr = [Gen(2000), Gen(100), Gen(3000)]
   for await (let item of arr) {
     console.log(Date.now(), item)
   }
 }
 test()
也可以使用 for...of { ... await ...}   即在for of  循环内使用await 
// 定时算乘法
function multi(num) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(num * num)
        }, 1000)
    })
}

// // 使用 forEach ,是 1s 之后打印出所有结果,即 3 个值是一起被计算出来的
// function test1 () {
//     const nums = [1, 2, 3];
//     nums.forEach(async x => {
//         const res = await multi(x);
//         console.log(res);
//     })
// }
// test1();

// 使用 for...of ,可以让计算挨个串行执行
async function test2 () {
    const nums = [1, 2, 3];
    for (let x of nums) {
        // 在 for...of 循环体的内部,遇到 await 会挨个串行计算
        const res = await multi(x)
        console.log(res)
    }
}
test2()

如果自定义数据结构中存在异步操作:
const obj = {
  count: 0,
  Gen (time) {
    return new Promise((resolve, reject) => {
      setTimeout(function () {
        resolve({ done: false, value: time })//要遵守可迭代协议
      }, time)
    })
  },
  [Symbol.asyncIterator] () {//迭代器协议
    let self = this
    return {
      next () {
        self.count++
        if (self.count < 4) {
          return self.Gen(Math.random() * 1000)
        } else {
          return Promise.resolve({
            done: true,
            value: ''
          })
        }
      }
    }
  }
}

async function test () {
  for await (let item of obj) {
    console.log(Date.now(), item)
  }
}

test()

(7)宏任务macroTask和微任务microTask

宏任务
setTimeout setInterval Ajax DOM
在DOM渲染后触发 
微任务
Promise async-await
在DOM渲染前触发

微任务比宏任务先执行
那么宏任务为什么比微任务先执行呢?这又要从异步的执行机制说起。
在(1)中,已经阐述了异步的执行机制:
但在这张图中,走stack——WebAPIs——callback queue这一条路的只是宏任务
微任务走的是另一条路:


www
微任务直接从stack进入micro task queue这个队列里,而不经过Web APIs,因为微任务都是ES6语法规定的,宏任务是由浏览器规定的。
因此,Event Loop的完整执行步骤是
同步任务执行完毕 call stack清空
执行 micro task queue 中的微任务
尝试DOM渲染
最后触发Event Loop 从callback queue中拿出待执行的宏任务

(8)品味一道经典好题

async function async1 () {
  console.log('async1 start')//2
  await async2() //立刻跳到async2 执行console.log('async2')
  //上面有 await ,下面就变成了“异步”
  console.log('async1 end')//微任务,先放着6
}

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

console.log('script start')//1

setTimeout(function () { // 异步,宏任务,先放着 8
  console.log('setTimeout')
}, 0)

async1()

//初始化Promise时,传入的函数会被立刻执行!!//
new Promise (function (resolve) { 
  console.log('promise1') //4
  resolve()
}).then (function () { // 异步,微任务,先放着 7
  console.log('promise2')
})

console.log('script end')//5

// 同步代码执行完之后,屡一下现有的异步未执行的,按照顺序
// 1. async1 函数中 await 后面的内容 —— 微任务
// 2. setTimeout —— 宏任务
// 3. then —— 微任务



Web API


首先,要明确:
JS基础知识 规定语法 是ECMA 262标准规定的
而html DOM BOM 事件绑定 存储 CSS样式 AJAX等是JS Web API 是W3C标准规定的
接下来都是Web API的内容

五、DOM

尽管现在的框架封装了DOM操作,但DOM也是必备的基础,不能忽略
DOM(Document Object Model)本质是从HTML中解析得到的一棵树

1.DOM节点操作

获取DOM节点:
document.getElementById('div1')
获取特定ID值得单个节点元素
 document.getElementsByName('name')
获取设置了name属性的元素集合
document.getElementsByTagName('div')
按标签名获取元素集合
document.getElementsByClassName('.class')
按class 样式属性值获取元素集合
document.querySelectorAll('p')
按CSS选择器获取元素集合
document.querySelector('p')
按CSS选择器获取单个元素


property
不是一个API 
而是以一个对象属性的形式进行操作 
可以获取、设置 并且能够渲染到页面上
如:className  style.width   
attribute
通过API进行修改

对比:
property是修改该JS变量对象的属性,这种修改不会体现在html的标签上 而attribute的修改可以真正作用到html的标签上,引起标签的变化
但两者都可能引起DOM重新渲染

2.DOM结构操作

2.1新增/插入节点
步骤是:获取原节点——新建节点——把新建的节点添加进去
<div id="app"></div>
<script>
  let app = document.querySelector('#app')
  let span = document.createElement('span')
  span.innerHTML = '1111'
  app.appendChild(span)
</script>
注:若使用appendChild添加已有节点,则会起到移动的效果
2.2节点关系
childNodes 获取所有子节点
parentNode 获取父节点
firstChild 子节点中第一个
lastChild 子节点中最后一个
nextSibling 下一个兄弟节点
previousSibling 上一个兄弟节点
但在使用childNodes获取子节点时,除了元素节点,还会获取文本节点等。可以通过nodeType来判断是否为文本节点,进行过滤
当然,可以使用 只操作元素节点的 方法:
parentElement 获取父元素
children 获取所有子元素
childElementCount 子标签元素的数量
firstElementChild 第一个子标签
lastElementChild 最后一个子标签
previousElementSibling 上一个兄弟标签
nextElementSibling 下一个兄弟标签
2.3其他节点操作方法
append
(内部)
节点尾部添加新节点或字符串
同时添加多个内容,包括字符串与元素标签
prepend
(内部)
节点开始添加新节点或字符串
before
(外部)
节点前面添加新节点或字符串
after
(外部)
节点后面添加新节点或字符串
replaceWith
将节点替换为新节点或字符串
app.replaceWith(h1)
remove
删除节
  app.remove()
(关于DOM节点的基本操作知识,还是要看后来做得那篇小结:https://blog.nowcoder.net/n/74dd311c22a244bf9e9fd5161617f71f

3.DOM性能优化

DOM操作耗费CPU计算,所以轻易不做DOM操作。
因此,为了提升DOM性能,有两种常用方法:
DOM查询做缓存
将频繁操作改为一次性操作
创建文件片段,文件片段游离在DOM结构之外,对文件片段操作完了
再一次性插入到DOM中

DOM优化的问题:一定要有优化的这个意识!!!方法思想并不难!但在实际操作当中要想着优化!这一点很重要!不仅仅是针对DOM操作的问题,在写代码的时候一定要有这个思想!!!
提醒自己,警示一下

六、BOM

BOM,Browser Object Model,即浏览器对象模型。
浏览器页面初始化时,会在内存创建一个全局对象,用来描述当前窗口的属性和状态,这个全局对象被称为浏览器对象模型。BOM没有官方标准,它最初是Netscape浏览器标准的一部分,也就是说,对于现代浏览器,每个浏览器都有自己的BOM实现方法,所以直接使用BOM会有兼容性问题。
于是,为了利用JavaScript完成交互,现代浏览器几乎都实现了相同的方法和属性,这些方法和属性被称作BOM的方法和属性。
重要知识点:
navigator
包含浏览器相关信息
navigator.userAgent
screen
用户显示屏幕相关属性
screen.width
.height
location
即当前页面的地址
location.href
.host
.protocol
.search
.hash
.pathname
history
即页面的历史记录
history.back
history.forward

七、事件

1.事件绑定

事件绑定一般有三种形式,其中第三种的事件侦听最为常用
HTML绑定
直接在html元素上设置事件处理程序
 <button onclick="show()">
注:绑定函数或方法时一定加上括号
DOM绑定
将事件处理程序以属性的形式绑定到DOM中
但不能以setAttribute的方法设置
const app = document.querySelector('#app')
  app.onclick = function () {
    this.style.color = 'red'
  }
addEventListener
(也是对DOM操作)
可以对同一事件类型设置多个事件处理程序,并按按设置顺序执行
const app = document.getElementById('app1')
  app.addEventListener('click', event => {
    console.log('clicked')
  })
传入三个参数:
参数1——事件类型
参数2——事件处理程序 如果参数2是对象 那么该对象的handleEvent 方法做为事件处理程序执行
参数3——布尔值,指定事件是否在捕获或冒泡阶段执行。
                 true - 事件在捕获阶段执行   false- 默认。事件在冒泡阶段执行
通用的绑定函数:
 通用的事件绑定函数
 function bindEvent(elem, type, fn) {
     elem.addEventListener(type, fn)
 }

const btn1 = document.getElementById('btn1')
bindEvent(btn1, 'click', event => {
    console.log(event.target) // 获取触发的元素
    event.preventDefault() // 阻止默认行为
    alert('clicked')
})


当然,上面的这个事件绑定函数太简单了  再写一个高级点的。能达到同时接受代理绑定和普通绑定的效果:
//普通绑定
const btn1 = document.getElementById('btn1')
bindEvent(btn1, 'click', function (event) {
    // console.log(event.target) // 获取触发的元素
    event.preventDefault() // 阻止默认行为
    alert(this.innerHTML)//this指向bnt1 所以不能使用箭头函数 不然this就指向window了
})

//代理绑定
const div3 = document.getElementById('div3')
bindEvent(div3, 'click', 'a', function (event) {
    event.preventDefault()
    alert(this.innerHTML)
})
我们把上面这个使用函数代码作为目标,来重写bindEvent函数:
function bindEvent(elem, type, selector, fn) {
    if (fn == null)//此时传入的是三个参数 说明第三个参数才是fn 需要进行调整
       {
        fn = selector
        selector = null
    }
    elem.addEventListener(type, event => {
        const target = event.target
        if (selector) {
            // 代理绑定
            if (target.matches(selector)) {
                fn.call(target, event)
            }
        } else {
            // 普通绑定
            fn.call(target, event)
        }
    })
}

2.事件冒泡

要把事件冒泡和事件捕获放在一起理解:
当我们在页面中点击一个元素——
当事件传递到这个元素之后,又会把事件逐成传递回去,直到根元素为止,这个阶段是事件的冒泡阶段。

当为元素绑定事件时,就可以通过设置addEventListener的第三个参数,来指定要在捕获阶段绑定或者换在冒泡阶段绑定  (当然,我们默认的都是冒泡阶段绑定)
理解了冒泡和绑定后,才能来理解event.target和event.currentTarget:
event.target
获取到的时触发事件的标签元素
event.currentTarget
获取到的是发起事件的标签元素
结合例子:

<ul>
  <li>1111</li>
  <li>2222</li>
</ul>

<script>
  const ul = document.querySelector('ul')
  ul.addEventListener('click', () => {
    console.log('clicked')
  })
</script>
在以上代码中,ul标签上绑定了click事件,当点击内部的li标签时,会触发click事件。
此时:   被点击的li标签就是————event.target
             而绑定了click的ul标签就是————event.currentTarget 不管点击哪里 ul都是事件的发起者
说白了,event.currentTarge与具体操作无关,但event.target由具体事件操作决定

阻止冒泡:
event.stopPropagation()
 只阻止当前的事件处理程序
 h2.addEventListener('click', (event) => {
    event.stopPropagation()
    console.log('clicked')
 }
event.stopImmediatePropagation()
阻止事件冒泡并且阻止相同事件的其他事件处理程序被调用
 h2.addEventListener('click', (event) => {
    event.stopImmediatePropagation()
    console.log('clicked')
 }
h2的其他同类型的事件处理程序将不执行,同时阻止冒泡

3.事件代理

事件代理是在冒泡基础上实现的,说白了————对每个子元素挨个绑定太麻烦时,就把事件绑定在父元素上,并通过在父元素上做判断,执行对应的事件处理程序
如:
 <div id="div3">
            <a href="#">a1</a><br>
            <a href="#">a2</a><br>
            <a href="#">a3</a><br>
            <a href="#">a4</a><br>
            <button>加载更多...</button>
        </div>

<script>
  const div = document.querySelector('div3')
  div.addEventListener('click', event => {
      event.preventDefault()  //阻止默认事件
      const target=event.target
      if(target.nodeName === 'A'){ //进行判断
      alert(target.innerHTML)
      }
  })
</script>

八.AJAX

AJAX是一种技术方案,而不是一种新技术,它是基于现有的Internet标准,并且联合使用它们:

1.XMLHttpReequest

XMLHttpRequest 对象用于在后台与服务器交换数据,可以在不重新加载页面的情况下更新网页
也就是,我们使用XMLHttpRequest对象来发送一个Ajax请求。
这个过程可以拆分为一下几步
(1)发送请求到服务器
如需将请求发送到服务器,我们使用 XMLHttpRequest 对象的 open() 和 send() 方法:
open(method,url,async)
xmlhttp.open("GET","ajax_info.txt",true);
规定请求的类型、URL 以及是否异步处理请求。
  • method:请求的类型;GET 或 POST
  • url:文件在服务器上的位置
  • async:true(异步)或 false(同步)
send(string)
xmlhttp.send();
将请求发送到服务器
string:仅用于 POST 请求
接下来,具体讲一讲这几个参数:
(1)method:GET/POST
在计算机网络中,学到过GET和POST是HTTP请求的两种基本方法
GET
xmlhttp.open("GET","/try/ajax/demo_get.php",true);
xmlhttp.send();
POST
xmlhttp.open("POST","/try/ajax/demo_post.php",true);
xmlhttp.send();
如果需要像HTML表单那样POST数据,就需要使用setRequestHeader() 来添加 HTTP 头
再在send()中规定需要发送的数据
xmlhttp.open("POST","/try/ajax/demo_post2.php",true);
xmlhttp.setRequestHeader("Content-type","application/x-www-form-urlencoded");
xmlhttp.send("fname=Henry&lname=Ford");
GET和POST表面上看起来有很多区别
如:
GET产生的URL地址可以被Bookmark,而POST不可以。
GET请求只能进行url编码,而POST支持多种编码方式
GET参数通过URL传递,POST放在Request body中
GET请求在URL中传送的参数是有长度限制的,而POST没有
.......

但在本质上,它们区别却并不大,接下来深入分析一下GET和POST:
首先,GET POST是HTTP协议中的两种发送请求的方法
而HTTP是基于TCP/IP的关于数据如何在万维网中如何通信的协议
补充一下TCP/IP的概念:
TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/网际协议)
是指能够在多个不同网络间实现信息传输的协议簇
TCP/IP协议不仅仅指的是TCP 和IP两个协议,而是指一个由FTP、SMTP、TCP、UDP、IP等协议构成的协议簇
 只是因为在TCP/IP协议中TCP协议和IP协议最具代表性,所以被称为TCP/IP协议。

所以,HTTP的底层是TCP/IP,GET和POST的底层也是TCP/IP
也就是说,GET和POST归根结底都是TCP链接,但他们是不同种类的TCP,因此产生了一些区别,如:GET方法把数据放在url中运输,POST把数据放在Request body中运输等
要给GET加上request body,给POST带上url参数,技术上是完全行的通的。但由于服务器的处理方式不同,服务器未必能读取这些“违规运输”的数据。因此GET和POST在应用过程中体现出了表格中的不同。

除去表格中提到的差别,GET POST还有一个最重要的区别:
GET产生一个TCP数据包;POST产生两个TCP数据包。

也就是说:
对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据)
而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)

 (2)url参数
open() 方法的 url 参数是服务器上文件的地址
该文件可以是任何类型的文件,比如 .txt 和 .xml,或者服务器脚本文件,比如 .asp 和 .php (在传回响应之前,能够在服务器上执行任务)
(3)async参数
AJAX值得就是异步JS和XML,所以XMLHttpRequest 对象用于AJAX中时,async参数必须为——true
true
需要规定处于 onreadystatechange 的就绪状态时执行的函数
xmlhttp.onreadystatechange=function()
{
    if (xmlhttp.readyState==4 && xmlhttp.status==200)
    {
        document.getElementById("myDiv").innerHTML=xmlhttp.responseText;
    }
}
xmlhttp.open("GET","/try/ajax/ajax_info.txt",true);
xmlhttp.send();
false
这时不能编写onreadystatechange 函数 ,并且需将responseText代码放到send()的后面
xmlhttp.open("GET","/try/ajax/ajax_info.txt",false);
xmlhttp.send();
document.getElementById("myDiv").innerHTML=xmlhttp.responseText;
(2)readyState
在发送请求之后,需要执行一些基于相应的任务:
在readyState属性中存有XMLHttpRequest 的状态信息,每当readyState改变,就会触发onreadystatechange 事件:
onreadystatechange
存储函数(或函数名),每当 readyState 属性改变时,就会调用该函数
一般是在该函数中规定当服务器响应已准备就绪所执行的任务
(即,readyState ===4&&status=200)
readyState
存有 XMLHttpRequest 的状态。从 0 到 4 发生变化
  • 0: 请求未初始化 还没有调用send()
  • 1: 载入 已经调用send() 正在发送请求
  • 2: 载入完成 send()方法执行完成 已经接受全部响应内容
  • 3: 交互 正在解析响应内容
  • 4: 请求已完成,且响应内容解析完成,可以在客户端调用了
status
在readyState===4后
2xx:表示成功处理请求 如200
3xx:需要重定向 浏览器直接跳转 如301 302 304
        301资源(网页等)被永久转移到其它URL
        302 临时移动 和301类似 
        304 表示未修改 所请求的资源未改变 因此不会返回任何资源 直接用缓存的
4xx:客户端请求错误 如404 403
5xx:服务端错误
(3)服务器响应
浏览器会获得来自服务器的响应。那么如何获取服务器的响应?还是使用XMLHttpReequest对象的属性获取:
xmlhttp.responseText
获得字符串形式的响应数据
xmlhttp.responseXML
获得 XML 形式的响应数据

在整理完上述的AJAX基础知识后,就可以来手写一个XMLHttpRequest了!

手写XMLHttpReequest
使用GET方法:
const xhr = new XMLHttpRequest()
 xhr.open('GET', '/data/test.json', true)//使用GET方法  true——异步请求
 xhr.onreadystatechange = function () { //相当于写 img.onloasd=... 也是一个回调函数
     if (xhr.readyState === 4) {
         if (xhr.status === 200) {//满足这两个条件时 数据才正常
           alert(xhr.responseText)//responseText是服务器返回的响应

         } else if (xhr.status === 404) {
             console.log('404 not found')
         }
     }
 }
 xhr.send(null)//get请求 不用返回string 返回null就行了
使用POST方法
const xhr = new XMLHttpRequest()
 xhr.open('POST', '/data/test.json', true)//使用POST方法
 xhr.onreadystatechange = function () { 
     if (xhr.readyState === 4) {
         if (xhr.status === 200) {
           alert(xhr.responseText)

         } else {
             console.log('其他情况')
         }
     }
     const postData={
      userName:'zhangsan'
      password:'xxx'
   }
 }
 xhr.send(JSON.stringify(postData)//send一个JSON字符串

2.跨域

跨域,也就是无视同源策略:ajax请求时,在浏览器情况下,要求当前网页和server必须同源,即协议、域名、端口,三者一致。若在非同源的情况下,请求数据,就是跨域。
(但从服务端,可以跨域攻击其他域名的数据)
但,加载图片、css、js,都可以无视同源策略
因此,<img/>可以用于统计打点,可在图片的地址第三方统计服务
<link/><script>可使用CDN CDN一般都是外域
<script>可实现JSONP,也是前端最常用的跨域方式
但:所有跨域必须经过server端的配合和允许,若未经允许,说明浏览器有漏洞
两种最常用的跨域方式:
JSONP
首先,要明确:
当请求访问一个网页时,服务端返回的并不一定是html文件。服务器可以任意拼接动态数据返回,只要符合html格式即可
<script src='###'>同理 返回的不一定是js文件 同样可以返回动态拼接数据
基于以上原理:
<script>可以获得跨域数据
例子:
<script>
window.abc = function (data) {
console.log(data)}
</script>
<script src="http://localhost:8002/jsonp.js?username=xxx&callback=abc"></script>
也可以使用jQuery实现jsonp (封装好了)


CORS
服务器设置:http header
是服务器端的操作 
注:jsonp是一种请求方式,和ajax同级别 

3.手写AJAX

最简单的形式,就是把之前写的XMLHttpRequest直接放进来
function ajax(url,successFn){    
const xhr = new XMLHttpRequest()
 xhr.open('GET', '/data/test.json', true)
 xhr.onreadystatechange = function () { 
     if (xhr.readyState === 4) {
         if (xhr.status === 200) {
           successFn(xhr.responseText)
         } else if (xhr.status === 404) {
             console.log('404 not found')
         }
     }
 }
 xhr.send(null)    
}
想要复杂一点,把promise用起来:
function ajax(url) {
    const p = new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest()
        xhr.open('GET', url, true)
        xhr.onreadystatechange = function () {
            if (xhr.readyState === 4) {
              if (xhr.status === 200) {
                  resolve(
                  JSON.parse(xhr.responseText)//将返回的字符串转换为对象
                  )
            }else if (xhr.status === 404 || xhr.status === 500) 
                        {
               reject(new Error('404 not found'))
                }
            }
        }
        xhr.send(null)
    })
    return p
}

const url = '/data/test.json'
ajax(url)
.then(res => console.log(res))
.catch(err => console.error(err))   

4.常用的AJAX工具

jQuery:有些过时了
$(selector).load(URL,data,callback);
//必需的 URL 参数规定希望加载的 URL
//可选的 data 参数规定与请求一同发送的查询字符串键/值对集合
//可选的 callback 参数是 load() 方法完成后所执行的函数名称
Fetch:一个新的API,类似于XMLHttpRequest ,但更加简洁一些

axios:框架中最常用 但本质用的还是XMLHttpRequestAPI

九.存储

存储一般都是比较cookie localStorage 和 sessionStorage
cookie
(字符串形式)
本身用于浏览器和server通讯
被借用到本地存储 所以存在很多缺点:只有4kb 而且http请求时cookie都要被发送到服务端
(因为这是还没有localStorage 和 sessionStorage)
使用document.cookie进行修改
 localStorage sessionStorage
(key-value形式)
HTML5为存储设计的 最大5M
不会随着http请求被发出去
提供了API:
setItem : localStorage .setItem('a',100)
getItem :   localStorage .getItem('a')
两者区别是:localStorage永久存储 除非手动删除
                    sessionStorage浏览器关闭就自动清空

十.HTTP面试题

搞前端,怎么可以不懂http呢

1.http的状态码

状态码,也就是之前AJAX部分的xhr.status === 200
状态码分类:
1xx 服务器收到请求 100
2xx 成功状态码 200
3xx 重定向状态码 比如你去找人,这家人搬家了,给你留了一个新的地址 302
4xx 客户端错误状态码 404
5xx 服务端错误状态码 500
常见的状态码:
200 成功
301 永久重定向(配合location 浏览器自动处理 下次访问就直接去重定向的地址)
302 临时重定向(配合location 浏览器自动处理 但下次访问还来这个地址找你)
       比如把长地址转为短地址 你去访问短地址 自动跳到长地址
304 资源未被修改 即你之前请求过 服务端说我这边没更新 你那头的还能继续用
404 资源没找到
403 没有权限
500 服务器错误
504 网关超时

2.常见的header

常见的Request Headers:
  • Accept    浏览器端接收的格式
  • Accept-Encoding    浏览器端接收的编码方式
  • Accept-Language    浏览器端接收的语言类型,用于服务器判断多语言
  • Connection    连接方式,如果是keep-alive,且服务端支持,则会复用连接
  • Host    HTTP访问使用的域名
  • User-Agent    客户端标识,,浏览器信息
  • Cookie    客户端存储的cookie字符串
  • Content-type:发送数据的格式 如appplication/json

常见的Response Headers:
  • Connection    连接类型,keep-alive表示复用连接
  • Content-Encoding    内容编码方式,通常是gzip
  • Content-Length    内容的长度,有利于浏览器判断内容是否已经结束
  • Content-Type    内容类型,所有请求网页的都是text/html
  • Set-Cookies    设置cookie,可以存在多个
自定义header:
有时候需要加上header
比如用axios做请求时,可以自定义header:

缓存相关的headers:
Expires 响应过期的日期和时间 Expires: Thu, 01 Dec 2010 16:00:00 GMT
Cache-Control 告诉所有的缓存机制是否可以缓存及哪种类型 Cache-Control: no-cache
Last-Modified 请求资源的最后修改时间 Last-Modified: Tue, 15 Nov 2010 12:45:26 GMT
ETag 请求变量的实体标签的当前值 ETag: “737060cd8c284d8af7ad3082f209582d”


3.Restful API

在学习restful api之前 先要了解http methods
传统methods
用get获取数据
用post提交数据
现在methods
get获取数据
post新建数据
patch/put 更新数据
delete删除数据
而restful api是一种API设计方法
  • 传统API设计:把每个url当作一个功能
  • restful api把每个url当成一个唯一的资源:
尽量不用url参数
(像一个方法)
(是唯一的资源)
用method表示操作类型
传统的:

(此时还是把url当作功能 post就表示提交数据 get就表示获取数据)
restful api:



(此时用method来表示操作类型)

4.*描述http的缓存机制(重要)

首先,我们要了解http为何需要缓存:
就是为了让页面加载更快一些 网络请求的加载比较慢 所以减少网络请求的数量 就可以提高页面加载速度
业务数据不易被缓存,但静态资源可以被缓存,如js css img
http有两种缓存方式:
强制缓存
在Response headers中会有一个Cache-Control
Cache-Control用来控制强制缓存的逻辑 规定缓存的时间

Cache-Control的值有:
  • max-age* 设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)
  • no-cache* 不用强制缓存 交给服务端处理 (可以使用服务端缓存策略—协商缓存)
  • no-store 不要强制缓存 也不要服务端缓存
  • private 表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它)
  • public 表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存
协商缓存
是一个服务端缓存策略:由服务端判断可不可以使用缓存资源,但不是在服务端做缓存
说白了,就是用服务器判断客户端资源是否和服务端一样
如果一样,则返回304 如果不一样 就返回200
(根据资源标识,来判断两边的资源是否一样)
资源标识也在resoponse headers中
  • Last-Modified:资源最后修改时间

  • Etag:资源的唯一标识 类似于人的指纹

两者中优先使用Etag
比较一下强制缓存和协商缓存:
  • 强制缓存就是直接去访问缓存资源,不去访问服务器了
  • 而协商缓存还是要先访问服务器,和服务器商量一下(即比较资源标识),商量后决定是浏览器是使用缓存资源,还是服务器返回新的资源

缓存的整体思路:


接下来分析一下刷新操作对缓存的影响
正常操作
地址栏输入url 跳转链接 前进后退等
强制缓存有效 协商缓存有效
手动刷新
F5 点击刷新按钮 点击菜单刷新
强制缓存失效 协商缓存有效
强制刷新
ctrl+F5
强制缓存失效 协商缓存失效

十一.开发环境

这一部分和真实项目息息相关呢

1.git

常用git命令

(两个图结合着品味)
git的指令众多,只有理解好git的工作原理才能更好的领会更个指令之前的区别:
工作区(working directory)
对于git而言,就是的本地工作目录。工作区的内容会包含提交到暂存区和版本库(当前提交点)的内容,同时也包含自己的修改内容。
暂存区(stage area, 又称为索引区index)
是git中一个非常重要的概念。是我们把修改提交版本库前的一个过渡阶段。查看GIT自带帮助手册的时候,通常以index来表示暂存区。在工作目录下有一个.git的目录,里面有个index文件,存储着关于暂存区的内容。
git add命令将工作区内容添加到暂存区。
本地仓库(local repository)
版本控制系统的仓库,存在于本地。
执行git commit-将暂存区内容提交到仓库之中。
在工作区下面有.git的目录,这个目录下的内容不属于工作区,里面便是仓库的数据信息,暂存区相关内容也在其中
使用merge或rebase将远程仓库副本合并到本地仓库。
远程版本库(remote repository)
与本地仓库概念基本一致,不同之处在于一个存在远程,可用于远程协作,一个却是存在于本地。
通过push/pull可实现本地与远程的交互;
远程仓库副本
可以理解为存在于本地的远程仓库缓存。如
需更新,可通过git fetch/pull命令获取远程仓库内容。
使用fech获取时,并未合并到本地仓库,此时可使用git merge实现远程仓库副本与本地仓库的合并。
可以说:git pull相当于 git fetch + git merge 或 git fetch + git rebase。
下面单独列出几个常用的git 指令

git add .  添加当前目录的所有文件到暂存区
git checkout . / 文件名 撤销修改
git commit -m "xxx名字" 提交暂存区到仓库区
git push origin master  将当前分支推送到origin主机的对应分支
git pull origin master 从服务端下载
git branch 列出所有本地分支
git checkout -b xxx 新建一个分支,并切换到该分支
git checkout  分支名 切换分支
git merge xxx 合并指定分支到当前分支
git status 查看代码的状态 看有没有改动
git diff 看具体改动了哪里
git log 修改记录
git show+修改记录的码  查看某次修改具体改了什么
如果想新建分支B在上面做修改,但却不小心在A上继续做修改了
可以使用git stash 把修改的内容放到一边 再去切换啊新建分支啊 其他操作都搞好啦 再到新分支上 git status 把修改内容释放出来
2.chrome调试js
这个面试可能不会问,但是很实用!


  • elements 
  • console 
  • sources(debugger)-断点 进行调试
  • network -各类型资源的加载
  • application -storage cookies 本地存储的一些

3.抓包

移动端h5页面,查看网络请求就需要使用工具抓包 (win:Fiddle)
1、手机和电脑在一个局域网里
2、把手机代理到电脑上
3、进行抓包
可以进行网址代理

4.webpack

是一个很复杂的工具,配置非常多
但为什么用它?
因为ES6模块化,浏览器暂不支持。ES6的语法,浏览器也并不完全支持。所以必须使用webpack进行打包和转义

那么就先来初始化一个webpack吧!

(这里先放一放,等一下回头搞他)

建一个配置文件:webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    // mode 可选 development 或 production ,默认为后者
    // production 会默认压缩代码并进行其他优化(如 tree shaking)
    mode: 'development',
    entry: path.join(__dirname, 'src', 'index'),
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist')
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                loader: ['babel-loader'],
                include:  path.join(__dirname, 'src'),
                exclude: /node_modules/
            },
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: path.join(__dirname, 'src', 'index.html'),
            filename: 'index.html'
        })
    ],
    devServer: {
        port: 3000,
        contentBase: path.join(__dirname, 'dist'),  // 根目录
        open: true,  // 自动打开浏览器
    }
}


十二.运行环境

运行环境通常指浏览器,移动端的运行环境较多(各种APP),但统一称为浏览器
浏览器的功能就是:下载网页代码,渲染页面,期间执行若干JS
我们优化的目标,就是保证代码在浏览器中稳定且高效

1.页面加载过程

首先,必须要了解网页的加载过程:
加载资源的形式
  • html代码
  • 媒体文件 如图片 视频
  • JS CSS
加载资源的过程
  • 进行DNS解析:由域名 解析得到 真实的IP地址
  • 浏览器根据IP地址向服务器发起http请求
  • 服务器处理http请求,并将资源返回给浏览器
渲染过程
根据返回的资源进行处理:
  • 根据HTML代码生成DOM Tree
  • 根据CSS代码生成CSSOM
  • 将DOM Tree 和 CSSOM整合成Render Tree
(可以理解为在DOM树上挂了很多CSS属性)
  • 根据Render Tree渲染页面
  • 遇到<script>则暂停渲染,优先加载并执行JS代码,完成后再继续渲染
  • 直到把Render Tree渲染完成
通过这个渲染过程,我们可以解答一些问题,如:
为何把CSS放在head中——是为了避免重复过程 这样CSS在DOM树生成之前就摆出来了
为何把JS放在body的最后——把JS放在中间会使渲染暂停
img加载不会阻碍渲染

以及window.load 与 DOMContentLoaded 的区别
window.addEventListener('load',function(){ }
页面全部资源加载完才会执行
document.addEventListener('DOMContentLoaded',function(){ }
DOM渲染完即可执行,此时图片视频可能还没有加载完
(更靠谱)

2.性能优化

性能优化没有正确答案,要尽可能全面,我们可以从一些原则出发:
  • 多使用内存、缓存或其他方法
  • 减少CPU计算量,减少网络加载耗时,用空间换时间
从原则出发,我们可以从以下两点入手进行优化:
让加载更快
  • 减少资源体积:压缩代码(可使用webpack打包压缩)
  • 减少访问次数:合并代码  SSR服务器端渲染(服务端把渲染好的东西直接给浏览器) 缓存
  • 使用更快的网络:CDN
让渲染更快
  • css放head JS放在body最下面
  • 尽早执行JS 用DOMContentLoaded触发
  • 懒加载(图片懒加载 一开始只是加载一个预览图 页面滑动了才加载图片)

  • 对DOM查询进行缓存
  • 把频繁的DOM操作合并到一起
  • 节流throttle 防抖debounce
接下来详细解释一下上面表格中的几个方法:
缓存:

在静态资源后面加上hash后缀,这个后缀是根据文件内容计算得出的
文件内容不变,则hash不变,则url不变
url和文件都不变,就会自动触发http缓存机制,返回304

SSR:
服务端渲染:将网页和数据一起加载,一起渲染 (如vue React)
非SSR(前后端分离):先加载网页 再加载AJAX数据 再渲染数据
手写防抖 debounce:
防抖就是——当监听一个输入框文字变化后触发事件时,使用防抖,在用户输入结束或暂停时触发事件。而不是敲一次键盘触发一次事件
手写防抖:
(这个代码不难理解 但是自己写的时候可能会犯迷糊,要好好琢磨一下)
先用下面这段代码阐述一下作用的机制:
const input1 = document.getElementById('input1')

 let timer = null //定时器
 input1.addEventListener('keyup', function () {
     if (timer) {
         clearTimeout(timer)//clearTimeout用来取消由setTiemout设置的timeout
     }
    timer = setTimeout(() => {
         // 模拟触发 change 事件
         console.log(input1.value)

         // 清空定时器
         timer = null
     }, 500)
})
总的来说,每一次keyup事件,都会生成一个新的timer,如果事件又触发了,那么timer就被清空重来,如果事件不再被触发,那么这个timer就可以继续下去,直到触发change事件

有了这个思路,把上述程序封装一下,就得到了手写防抖的程序:
// 防抖
function debounce(fn, delay = 500) {
    // timer 是闭包中的  不会对外暴露
    let timer = null

    return function () {//返回的是个函数哦!!!
        if (timer) {
            clearTimeout(timer)
        }
        timer = setTimeout(() => {
            fn.apply(this, arguments) //进行绑定
            timer = null
        }, delay)
    }
}
const input1 = document.getElementById('input1')
input1.addEventListener('keyup', debounce(function (e) {
    console.log(e.target)
    console.log(input1.value)
}, 600))
手写节流throttle:
如拖拽一个元素时,要随时拿到元素拖拽的位置,节流就是无论拖拽多块,都以固定的事件间隔触发一次
同样,首先了解一下节流的思路:
(html元素上设置属性 draggable=”true" 即可拖拽啦)
const div1 = document.getElementById('div1')

 let timer = null//还是要定义一个定时器
 div1.addEventListener('drag', function (e) {
     if (timer) {
        return
     }
     timer = setTimeout(() => {
         console.log(e.offsetX, e.offsetY)
         timer = null
     }, 100)
 })
在节流中,如果timer的时间没到,那就return,不管了,如果到了时间,执行函数把timer清空了,那下一次再触发事件时就不return了,重新设置一次timer
理清楚了,接下来就封装啦
// 节流
function throttle(fn, delay = 100) {
    let timer = null //同理,也是闭包

    return function () {
        if (timer) {
            return
        }
        timer = setTimeout(() => {
            fn.apply(this, arguments) //进行绑定
            timer = null
        }, delay)
    }
}

div1.addEventListener('drag', throttle(function (e) {
    console.log(e.offsetX, e.offsetY)
},200))

为什么要用apply呢?
因为如果不用apply绑定的话,throttle(function (e){console.log(e.offsetX, e.offsetY)},200)中的e直接传给throttle这个函数,而真正要用到这个e的参数函数fn却没法获取到它,所以,要使用fn.apply(this, arguments)进行绑定


最后,把防抖和节流做个对比:
  • 防抖策略是将 高频操作合并为一次执行,如果高频操作每次清空定时器,以最后一次操作为主。
  • 节流策略是 将高频操作 按周期执行,一个timeout 周期内执行一次,如果第一个周期执行完,有新的操作进来进行另一个周期。

3.安全

看到了我一直不太会的安全部分了!但这部分其实问题并不多 常见的前端攻击方式是:XSS 和 XSRF
XSS
Cross Site Scripting
跨站脚本攻击
比如:
发表一篇博客,在其中嵌入<script>脚本
用脚本内容获取cookie 发送到自己的服务器
用这种方式就能收割访问者cookie
预防:
将特殊字符通通替换
如把 < 变为 &lt  
> 变为 &gt
这样 <script>就变成了
&lt script&gt
会直接显示在页面上
而不是作为脚本运行
(可以使用xss工具做替换)
XSRF
Cross-site request forgery
跨站请求伪造
你正在购物 看重某个商品 id100
付费接口是/pay?id=100 但没有任何验证
我攻击你
给你发送一封邮件 邮件中隐藏着
<img src = /pay?id=200/>
你一看邮件 就帮我买了
预防:
使用post接口
增加验证(密码 验证码 指纹)


十三.面试真题

总结一下网络上的高频JS面试题

1.var 和 let const的区别

  • var是ES5语法 存在变量提升(但ES6中已经没有了) let const是ES6语法
  • var let是变量 可修改  const是常量 不可修改
  • let const有块状作用域 var没有

2.强制类型转换和隐式类型转换

强制:parseInt parseFloat toString
隐式:if 逻辑运算 ==  +拼接字符串

3.手写深度比较

深度比较要实现的效果是:
即两个引用类型的属性都相等,那么就相等
// 测试
const obj1 = {
    a: 100,
    b: {
        x: 100,
        y: 200
    }
}
const obj2 = {
    a: 100,
    b: {
        x: 100,
        y: 200
    }
}
console.log( isEqual(obj1, obj2) )
基于这个效果,编写代码
// 首先判断是否是对象或数组
function isObject(obj) {
    return typeof obj === 'object' && obj !== null//注意 !==null
}
// 全相等(深度)
function isEqual(obj1, obj2) {
    if (!isObject(obj1) || !isObject(obj2)) {
        // 如果两者中有一个值类型(注意,参与 equal 的一般不会是函数)
               //这个判断可以避免[1,2,3]和{1,2,3}这种情况
        return obj1 === obj2
    }
    if (obj1 === obj2) {
        return true
    }
    // 两个都是对象或数组,而且不相等
    // 首先,先取出obj1和obj2的keys,比较keys的个数 如果属性个数都不同 肯定不等
    const obj1Keys = Object.keys(obj1)//迭代器
    const obj2Keys = Object.keys(obj2)
    if (obj1Keys.length !== obj2Keys.length) {
        return false
    }
    // 以obj1为基准,和 obj2依次递归比较
    for (let key in obj1) {
        // 比较当前 key 的 val 递归!!!
        const res = isEqual(obj1[key],obj2[key])//递归请注意!
        if (!res) {
            return false
        }
    }
    //全部遍历完还没有return false 就可以顺利的return true了
    return true
}

4.split和 join的区别

split:将字符串拆分成数组———— ‘1-2-3’.split('-') //[1,2,3]
join:将数组连接成字符串———— [1,2,3].join('-') //'1-2-3'

5.数组的pop push unshift shift

这个问题看起来很简单,但是我掌握的其实不到位,没有记住一些细节问题,要从以下三个角度来进行思考:

功能是什么 返回值是什么 是否会对原数组造成影响
pop 弹出最后一个数 弹出的最后一个数
push 在尾部加入一个数 改变后的数组长度
unshift 在头部加入一个数 改变后的数组长度
shift 弹出的第一个数 弹出的第一个数
在此基础上,总结一些其他常用的数组API,并分析哪些API是纯函数

纯函数定义:
  • 不改变原数组
  • 返回一个数组
纯函数API有:
 const arr = [10, 20, 30, 40]

// // concat  拼接
 const arr1 = arr.concat([50, 60, 70])
 // map  映射
 const arr2 = arr.map(num => num * 10)
// // filter  过滤 
 const arr3 = arr.filter(num => num > 25)
// // slice 拷贝截取
 const arr4 = arr.slice()
非纯函数就很多了:
 push pop shift unshift
 forEach
 some every
 reduce
 splice

5.slice 和 splice 的区别


功能区别   
参数和返回值
是否为纯函数
slice
切片
slice(start,end)
参数:
start——规定从何处开始选取。
如果是负数,那么它规定从数组尾部开始算起的位置。
end——规定从何时结束选取,为片段结束出的数组下标
(不包含end)
如果没有指定 则一直到数组结尾(
返回值:一个新数组

splice
剪接
arrayObject.splice(index,howmany,item1,.....,itemX)
参数:
index——规定添加/删除项目的位置
howmany——要删除的项目数量 0则不删除
item...——向数组添加的新项目

返回值:包含被删除项目的新数组
不是

6.[10,20,30].map(parseInt)

这道题的考察点有两个:
  • map的参数和返回值
  • parseInt的参数和返回值
const res = [10, 20, 30].map(parseInt)
console.log(res)

// 重点是做好拆解!
[10, 20, 30].map((num, index) => {
    return parseInt(num, index)
})
最后输出结果为:[10,NaN,NaN]
因为parseInt是用来解析字符串,返回一个整数的,其语法为:
parseInt(string, radix)
redix——表示要解析的数字的基数。该值介于 2 ~ 36 之间。

如果省略该参数或其值为 0,则数字将以 10 为基础来解析。
如果它以 “0x” 或 “0X” 开头,将以 16 为基数。
如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN。
在这道题目中,就是因为20和30的index为1和2 所以返回结果NaN

7.ajax请求get和post的区别

(这个之前自己做过一个小节,这里写一下比较常见的比较答案)
get一般用于查询操作 post一般用于用户提交操作
get的参数拼接在url上 post的参数放在request体内
post的安全性更高 易于防止XSRF攻击

8.call和apply的区别

call传入的参数单独传入,apply参数以数组形式传入

9.事件代理是什么

(之前整理过了,这里就复制一下拉到)
事件代理是在冒泡基础上实现的,说白了————对每个子元素挨个绑定太麻烦时,就把事件绑定在父元素上,并通过在父元素上做判断,执行对应的事件处理程序
如:
 <div id="div3">
            <a href="#">a1</a><br>
            <a href="#">a2</a><br>
            <a href="#">a3</a><br>
            <a href="#">a4</a><br>
            <button>加载更多...</button>
        </div>
 
<script>
  const div = document.querySelector('div3')
  div.addEventListener('click', event => {
      event.preventDefault()  //阻止默认事件
      const target=event.target
      if(target.nodeName === 'A'){ //进行判断
      alert(target.innerHTML)
      }
  })
</script>

10.闭包是什么?有何特性?有何影响?

闭包就是函数作为参数传入,或作为返回值被返回
它的重点在于自由变量要在函数定义的地方进行查找,而不是执行的地方
它的影响是:变量会常驻内存,得不到释放。
例子:
 // 自由变量示例 —— 内存会被释放
 let a = 0
 function fn1() {
     let a1 = 100

     function fn2() {
         let a2 = 200

        function fn3() {
             let a3 = 300
             return a + a1 + a2 + a3
         }
         fn3()//fn3执行完毕 a3被释放
     }
     fn2()//fn2执行完毕 a2被释放

 }
 fn1()//fn1执行完毕 a1被释放

// // 闭包 函数作为返回值 —— 内存不会被释放
//例子1 函数作为返回值
 function create() {
     let a = 100
     return function () {
         console.log(a)
     }
 }
 let fn = create()//虽然create被执行完了 但是a被跟着函数一起返回 不能被释放
 let a = 200//没人用它 自然释放了
 fn() // 100
//例子2 函数作为参数
function print(fn) {
    let a = 200//没人理它 释放了
    fn()
}
let a = 100
function fn() {
    console.log(a)
}//这个函数被当作参数 所以a不能释放
print(fn) // 100
总的来说:没人理没人用的变量自然被释放了 可闭包函数的变量不知道什么时候又会被用到 所以还得留着 不能释放

11.如何阻止时间冒泡和默认行为

太简单了 随便截图一下拉到了 

12.查找、添加、删除、移动DOM节点

也太简单了!复制都懒得复制了,回头看就得了
就注意一点:
前面写的删除节点直接是remove
还是用 div1.removeChild(div2) 这种移除子元素的方法比较保险吧

13.如何减少DOM操作

还是之前提过了
缓存DOM查询结果 和 较少DOM插入操作 

14.解释JSONP的原理 为何它不是真正的AJAX?

从浏览器的同源策略和跨域这个角度出发做解释:
jsonp本质还是一个script请求,而没有用到XMLHttpRequest 

15.document load 和 ready (DOMContentLoaded)的区别


16.函数声明和函数表达式的区别

和变量提升非常类似
函数声明 function fn () { }
函数表达式 const fn = function
函数声明会预加载(类似变量提升) 而函数表达式不会
 // 函数声明 sum直接就可以用了 预加载过了
const res = sum(10, 20)
 console.log(res)
 function sum(x, y) {
     return x + y
 }
 // 函数表达式 此时sum不能用
 var res = sum(10, 20)
 console.log(res)
 var sum = function (x, y) {
     return x + y
 }

17.new Object ()和 Object.create() 的区别

{}直接声明等同于new  其原型是Object.prototype
Object.create(null )没有原型,因为它可以使用 Object.create({}) 指定原型 
const obj1 = {
    a: 10,
    b: 20,
    sum() {
        return this.a + this.b
    }
}//有原型

const obj2 = new Object({
    a: 10,
    b: 20,
    sum() {
        return this.a + this.b
    }
})//也有原型

const obj21 = new Object(obj1) // obj1 === obj21为true  但 obj1 === obj2 为false

const obj3 = Object.create(null)//没有原型
const obj4 = new Object() // 虽然是空的{} 但是有原型

const obj5 = Object.create({
    a: 10,
    b: 20,
    sum() {
        return this.a + this.b
    }
})//此时 obj5还是一个空对象{ }  但它的原型为create内{...}的内容

const obj6 = Object.create(obj1)//即 obj6.__proto__===obj.1
说白了 Object.create()  是创建一个空对象实例,对把空对象的原型事指向create内的{ }

18.关于this的场景题

牢记,this只有执行时才知道它指向谁!!!

图中这道题:what1 为 1  what2 为 undefinde(因为此时根本没有指向对象)

19.作用域和自由变量的场景题


上题结果为:444 因为等执行异步操作打印i的时候,i的值已经增到4了

答案是100 ---10----10
在阅读代码时,遇到函数可以直接跳过,不要管,后面遇到函数执行得时候再往回找补

20.判断字符串以字母开头,后面字母数字下划线,长度6-30

答案:

^ 字符串的开头
[ ]框内内容多选
\w 字母数字下划线
{5,29}长度

之前特意总结过一个正则的内容,面试前把那个过一下:https://blog.nowcoder.net/n/fdbcddff74494c1ebfcc448c17679d64
又找了一个老师写的blog 写得好好 :

21.手写字符串trim方法,并保证浏览器兼容性

利用正则表达式来解决,把前后得空白字符串都replace掉

22.获取多个数字的最大值

Math.max Math.min
或者,笨一点:

23.如何用JS实现继承

class继承
prototype继承(已经过时了 不必计较)

24.如何捕获JS中的异常

手动捕获:try...catch
自动捕获:window.onerror  
window.onerror = function(message, source, lineno, colno, error) { ... }
onerror函数会在页面发生js错误时被调用,但:
  • 对于跨域的JS 如CDN的,不会有详细的报错信息
  • 对于压缩的JS 还要配合sourceMap 反差到未压缩代码的行和列

25.什么是JSON

json是一种数据格式,本质是一段字符串(要用双引号" " 而不是JS中的单引号‘ ’)

但它的格式和JS对象结构一致,对JS语言更友好
window.JSON是一个全局对象:
JSON.stringify 将对象转为字符串
JSON.parse 将字符串转为对象

26.获取页面url参数

传统方式
查找location.search
 function query(name) {
     const search = location.search.substr(1) //截取一下
     // search: 'a=10&b=20&c=30'
     const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`, 'i')
          //i 表示大小写不区分
     const res = search.match(reg)
     if (res === null) {
         return null
     }
     return res[2]//res[2]才是要取的参数 
 }
 query('b')//20

新API
URLSearchParam
// URLSearchParams
function query(name) {
    const search = location.search
    const p = new URLSearchParams(search)
    return p.get(name)
}
console.log( query('b') )
也太简单了吧?我落泪了

27.将url参数解析为JS对象

28.手写flatern考虑多层级

如果里面只包含了一层
使用concat就能拍平
因为:concat的参数可以传入数组或单个元素,当传入数组时,添加的还是数组中的元素。所以能拍平一层
但是多层 concat就无能为力 所以基于concat来写flat:
function flat(arr) {
    // 验证 arr 中,还有没有深层数组 [1, 2, [3, 4]]
    const isDeep = arr.some(item => item instanceof Array)
    if (!isDeep) {
        return arr // 已经是 flatern [1, 2, 3, 4]
    }

    const res = Array.prototype.concat.apply([], arr)//利用concat拍平
    return flat(res) // 递归 一次没拍平 就拍第二次
}

const res = flat( [1, 2, [3, 4, [10, 20, [100, 200]]], 5] )
console.log(res)//

29.数组去重

传统方式
 function unique(arr) {
     const res = []
     arr.forEach(item => {
         if (res.indexOf(item) < 0) {
             res.push(item)
         }
     })
     return res
 }
使用Set
// 使用 Set (无序,不能重复)
function unique(arr) {
    const set = new Set(arr)
    return [...set] //注意这里 return是把set打散 打成一个数组输出 要记住这一点
       //因为set本质还是一个集合 如果不打散 输出结果为  }

const res = unique([30, 10, 20, 30, 40, 10])
console.log(res)

30.手写深拷贝

之前也已经写过了

function deepClone(obj = {}) {
    if (typeof obj !== 'object' || obj == null) {
        // obj 是 null ,或者不是对象和数组,直接返回
        return obj
    }

    // 初始化返回结果
    let result
    if (obj instanceof Array) {
        result = []
    } else {
        result = {}
    }

    for (let key in obj) {
        // 保证 key 不是原型的属性
        if (obj.hasOwnProperty(key)) {
            // 递归调用!!!
            result[key] = deepClone(obj[key])
        }
    }

    // 返回结果
    return result
}
记住:Object.assign不是深拷贝 只是一个浅层拷贝

31.RAF requetAnimateFrame

这是一个动画的API 要想保证动画流程 更新频率要60帧/s 即16.67ms更新一次试图
使用RAF的话,浏览器可以自动控制
在后台标签里,RAF就会暂停,节省流量
// 3s 把宽度从 100px 变为 640px ,即增加 540px
// 60帧/s ,3s 180 帧 ,每次变化 3px

const $div1 = $('#div1')
let curWidth = 100
const maxWidth = 640

// // setTimeout
// function animate() {
//     curWidth = curWidth + 3
//     $div1.css('width', curWidth)
//     if (curWidth < maxWidth) {
//         setTimeout(animate, 16.7) // 自己控制时间
//     }
// }
// animate()

// RAF
function animate() {
    curWidth = curWidth + 3
    $div1.css('width', curWidth)
    if (curWidth < maxWidth) {
        window.requestAnimationFrame(animate) // 时间不用自己控制
    }
}
animate()

32.如何优化性能

之前也总结过了
让加载更快
  • 减少资源体积:压缩代码(可使用webpack打包压缩)
  • 减少访问次数:合并代码  SSR服务器端渲染(服务端把渲染好的东西直接给浏览器) 缓存
  • 使用更快的网络:CDN
让渲染更快
  • css放head JS放在body最下面
  • 尽早执行JS 用DOMContentLoaded触发
  • 懒加载(图片懒加载 一开始只是加载一个预览图 页面滑动了才加载图片)

  • 对DOM查询进行缓存
  • 把频繁的DOM操作合并到一起
  • 节流throttle 防抖debounce