笔记|BAT大牛带你横扫初级前端JavaScript面试(第二版)

2,069 阅读23分钟

变量类型和计算

一、题目

1.1 typeof 能判断哪些类型

1.2 何时使用=== 何时使用==

1.3 值类型和引用类型的区别

1.4 手写深拷贝

二、知识点

2.1 变量类型

2.1.1 值类型 vs 引用类型

  1. 值类型

(1)值类型赋值的时候不会相互影响

// 值类型
let a = 100
let b = a
a = 200
console.log(b) // 100

(2)常见值类型

let a // Undefinded
const s = 'abc' // String
const n = 100 // Number
const b = true // Boolean
const s = Symbol('s') // Symbol
  1. 引用类型

(1)引用类型赋值的时候,赋值的是引用地址,会相互影响。引用类型赋值地址主要是考虑计算机性能问题

// 引用类型
let a = { age: 20 }
let b = a
b.age = 21
console.log(a.age) // 21

栈从上往下添加数据,堆从下往上添加数据

(2)常见引用类型

const obj = { x: 100 } // 对象
const arr = [1, 2, 3] // 数组

// 特殊引用类型,指针指向空地址(《JS高级程序设计》中将null归为值类型)
const n = null

// 特殊引用类型,但不用于存储数据,所以没有‘拷贝、复制函数’这一说
function fn() {}

2.1.2 typeof运算符

// 判断所有值类型
let a;                 typeof a // 'undefinded'
const s = 'abc';       typeof s // 'string'
const n = 100;         typeof n // 'number'
const b = true;        typeof b // 'boolean'
const s = Symbol('s'); typeof s // 'symbol'

// 能判断函数
typeof console.log // 'function'
typeof function () {} // 'function'

// 能识别引用类型(不能再识别的更细)
typeof null // 'object'
typeof ['a', 'b'] // 'object'
typeof { x: 100 } // 'object'

2.1.3 深拷贝

/**
* 深拷贝
*/
const obj1 = {
  name: 'Mary',
  age: 21,
  address: {
    city: 'Beijing'
  },
  arr: [1, 2, 3]
}
const obj2 = deepClone(obj1)
obj2.address.city = 'Shanghai'
obj2.arr[0] = 0
console.log(obj1, obj2)

function deepClone(obj = {}) {
  if (typeof obj !== 'object' || obj == null) {
    return obj
  }
  // 初始化返回结果
  let result
  if (obj instanceof Array) {
    result = []
  } else {
    result = {}
  }
  for(let key in obj) {
    if (obj.hasOwnProperty(key)) { // 保证key不是原型的属性
      result[key] = deepClone(obj[key]) // 递归调用
    }
  }
  return result
}

2.2 变量计算-类型转换

2.2.1 字符串拼接

const a = 100 + 10    // 110
const b = 100 + '10'  // '10010'
const c = true + '10' // 'true10'

2.2.2 == 运算符

100 == '100'      // true
0 == ''           // true
0 == false        // true
false == ''       // true
null == undefined // true
// 除了 == null之外,其他一律都用 === 
const obj = { x: 100 }
if (obj.a == null) {}
// 相当于:
// if (obj.a === null || obj.a === undefined)

2.2.3 if语句和逻辑运算

  • truly变量:!!a === true的变量
  • falsely变量:!!a === false的变量
// 以下是falsely变量。除此之外都是truly变量
!!0 === false
!!NaN === false
!!'' === false
!!null === false
!!undefined === false
!!false === false
  1. if语句
// if语句判断的不是true还是false,而是truly变量或falsely变量
// truly变量
const a = true
if (a) {}
const b = 100
if (b) {}

// falsely变量
const c = ''
if (c) {}
const d = null
if (d) {}
let e
if (e) {}
  1. 逻辑判断
// 逻辑判断也是判断truly变量和falsely变量
console.log(10 && 0) // 0
console.log('' || 'abc') // 'abc'
console.log(!window.abc) // true

三、题目解析

3.1 typeof 能判断哪些类型

  • 识别所有值类型
  • 识别函数
  • 判断是否是引用类型(不可再细分)

3.2 何时使用=== 何时使用==

==会转换数据类型,尝试使两端相等,因此最好使用===

// 除了 == null 之外,其他一律都用 === 
const obj = { x: 100 }
if (obj.a == null) {}
// 相当于:
// if (obj.a === null || obj.a === undefined)

3.3 值类型和引用类型的区别

面试题:

const obj1 = { x: 100, y: 200 }
const obj2 = obj1
let x1 = obj1.x
obj2.x = 101
x1 = 102
console.log(obj1) // { x: 101, y: 200 }
  • 存储位置不一样
    • 值类型存放在栈内存中
    • 引用类型在堆内存中开辟一个空间,存储具体的值,而在栈内存中存储的是指向该值的地址

3.4 手写深拷贝

  • 注意判断值类型和引用类型
  • 注意判断是数组还是对象
  • 递归

具体代码见2.1.3 深拷贝

原型和原型链

一、题目

1.1 如何准确判断一个变量是不是数组?

1.2 手写一个简易的jQuery,考虑插件和扩展性

1.3 class的原型本质,怎么理解?

二、知识点

2.1 class和继承

2.1.1 class

  • constructor
  • 属性
  • 方法
// 类
class Student {
  constructor(name, number) {
    this.name = name
    this.number = number
  }
  sayHi () {
    console.log(`姓名:${this.name} 学号:${this.number}`)
  }
}
// 通过类 new 对象/实例
const tom = new Student('Tom', 21)
console.log(tom.name)
console.log(tom.number)
tom.sayHi()

2.1.2 继承

  • extends
  • super
  • 扩展和重写方法
// Student继承了People,People继承了Object
// 父类
class People {
  constructor(name) {
    this.name = name
  }
  eat () {
    console.log(`${this.name} eat something`)
  }
}
// 子类
class Student extends People {
  constructor(name, number) {
    super(name);
    this.number = number
  }
  sayHi () {
    console.log(`姓名:${this.name} 学号:${this.number}`)
  }
}
// 子类
class Teacher extends People {
  constructor(name, major) {
    super(name)
    this.major = major
  }
  teach () {
    console.log(`${this.name} 教授:${this.major}`)
  }
}
// 通过类 new 对象/实例
const tom = new Student('Tom', 21)
console.log(tom.name)
console.log(tom.number)
tom.sayHi()
tom.eat()

// 通过类 new 对象/实例
const teacherWang = new Teacher('Wang', 'Math')
console.log(teacherWang.name)
console.log(teacherWang.major)
teacherWang.teach()
teacherWang.eat()

2.2 类型判断 instanceof

instanceof 可以判断变量属于哪个class或构造函数

tom instanceof Student // true
tom instanceof People // true
tom instanceof Object // true

[] instanceof Array // true
[] instanceof Object // true

{} instanceof Object // true

2.3 原型和原型链

2.3.1 原型

// class实际上是函数,可见是语法糖
typeof People // 'function'
typeof Student // 'function'

// 隐式原型和显示原型
console.log(tom.__proto__)
console.log(Student.prototype)
console.log(tom.__proto__ === Student.prototype)

原型关系:

  • 每个class都有显示原型prototype
  • 每个实例都有隐式原型__proto__
  • 实例的__proto__指向对应class的prototype

基于原型的执行规则

获取属性tom.name或执行tom.sayHi()时,

  1. 先在自身属性和方法寻找
  2. 如果找不到则自动去__proto__中寻找

2.3.2 原型链

console.log(Student.prototype.__proto__)
console.log(People.prototype)
console.log(People.prototype === Student.prototype.__proto__)

再看instanceof:

tom instanceof People tom的隐式原型沿着原型链,一路往上查找,能否找到People的显示原型,若能找到,则返回true

重要提示!

  1. class是ES6语法规范,由ECMA委员会发布
  2. ECMA只规定语法规则,即我们代码的书写规范,不规定如何实现
  3. 以上实现方式否是V8引擎的实现方式,也是主流的

三、题目解答

3.1 如何准确判断一个变量是不是数组?

  • a instanceof Array

3.2 手写一个简易的jQuery,考虑插件和扩展性

class jQuery {
  constructor(selector) {
    const result = document.querySelectorAll(selector)
    const length = result.length
    for(let i = 0;i < length; i++) {
      this[i] = result[i]
    }
    this.length = length
    this.selector = selector
  }
  get(index) {
    return this[index]
  }
  each(fn) {
    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)
    })
  }
}
// 扩展方式1:插件
jQuery.prototype.dialog = function (info) {
  alert(info)
}

// 扩展方式2:'造轮子'
class myJQuery extends jQuery {
  constructor(selector) {
    super(selector);
  }
  // 扩展自己的方法
  addClass(className) {

  }
  style (data) {

  }
}

const $p = new jQuery('p')
$p.get(0)
$p.each((elem) => console.log(elem.innerHTML))
$p.on('click', () => alert('clicked'))
$p.dialog('sth')

3.3 class的原型本质,怎么理解?

  • 原型和原型链的图示
    • 每个class都有显示原型prototype
    • 每个实例都有隐式原型__proto__
    • 实例的__proto__指向对应class的prototype
  • 属性和方法的执行规则
    • 先在自身属性和方法寻找
    • 如果找不到则自动去__proto__中寻找

作用域和闭包

一、题目

1.1 this的不同应用场景,如何取值?

1.2 手写bind函数

1.3 实际开发中闭包的应用场景,举例说明

1.4 创建10个a标签,点击的时候弹出来对应的序号

二、知识点

2.1 作用域和自由变量

2.1.1作用域

作用域代表了一个变量的合法使用范围,如上图红框所示,若变量在所在红框之外是无法使用的。 作用域分为:

  • 全局作用域:代码中定义一个变量,不受函数约束,在全局均可使用,如window对象,document对象
  • 函数作用域:任何定义在函数体内的变量或者函数都将处于函数作用域中,跳出该作用域外,变量无法使用
  • 块级作用域(ES6新增)
// 块级作用域
if (true) {
    let a = 1
}
console.log(a) // 报错

2.1.2 自由变量

  • 一个变量在当前作用域没有定义,但被使用了
  • 向上级作用域,一层一层依次寻找,直至找到为止
  • 如果到了全局作用域都没找到,则报错 xx is not defined

如对于function fn3() {}中,a,a1,a2均属于自由变量

2.2 闭包

作用域应用的特殊情况,有两种表现:

  1. 函数作为参数被传递
// 函数作为参数
function print(fn) {
  const a = 200
  fn()
}
const a = 100
function fn() {
  console.log(a)
}
print(fn) // 100

  1. 函数作为返回值被返回
// 函数作为返回值
function create() {
  const a = 100
  return function () {
    console.log(a)
  }
}
const fn = create()
const a = 200
fn() // 100

所有的自由变量的查找,是在函数【定义】的地方,向上级作用域查找,不是在执行的地址!!!

2.3 this

2.3.1 this使用场景有:

  • 作为普通函数:this指向全局对象(浏览器是window),严格模式中,表现为undefined
  • 使用call apply bind:this指向绑定的对象上
  • 作为对象方法被调用:this指向调用函数的对象
  • 在class方法中调用:this指向对应的实例
  • 箭头函数:所有的箭头函数都没有自己的this,都指向上级作用域(箭头函数本身不支持this)

2.3.2 this取什么值,是在函数【执行】的时候确定的,不是在函数定义的时候

function fn1() {
  console.log(this)
}
fn1() // window

fn1.call({ x: 100 }) // { x: 100 }

const fn2 = fn1.bind({ x: 200 })
fn2() // { x: 200 }

const zhangsan = {
  name: '张三',
  sayHi() {
    console.log(this) // this即当前对象
  },
  wait() {
    setTimeout(function() {
      console.log(this) // function() {}是由setTimeout触发了执行(作为一个普通函数被执行),而非作为zhangsan的对象方法被执行,因此this === window
    }, 1000)
  },
  waitAgain() {
    setTimeout(() => {
      console.log(this) // this即当前对象
    }, 1000)
  }
}
zhangsan.sayHi()
zhangsan.wait()
zhangsan.waitAgain()

class People {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
  sayHi() {
    console.log(this)
  }
}
const zhangsan = new People('张三', 20)
zhangsan.sayHi() // zhangsan 对象

三、题目解答

3.1 this的不同应用场景,如何取值?

见2.3.1

3.2 手写bind函数

Function.prototype.myBind = function () {
  // 将参数拆解为数组
  const args = Array.prototype.slice.call(arguments)
  // 获取this(args数组第一项)
  const t = args.shift()
  // fn1.bind(...)中的fn1
  const self = this
  // 返回一个函数
  return function () {
    return self.apply(t, args)
  }
}
function fn1(a, b, c) {
  console.log('this ', this)
  console.log(a, b, c)
  console.log('this is fn1')
}
const fn2 = fn1.myBind({ x: 100 }, 1, 2, 3)
fn2()

3.3 实际开发中闭包的应用场景,举例说明

  • 隐藏数据
// 闭包隐藏数据,做一个简单的cache工具,只提供API
function createCache() {
  const data = {}
  return {
    set: function (key ,val) {
      data[key] = val
    },
    get: function (key, val) {
      return data[key]
    }
  }
}
const c = createCache()
c.set('a', 100)
console.log(c.get('a'))

3.4 创建10个a标签,点击的时候弹出来对应的序号

// i是全局变量,a的点击事件发生时,i的值已经变为10
// let i, a
// for (i = 0; i < 10; i++) {
//   a = document.createElement('a')
//   a.innerHTML = i + '<br>'
//   a.addEventListener('click', function(e){
//     e.preventDefault()
//     alert(i) // 10
//   })
//   document.body.appendChild(a)
// }

// 修改定义i的位置,构成块级作用域
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) // 10
  })
  document.body.appendChild(a)
}

异步

一、题目

1.1 同步和异步的区别是什么?

1.2 手写用Promise加载一张图片

1.3 前端使用异步的场景有哪些?

1.4 说出以下的打印顺序

// setTimeout 笔试题
console.log(1)
setTimeout(function() {
    console.log(2)
}, 1000)
console.log(3)
setTimeout(function() {
    console.log(4)
}, 0)
console.log(5)

二、知识点

2.1 单线程和异步

  • JS是单线程语言,只能同时做一件事
  • 浏览器和nodejs已支持JS启动进程,如Web Worker
  • JS和DOM渲染共用同一个线程,因为JS可修改DOM结构
  • 遇到等待(网络请求,定时任务)不能卡住
  • 因此,需要异步(为了解决单线程可能发生等待的问题)
  • 异步基于回调callback函数形式
// 异步 (callback 回调函数)
console.log(100)
setTimeout(() => {
    console.log(200)
}, 1000)
console.log(300)

// 同步
console.log(100)
alert(200)
console.log(300)

异步和同步

  • 基于JS是单线程语言
  • 异步不会阻塞代码执行
  • 同步会阻塞代码执行

2.2 异步的应用场景

  • 网络请求,如ajax,图片加载
// ajax
console.log('start')
$.get('./data1.json', function(data1) {
    console.log(data1)
})
console.log('end')

// 图片加载
console.log('start')
let img = document.createElement('img')
img.onload = function () {
    console.log('loaded')
}
img.src = '/xxx.png'
console.log('end')
  • 定时任务,如setTimeout
// setTimeout
console.log(100)
setTimeout(() => {
    console.log(200)
}, 1000)
console.log(300)

// setIntelval
console.log(100)
setIntelval(() => {
    console.log(200)
}, 1000)
console.log(300)

2.3 callback hell和Promise

callback hell 回调地狱

// 获取第一份数据
$.get(url1, (data1) => {
  console.log(data1)

  // 获取第二份数据
  $.get(url2, (data2) => {
    console.log(data2)

      // 获取第三份数据
    $.get(url3, (data3) => {
      console.log(data3)

      // 还可能获取更多数据
    })
  })
})

Promise解决回调地狱

function getData(url) {
  return new Promise((resolve, reject) => {
    $.ajax({
      url,
      success(data) {
        resolve(data)
      },
      error(err) {
        reject(err)
      }
    })
  })
}
getData(url1).then(data1 => {
  console.log(data1)
  return getData(url2)
}).then(data2 => {
  console.log(data2)
  return getData(url3)
}).then(data3 => {
  console.log(data3)
}).catch(err => console.error(err))

三、题目解答

3.1 同步和异步的区别是什么?

  • 基于JS是单线程语言
  • 异步不会阻塞代码执行
  • 同步会阻塞代码执行

3.2 手写用Promise加载一张图片

function loadImg(url) {
  return new Promise((resolve, reject) => {
    const image = new Image();
    image.onload = () => {
      resolve(image);
    };
    image.onerror = () => {
      reject(new Error('图片加载失败:' + url));
    };
    image.src = url;
  });
}
const url = '*'
loadImg(url).then(img => {
  console.log(img.width)
}).catch(e => {
  console.log(e)
})

const url1 = '**'
const url2 = '***'
loadImg(url1).then(img1 => {
  console.log(img1.width)
  return img1 // 普通对象
}).then(img1 => {
  console.log(img1.height)
  return loadImg(url2) // Promise 实例
}).then(img2 => {
  console.log(img2.width)
  return img2
}).then(img2 => {
  console.log(img2.height)
}).catch(e => {
  console.log(e)
})

3.3 前端使用异步的场景有哪些?

  • 网络请求,如ajax,图片加载
  • 定时任务,如setTimeout

3.4 说出以下的打印顺序

1 3 5 4 2

// setTimeout 笔试题
console.log(1)
setTimeout(function() {
    console.log(2)
}, 1000)
console.log(3)
setTimeout(function() {
    console.log(4)
}, 0)
console.log(5)

JS Web API:DOM操作

DOM(Document Object Model)文档对象模型

一、题目

1.1 DOM是哪种数据结构

1.2 DOM操作的常用API

1.3 attr和property的区别

1.4 一次性插入多个DOM节点,考虑性能

二、知识点

2.1 DOM本质

本质上是一棵树

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
  <div>
    <p>this is p</p>
  </div>
</body>
</html>

2.2 DOM节点操作

2.2.1 获取DOM节点

const div1 = document.getElementById('div1') // 元素
const divList = document.getElementsByTagName('div') // 集合
const containerList = document.getElementsByClassName('container') // 集合
const pList = document.querySelectorAll('p') // 集合

2.2.2 attribute

const pList = document.querySelectorAll('p')
const p = pList[0]
p.setAttribute('data-name', 'type')
console.log(p.getAttribute('data-name'))

p.setAttribute('style', 'color: red;')
console.log(p.getAttribute('style'))

2.2.3 property

JS通过property这种形式,修改页面的DOM渲染结构

const pList = document.querySelectorAll('p')
const p = pList[0]
p.style.width = '100px'
p.className = 'red-text'
console.log(p.nodeName) // P
console.log(p.nodeType) // 1
  • attribute:
    • 是HTML标签上的某个属性,如id、class、value等以及自定义属性。
    • 它的值只能是字符串
    • 关于这个属性一共有三个相关的方法,setAttribute、getAttribute、removeAttribute;
    • 修改HTML属性,会改变HTML结构
  • property:
    • 是js获取的DOM对象上的属性值,比如a,你可以将它看作为一个基本的js对象。
    • 这个节点包括很多property,比如value,className以及一些方法onclik等方法。
    • 修改对象属性,不会体现到HTML结构中

以上两者都有可能引起DOM重新渲染

2.3 DOM结构操作

2.3.1 新增/插入节点

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
  <div id="div1" style="border-bottom: 1px solid red;">
    <p id="p1">this is p1</p>
    <p>this is p2</p>
    <p>this is p3</p>
  </div>
  <div id="div2">
    <p>this is p in div2</p>
  </div>
<script>
const div1 = document.getElementById('div1')
const div2 = document.getElementById('div2')

// 新建节点
const newP = document.createElement('p')
newP.innerHTML = 'this is new p'

// 插入节点
div1.appendChild(newP)

// 移动节点
const p1 = document.getElementById('p1')
div2.appendChild(p1)
</script>
</body>
</html>

2.3.2 获取子元素列表,获取父元素

// 获取父元素
console.log(p1.parentNode)
// 获取子元素列表
const div1ChildeNodes = div1.childNodes
console.log('div1ChildeNodes: ', div1ChildeNodes)
const div1ChildeNodesP = Array.prototype.slice.call(div1ChildeNodes).filter(item => item.nodeType === 1) // 文本节点的nodeType === 3
console.log('div1ChildNodesP: ', div1ChildeNodesP)

2.3.3 删除子元素

// 删除子元素节点
div1.removeChild(div1ChildeNodesP[0])

2.4 DOM性能

DOM操作非常‘昂贵’,避免频繁的DOM操作:

2.4.1 对DOM查询做缓存

// 不缓存DOM查询结果
for(let i = 0; i < document.getElementsByTagName('p').length; i++) {
  // 每次循环,都会计算length,频繁进行DOM操作
}

// 缓存DOM查询结果
const pList = document.getElementsByTagName('p')
const length = pList.length
for(let i = 0; i < length; i++) {
  // 缓存length,只进行一次DOM查询
}

2.4.2 将频繁操作改为一次性操作

const list = document.getElementById('list')

// 方法1:频繁操作
for(let i = 0; i < 10; i++) {
  const li = document.createElement('li')
  li.innerHTML = `list item ${i}`
  list.appendChild(li)
}

// 方法2:一次性操作
// 创建一个文档片段,此时还没有插入到DOM树中
const frag = document.createDocumentFragment()
// li元素插入到文档片段
for(let i = 10; i < 20; i++) {
  const li = document.createElement('li')
  li.innerHTML = `list item ${i}`
  frag.appendChild(li)
}
// 都完成之后,再将文档片段插入到DOM树中
list.appendChild(frag)

三、题目解答

3.1 DOM是哪种数据结构

树(DOM树)

3.2 DOM操作的常用API

  • DOM节点操作
  • DOM结构操作
  • attribute和property

3.3 attr和property的区别

  • property:修改对象属性,不会体现到HTML结构中
  • attribute:修改HTML属性,会改变HTML结构
  • 两者都有可能引起DOM重新渲染

(优先使用property)

3.4 一次性插入多个DOM节点,考虑性能

const list = document.getElementById('list')
// 创建一个文档片段,此时还没有插入到DOM树中
const frag = document.createDocumentFragment()
// li元素插入到文档片段
for(let i = 10; i < 20; i++) {
  const li = document.createElement('li')
  li.innerHTML = `list item ${i}`
  frag.appendChild(li)
}
// 都完成之后,再将文档片段插入到DOM树中
list.appendChild(frag)

JS-Web-API-BOM

一、题目

1.1 如何识别浏览器的类型

1.2 分析拆解URL各部分

二、知识点

2.1 navigator

const ua = navigator.userAgent
const isChrome = ua.indexOf('Chrome')
console.log(isChrome)

2.2 screen

console.log(screen.width)
console.log(screen.height)

2.3 location

location.href
location.protocol // 'http:' 'https:'
location.host // location.hostname + location.port
location.pathname
location.search
location.hash

2.4 history

history.back()
history.forward()

JS-Web-API-事件

一、题目

1.1 编写一个通用的事件监听函数

1.2 描述事件冒泡的流程

1.3 无限下拉的图片列表,如何监听每个图片的点击?

二、知识点

2.1 事件绑定

// 点击事件
const btn = document.getElementById('btn1')
btn.addEventListener('click', event => {
  console.log('clicked')
})
// 通用的绑定函数(简版
<body>
<a id="link1" href="https://www.baidu.com">link</a>
<script>)
function bindEvent(elem, type, fn) {
  elem.addEventListener(type, fn)
}
const a = document.getElementById('link1')
bindEvent(a, 'click', e => {
  console.log(event.target) // 获取触发的元素
  e.preventDefault() // 阻止默认行为
  alert('clicked')
})
</script>
</body>
// 通用的绑定函数(支持普通绑定和事件代理)
<body>
<a id="link1" href="https://www.baidu.com">link</a>
<div id="div3">
  <a href="#">a1</a>
  <a href="#">a2</a>
  <a href="#">a3</a>
  <button>加载更多</button>
</div>
<script>
function bindEvent(elem, type, selector, fn) {
  if (fn == null) {
    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)
    }
  })
}
const link1 = document.getElementById('link1')
bindEvent(link1, 'click', function(e) {
  e.preventDefault() // 阻止默认行为
  console.log(this.innerHTML)
})
const div3 = document.getElementById('div3')
bindEvent(div3, 'click', 'a', function(event) {
  event.preventDefault()
  console.log(this.innerHTML)
})
</script>
</body>

2.2 事件冒泡

子元素身上的事件,会冒泡到父元素身上,可用event.stopPropagation() 阻止冒泡行为

<body>
<div id="div1">
  <p id="p1">激活</p>
  <p id="p2">取消</p>
  <p id="p3">取消</p>
  <p id="p4">取消</p>
</div>
<div id="div2" style="border-top: 1px solid red;">
  <p id="p5">取消</p>
  <p id="p6">取消</p>
</div>
<script>
function bindEvent(elem, type, fn) {
  elem.addEventListener(type, fn)
}
const p1 = document.getElementById('p1')
const body = document.body
bindEvent(p1, 'click', event => {
  event.stopPropagation() // 阻止冒泡
  console.log('激活')
})
bindEvent(body, 'click', event => {
  console.log('取消')
})
</script>

2.3 事件代理(事件委托)

针对瀑布流这种子元素个数很多,或者内部结构比较复杂的情况下,将本来加在子元素身上的事件,加在其父元素身上。事件代理的好处有:

  1. 代码简洁,比如不用for循环为子元素添加事件,js新生成的子元素不用新为其添加事件
  2. 减少浏览器内存占用(相对于只为一个父元素绑定监听事件,为每个子元素都绑定监听事件更耗费内存)

但是,不要滥用

<body>
<div id="div3">
  <a href="#">a1</a>
  <a href="#">a2</a>
  <a href="#">a3</a>
  <button>加载更多</button>
</div>
<script>
function bindEvent(elem, type, fn) {
  elem.addEventListener(type, fn)
}
const div3 = document.getElementById('div3')
bindEvent(div3, 'click', event => {
  event.preventDefault()
  const target = event.target
  if (target.nodeName === 'A') {
    console.log(target.innerHTML)
  }
})
</script>
</body>

三、题目解答

3.1 编写一个通用的事件监听函数

见2.1

3.2 描述事件冒泡的流程

基于DOM树形结构,事件会顺着触发元素往上冒泡

应用场景:代理

3.3 无限下拉的图片列表,如何监听每个图片的点击?

  1. 利用事件代理,
  2. 用e.target获取触发元素,
  3. 用matches来判断触发元素是否在限制的触发条件内

JS-Web-API-Ajax

一、题目

1.1 手写一个简易的ajax

1.2 跨域的常用实现方式

二、知识点

2.1 XMLHttpRequest

2.1.1 get请求

// get 请求
const xhr = new XMLHttpRequest()
xhr.open('GET', '/data/test.json', true) // true 异步请求
xhr.onreadystatechange = function () {
  if (xhr.readyState === 4) {
    if (xhr.status === 200) {
      console.log(JSON.parse(xhr.responseText))
    } else {
      console.log('其他情况:', xhr.status)
    }
  }
}
xhr.send(null)

2.1.2 POST请求

// POST 请求
const xhr = new XMLHttpRequest()
xhr.open('POST', '/login', true)
xhr.onreadystatechange = function () {
  if (xhr.readyState === 4) {
    if (xhr.status === 200) {
      console.log(JSON.parse(xhr.responseText))
    } else {
      console.log('其他情况:', xhr.status)
    }
  }
}
const postData = {
  username: 'zhangsan',
  password: 'xxx'
}
xhr.send(JSON.stringify(postData))

2.1.3 xhr.readyState

  • 0-(未初始化)还没有调用send()方法
  • 1-(载入)已调用send()方法,正在发送请求
  • 2-(载入完成)send()方法执行完成,已经接收到全部响应内容
  • 3-(交互)正在解析响应内容
  • 4-(完成)响应内容解析完成,可以在客户端调用

2.1.4 xhr.status

  • 2xx - 表示成功处理请求,如200
    • 200 请求成功,对GET和POST请求的应答文档跟在后面
  • 3xx - 需要重定向,浏览器直接跳转,如301,302,304
    • 301 永久重定向,被请求的资源已永久移动到新位置,并且将来任何对此资源的引用都应该使用本响应返回的若干个 URI 之一。
    • 302 临时重定向,请求的资源被暂时的移动到了由Location 头部指定的 URL上。浏览器会重定向到这个URL
    • 304 未改变,说明无需再次传输请求的内容,也就是说可以使用缓存的内容
  • 4xx - 客户端请求错误,如400,403,404
    • 400 Bad Request,响应状态码表示由于语法无效,服务器无法理解该请求
    • 403 Forbidden, 资源不可用,服务器理解客户的请求,但拒绝处理它。通常由于服务器上文件或目录的权限设置导致。
    • 404 Not Found,请求失败,请求所希望得到的资源未被在服务器上发现
  • 5xx - 服务器端错误
    • 500 Internal Server Error,服务器遇到了不知道如何处理的情况。

2.2 跨域:同源策略,跨域解决方案

2.2.1 同源策略

  1. ajax请求
  • ajax请求时,浏览器要求当前网页和server必须同源(安全)
  • 同源:协议、域名、端口,三者必须一致。如下就不属于同源:前端(http:///a.com:8080/),server(a.com/api/xxxx)
  1. 加载图片、css、js
  • 可无视同源策略:
    <img src=跨域的图片地址 />
    <link src=跨域的css地址 />
    <script src=跨域的js地址></script>
    
  • 加载图片、css、js可无视同源策略,实现跨域,它们可实现的功能有:
    • <img />可用于统计打点,可使用第三方统计服务
    • <link /> <script>可使用CDN,CDN一般都是外域
    • <script>可实现JSONP

2.2.2 跨域

  • 所有的跨域,都必须经过server端的允许和配合
  • 未经server端允许就实现跨域,说明浏览器有漏洞,危险信号
// CORS - 服务器设置http header
//第二个参数填写允许跨域的域名称,不建议直接写'*'
response.setHeader('Access-Control-Allow-Origin', 'http://localhost:8011');
response.setHeader('Access-Control-Allow-Headers', 'X-Requested-With');
response.setHeader('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
// 接收跨域的cookie
response.setHeader('Access-Control-Allow-Credentials', 'true');

2.2.3 JSONP

访问https://imooc.com,服务端一定返回一个html文件吗?- 不是的,服务器可以任意动态拼接数据返回,只要符合html格式要求。同理于<script src="https://imooc.com/getData.js"></script>

<script>可绕过跨域限制,服务器可以任意动态拼接数据返回。所以,<script>就可以获得跨域的数据,只要服务端愿意返回

JSONP的实现原理:创建一个回调函数,然后在远程服务上调用这个函数并且将JSON 数据形式作为参数传递,完成回调。

// 前端jsonp.html
<script>
  window.callback = function(data) {
    console.log(data)
  }
</script>
<script src='http://127.0.0.1:5501/jsonp.js'></script>

// 服务jsonp.js
callback(
  { name: 'zhangsan' }
)

三、题目解答

3.1 手写一个简易的ajax

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 {
          reject('其他情况' + xhr.status)
        }
      }
    }
    xhr.send(null)
  })
  return p
}
ajax('/data/test.json').then(res => {
  console.log(res)
}).catch(err => {
  console.log(err)
})

3.2 跨域的常用实现方式

  1. JSONP
  2. CORS

JS-Web-API 存储

一、题目

1.1 描述cookie、localStorage、sessionStorage的区别

二、知识点

2.1 cookie

  • 本身是用于浏览器和server通讯
  • 被‘借用’到本地存储来
  • 可用document.cookie = 'val=xxxxx'来新增或修改cookie

cookie的缺点:

  1. 存储大小,最大仅有4kb
  2. http请求时会一起发送到服务端,增加请求数据量
  3. 只能用可用document.cookie = '***'来修改,太过简陋

2.2 localStorage、sessionStorage

  • HTML5专门为存储而设计,最大可存5M(针对每个域名)
  • API简单易用 setItem getItem
  • 不会随着http请求被发送出去

localStorage、sessionStorage的区别:

  1. localStorage数据会永久存储,除非代码或手动删除
  2. sessionStorage数据只存在于当前会话,浏览器关闭则清空
  3. 一般用localStorage会更多一些

三、题目解答

3.1 描述cookie、localStorage、sessionStorage的区别

  1. 容量
  2. API易用性
  3. 是否会跟随http请求发送出去

开发环境

一、Git

  • 最常用的代码版本管理工具
  • 大型项目需要多人协作开发,必须熟用git
  • Mac OS自带git命令,windows可去官网下载安装
  • git服务端常见的有:github,coding.net等

1.1 常用git命令

git命令 作用
git init 在目录中创建新的 Git 仓库
git clone [url] 拷贝一个 Git
git add index.html hello.js 将文件添加到缓存
git add . 将所有文件添加到缓存
git status 查看在上次提交之后是否有修改
git diff 查看执行 git status 的结果的详细信息
git commit -m "提交注释" 将缓存区内容添加到仓库中
git branch [branchname] 创建分支
git checkout [branchname] 切换到需要修改的分支
git checkout -b [branchname] 创建新分支并立即切换到该分支下
git log 查看提交历史
git push origin master 上传代码至远程仓库
git pull origin master 从远程仓库下载代码病快速合并
git fetch 从远程仓库下载新分支与数据,但不会自动merge
git merge 远端仓库提取数据并尝试合并到当前分支

二、Chrome调试工具

2.1 Elements

dom结构

2.2 Console

打印日志窗口

2.3 debugger

代码中在需设置断点处的地址加上debugger,或在Sources对应的js文件添加断点(点击左侧代码行序号),则可以触发断点调试

2.4 Network

所有资源加载

2.5 Application

查看和操作本地存储信息

三、抓包

移动端H5页,查看网络请求,需要用工具抓包:

  • windows一般用fiddler
  • Mac OS一般用charles

抓包过程:

  1. 手机和电脑连同一个局域网
  2. 将手机代理到电脑上
  3. 手机浏览网页,即可抓包

四、webpack babel

  • ES6模块化,浏览器暂不支持
  • ES6语法,浏览器并不完全支持
  • 压缩代码,整合代码,以让网页加载更快

五、Linux常用命令

linux命令 作用
ls 查看所有文件
ls -a 查看所有文件,包括隐藏文件(-a 是all的意思)
ll 查看所有文件列表
clear 清屏
mkdir [name] 创建文件夹
rm -rf [name] 删除文件夹及文件夹内部所有内容(-r递归,-f强制)
rm [name] 删除文件
cd [name] 进入文件夹
mv [oldName] [newName] 修改文件名称
mv index.html ../index.html 移动文件存放路径
cp index.html index1.html 复制文件
cat [name] 将该文件的所有内容显示在控制台中
grep "关键字" [name] 在文件中查找关键字
touch [name] 新建文件
vi [name] 若无该文件则新建文件并进入vim编辑器模式,若有该文件则直接打开进入vim
进入vim编辑器模式,输入i,即可编辑输入 -
点击esc退出编辑模式,再输入:wq,即可保存并退出vim模式 -
若输入后,不想保存,则输入:q!,即可强制退出 -

运行环境

  • 运行环境即浏览器(server端有nodejs)
  • 下载网页代码,渲染出页面,期间会执行若干JS
  • 要保证代码在浏览器中稳定且高效

一、网页加载过程

1.1 题目

1.1.1 从输入URL到渲染出页面的整个过程

1.1.2 window.onload和DOMContentLoaded的区别

1.2 知识点

1.2.1 加载资源的形式

  • HTML代码
  • 媒体文件,如图片、视频等
  • javascript css

1.2.2 加载资源的过程

  1. DNS解析:域名 -> IP地址
  2. 浏览器根据IP地址向服务器发起http请求
  3. 服务器处理http请求,并返回给浏览器

1.2.3 渲染页面的过程

  1. 根据HTML代码生成 DOM Tree

  2. 根据CSS代码生成CSSOM(object model)

  3. 将DOM Tree和CSSOM整合形成Render Tree

  4. 根据Render Tree渲染页面

  5. 遇到<script>则暂停渲染,优先加载并执行JS代码,完成再继续

  6. 直至把Render Tree渲染完成

思考

为什么把CSS放在head标签中?

  1. css放在body标签尾部时, DOMTree构建完成之后便开始构建RenderTree, 并计算布局渲染网页, 等加载解析完css之后, 开始构建CSSOMTree, 并和DOMTree重新构建RenderTree, 重新计算布局渲染网页
  2. css放在head标签中时, 先加载并解析css,构建CSSOMTree, 与此同时构建DOMTree, CSSOMTree和DOMTree都构建完毕之后开始构建RenderTree, 计算布局渲染网页

对比两者, css放在head标签中比css放在body标签尾部少了一次构建RenderTree, 一次计算布局和一次渲染网页, 因此性能会更好; 并且css放在body标签尾部时会在网页中短暂出现"裸奔"的HTML, 这不利于用户体验

为何建议把js放在body最后?

  1. 避免JavaScript操作DOM失效
  2. 在解析JavaScript代码之前,将页面的内容完全呈现在浏览器中,用户会因为浏览器显示空白页面的时间缩短而感到打开页面的速度变快了

window.onload和DOMContentLoaded

window.addEventListener('load', function() {
  // 页面的全部资源加载完才会执行,包括图片、视频等
})
window.addEventListener('DOMContentLoaded', function() {
  // DOM渲染完即可执行,此时图片、视频还可能没加载完
})

1.3 题目

1.3.1 从输入URL到渲染出页面的整个过程

  • 下载资源:各个资源类型,下载过程
  • 渲染页面:结合html css javascript 图片等

1.3.2 window.onload和DOMContentLoaded的区别

  • window.onload资源全部加载完成后才能执行,包括图片
  • DOMContentLoaded DOM渲染完成即可,图片可能尚未下载

二、性能优化

2.1 性能优化原则

  • 多使用内存、缓存或其他方法
  • 减少CPU计算量,减少网络加载耗时

2.2 从何入手

2.2.1 让加载更快

  1. 减少资源体积:
    • 压缩代码
  2. 减少访问次数:
    • 合并代码(webpack合并js css文件、雪碧图)
    <script src="a.js"></script>
    <script src="b.js"></script>
    <script src="c.js"></script>
    
    <script src="abc.js"></script>
    
    • SSR服务端渲染
      1. 服务端渲染:将网页和数据一起加载,一起渲染
      2. 非SSR:先加载网页,再加载数据,再渲染数据
    • 缓存
      1. 静态资源加hash后缀,根据文件内容计算hash
      2. 文件内容不变,则hash不变,则URL不变
      3. URL和文件不变,则会自动触发http缓存机制,返回304
  3. 使用更快的网络:CDN(根据地域做静态服务)

2.2.2 让渲染更快

  1. CSS 放在head,JS放在body最下面
  2. 尽早开始执行JS,用DOMContentLoaded触发
  3. 懒加载(图片懒加载,上滑加载更多)
  4. 对DOM查询进行缓存
  5. 频繁DOM操作,合并到一起再插入到DOM结构
  6. 节流 防抖

2.2.3 防抖 debounce

含义:当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时

function debounce (fn, delay = 500) {
  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(this.value)
}, 1000))

2.2.4 节流

含义:当持续触发事件时,保证一定时间段内只调用一次事件处理函数。节流通俗解释就比如我们水龙头放水,阀门一打开,水哗哗的往下流,秉着勤俭节约的优良传统美德,我们要把水龙头关小点,最好是如我们心意按照一定规律在某个时间间隔内一滴一滴的往下滴。

/** 
* 拖拽一个元素时,要随时拿到该元素被拖拽的位置
* 直接用drag事件,则会频繁触发,很容易导致卡顿
* 节流:无论拖拽速度多快,都会每隔100ms触发一次
*/
<body>
  <div id="div1" draggable="true" style="width: 100px;height: 100px;border: 1px solid red;">可拖拽</div>
<script>
// 节流
function throttle(fn, delay = 100) {
  let timer = null
  return function () {
    if (timer) {
      return
    }
    timer = setTimeout(() => {
      fn.apply(this, arguments)
      timer = null
    }, delay)
  }
}
const div1 = document.getElementById('div1')
div1.addEventListener('drag' , throttle(function(e) {
  console.log(e.offsetX, e.offsetY)
}, 500))
</script>
</body>

三、安全

3.1 常见的web前端攻击方式有哪些

3.1.1 XSS跨站请求攻击

攻击情景模拟:

  1. 一个博客网站,A某发表一篇博客,其中嵌入了<script>脚本
  2. 脚本内容:获取cookie,发送到我的服务器(服务器配合跨域)
  3. 发布这篇博客,只要有人查看它,则A某即可轻松收割访问者的cookie

XSS攻击预防:

  1. 替换特殊字符,如<变为&lt; >变为&gt;
  2. <script>变为&lt;script&gt;,将会作为字符串直接显示,而不会作为脚本执行(npm 有一个xss工具包可帮忙替换:www.npmjs.com/package/xss…
  3. 前端要替换,后端也要替换

3.1.2 CSRF跨站请求伪造

攻击情景模拟:

  1. 银行网站A,它以GET请求来完成银行转账的操作,如:www.mybank.com/Transfer.ph…
  2. 危险网站B,它里面有一段HTML的代码如下:
&emsp;<img src=http://www.mybank.com/Transfer.php?toBankId=11&money=1000>
  1. 首先,你登录了银行网站A,然后访问危险网站B,噢,这时你会发现你的银行账户少了1000块......

为什么会这样呢?原因是银行网站A违反了HTTP规范,使用GET请求更新资源。在访问危险网站B的之前,你已经登录了银行网站A,而B中的<img>以GET的方式请求第三方资源(这里的第三方就是指银行网站了,原本这是一个合法的请求,但这里被不法分子利用了),所以你的浏览器会带上你的银行网站A的Cookie发出Get请求,去获取资源“www.mybank.com/Transfer.ph… ,结果银行网站服务器收到请求后,认为这是一个更新资源操作(转账操作),所以就立刻进行转账操作.

CSRF攻击预防:

  1. 使用POST接口
  2. 增加验证,例如密码、短信验证码、指纹等