看了很多 JS 的书籍和教程,往往会模糊焦点,这篇文章会做一个梳理,将 JS 中的重难点进行总结。
文章分为上下两篇。上篇是基本知识,下篇是进阶知识。
#变量类型和计算
引例:
值类型和引用类型的区别
typeof能判断哪些类型?
何时使用
===
何时使用==
?手写深拷贝
变量类型
- 值类型
- 引用类型
值类型
let a = 100
let b = a
a = 200
console.log(b) // 100
引用类型
let a = { age: 20 }
let b = a
b.age = 21
console.log(a.age) // 21
分析:
值类型的变量,赋值时会重新将值拷贝一份,因此两个变量之间相互独立。
而引用类型的变量名指向的是一个内存地址,真实的对象存储在该内存地址所指向的内存中(堆)。当变量进行赋值时let b = a
,b
也会指向 a
所指向的那块内存,两个变量实际上指向的是同一个对象。
常见值类型:
const s = 'abc' // 字符串
const n = 100 // 数字
const b = true // 布尔
const s = Symbol('s') // Symbol
常见引用类型:
const obj = { x: 100 } // 对象
const arr = ['a', 'b', 'c'] // 数组
const n = null // 空,特殊引用类型,指针指向为空地址
function fn() { } // 函数,特殊引用类型,但不用于存储数据,所以没有“拷贝,复制函数”这一说
typeof 运算符
- 识别所有值类型
- 识别函数
- 判断是否是引用类型(不可再细分)
识别所有值类型
let a typeof a // undefined
const s = 'abc' typeof a // 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
深拷贝
/**
* 深拷贝
*/
const obj1 = {
age: 20,
name: 'xxx',
address: {
city: 'beijing'
},
arr: ['a', 'b', 'c']
}
const obj2 = deepClone(obj1)
obj2.address.city = 'shanghai'
obj2.arr[0] = 'a1'
console.log(obj1.address.city)
console.log(obj1.arr[0])
/**
* 深拷贝
* @param {Object} obj 要拷贝的对象
*/
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
}
变量计算
类型转换:
- 字符串拼接
- ==
- if语句和逻辑运算
字符串拼接
const a = 100 + 10 // 110
const b = 100 + '10' // '10010'
const c = true + '10' // 'true10'
==
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) { }
if语句和逻辑运算
truly变量:
!!a === true
的变量falsely变量:
!!a === false
的变量
truly变量
!!1 // true
!!{} // true
falsely变量
!!0 // false
!!null // false
!!'' // false
!!undefined // false
!!NaN // false
!!false // false
例子1:if 语句
// 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) {
// ...
}
例子2:逻辑判断
console.log(10 && 0) // 0
console.log('' || 'abc') // 'abc'
console.log(!window.abc) // true
#原型和原型链
引例:
- 如何判断一个变量是不是数组?
- 手写一个简易的jQuery,考虑插件和扩展性。
- class的原型本质,怎么理解?
知识点:
- class和继承
- 类型判断instanceof
- 原型和原型链
class
- constructor
- 属性
- 方法
例子:
// 类
class Student {
constructor(name, number) {
this.name = name
this.number = number
// this.gender = 'male'
}
sayHi() {
console.log(`姓名 ${this.name} ,学号 ${this.number}`)
// console.log(
// '姓名 ' + this.name + ' ,学号 ' + this.number
// )
}
// study() {
// }
}
// 通过类 new 对象/实例
const xialuo = new Student('夏洛', 100)
console.log(xialuo.name)
console.log(xialuo.number)
xialuo.sayHi()
const madongmei = new Student('马冬梅', 101)
console.log(madongmei.name)
console.log(madongmei.number)
madongmei.sayHi()
继承
- extends
- super
- 扩展或重写方法
// 父类
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}`)
}
}
// 实例
const xialuo = new Student('夏洛', 100)
console.log(xialuo.name)
console.log(xialuo.number)
xialuo.sayHi()
xialuo.eat()
// 实例
const wanglaoshi = new Teacher('王老师', '语文')
console.log(wanglaoshi.name)
console.log(wanglaoshi.major)
wanglaoshi.teach()
wanglaoshi.eat()
类型判断 instanceof
xialuo instanceof Student // true
xialuo instanceof People // true
xialuo instanceof Object // true
[] instanceof Array // true
[] instanceof Object // true
{} instanceof Object // true
原型
// class 实际上是函数,可见是语法糖
typeof People // 'function'
typeof Student // 'function'
// 隐式原型和显式原型
console.log(xialuo.__proto__) // 隐式原型
console.log(Student.prototype) // 显式原型
console.log(xialuo.__proto__ === Student.prototype) // true
原型关系
- 每个class都有显式原型prototype
- 每个实例都有隐式原型__proto__
- 实例的__proto__指向对应class的prototype
示意图:
基于原型的执行规则
- 获取属性xialuo.name或执行方法xialuo.sayhi()时
- 先在自身属性和方法寻找
- 如果找不到,则自动__proto__去中查找。
原型链
console.log(Student.prototype.__proto__)
console.log(People.prototype)
console.log(People.prototype === Student.prototype.__proto__) // true
示意图
引例解答:
如何判断一个变量是不是数组?
a instanceof Array
手写一个简易的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)
})
}
// 扩展很多 DOM API
}
// 插件
jQuery.prototype.dialog = function (info) {
alert(info)
}
// “造轮子”
class myJQuery extends jQuery {
constructor(selector) {
super(selector)
}
// 扩展自己的方法
addClass(className) {
}
style(data) {
}
}
// const $p = new jQuery('p')
// $p.get(1)
// $p.each((elem) => console.log(elem.nodeName))
// $p.on('click', () => alert('clicked'))
class的原型本质,怎么理解?
- 原型和原型链的图示
- 属性和方法的执行规则
#作用域和闭包
引例:
- this的不同应用场景,如何取值?
- 手写bind函数
- 实际开发中闭包的应用场景,举例说明
- 创建10个a标签,点击的时候弹出对应的序号
知识点:
- 作用域和自由变量
- 闭包
- this
作用域
分类
- 全局作用域
- 函数作用域
- 块级作用域(ES6新增)
// ES6 块级作用域
if (true) {
let x = 100 // 只有使用let或const等ES6特性才会触发块级作用域
}
console.log(x) // 会报错
自由变量
- 一个变量在当前作用域没有定义,但被使用了
- 向上级作用,一层一层依次查找,直到找到为止
闭包
- 作用域应用的特殊情况,有两种表现:
- 若函数作为参数被传递
- 函数作为返回值被返回
函数作为参数
// 函数作为参数被传递
function print(fn) {
const a = 200
fn()
}
const a = 100
function fn() {
console.log(a)
}
print(fn) // 100
函数作为返回值
// 函数作为返回值
function create() {
const a = 100
return function () {
console.log(a)
}
}
const fn = create()
const a = 200
fn() // 100
所有的自由变量的查找,是在函数定义的地方,向上级作用域查找,不是在执行的地方!!!
this
- 作为普通函数
- 使用call、apply、bind
- 作为对象方法被调用
- 在class方法中调用
- 箭头函数
this 取什么值,是在函数执行的时候确定的,不是在函数定义的时候确定的。该原则适用于以上5种情况。
例子1:普通函数及call、apply、bind中的this
function fn1() {
console.log(this)
}
fn1() // window
fn1.call({ x: 100 }) // { x :100 }
const fn2 = fn1.bind({ x: 200 })
fn2() // { x : 200 }
注:关于call,apply,bind的区别,见 JavaScript 中 call()、apply()、bind() 的用法
例子2:对象方法中的this
const zhangsan = {
name: '张三',
sayHi() {
// this 即当前对象
console.log(this)
},
wait() {
setTimeout(function () {
// this === window
console.log(this)
})
}
}
例子3:箭头函数中的this
const lisi = {
name: '李四',
sayHi() {
// this 即当前对象
console.log(this)
},
wait() {
setTimeout(() => {
// this === window
console.log(this)
})
}
}
例子4:class中的this
class People {
constructor(name) {
this.name = name
this.age = 20
}
sayHi() {
console.log(this)
}
}
const zhangsan = new People('张三')
zhangsan.sayHi() // zhangsan 对象
引例解答:
1. this的不同应用场景,如何取值?
- 作为普通函数
- 使用call、apply、bind
- 作为对象方法被调用
- 在class方法中调用
- 箭头函数
2. 手写bind函数
// 模拟 bind
Function.prototype.bind1 = function () {
// 将参数拆解为数组
const args = Array.prototype.slice.call(arguments)
// 获取 this(数组第一项)
const t = args.shift()
// fn1.bind(...) 中的 fn1
const self = this
// 返回一个函数
return function () {
return self.apply(t, args)
}
}
/*
fn1.__proto__ === Function.prototype // true
*/
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)
3. 实际开发中闭包的应用场景,举例说明
- 隐藏数据
- 如做一个简单的cache工具
// 闭包隐藏数据,只提供 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'))
4. 创建10个<a>
标签,点击的时候弹出对应的序号
错误示例:❎
// 创建10个a标签,点击的时候弹出对应的序号
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) // i 是全局作用域
})
document.body.appendChild(a)
}
正确示例:✅
// 创建10个a标签,点击的时候弹出对应的序号
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) // i 是块级作用域
})
document.body.appendChild(a)
}
注:若将上面正确示例中for循环的let i = 0
改为var i = 10
,则其结果同错误示例,因为var
定义的变量为全局变量。
#异步
引例:
- 同步和异步的区别是什么?
- 手写用promise加载一张图片
- 前端使用异步的场景有哪些?
- 读下段代码:
console.log(1)
setTimeout(function () {
console.log(2)
}, 1000)
console.log(3)
setTimeout(function () {
console.log(4)
}, 0)
console.log(5)
问:思考,上段代码中,数字的打印顺序是什么?本节末尾会解答。
知识点:
- 单线程和异步
- 应用场景
- callback hell 和 Promise
单线程和异步
- JS是单线程语言,只能同时做一件事
- 浏览器和notejs已支持JS启动进程,如Web Worker
- JS和DOM渲染共用同一个线程,因为JS可修改DOM结构
- 遇到等待(网络请求,定时任务)不能卡住
- 需要异步
- 回调callback函数形式
异步和同步
// 异步 100 300 200
console.log(100)
setTimeout(function () {
console.log(200)
}, 1000)
console.log(300)
// 同步 100 200 300
console.log(100)
alert(200)
console.log(300)
js是单线程语言,异步不会阻塞代码执行,同步会阻塞代码执行。
应用场景
- 网络请求,如ajax、图片加载
- 定时任务,如setTimeout
ajax:
// start end data1
console.log('start')
$.get('./data1.json', function (data1) {
console.log(data1)
})
console.log('end')
图片加载:
// start end loaded
console.log('start')
let img = document.createElement('img')
img.onload = function () {
console.log('loaded')
}
img.src = '/xxx.jpg'
console.log('end')
定时器:
// 100 300 200
console.log(100)
setTimeout(function () {
console.log(200)
}, 1000)
console.log(300)
/*-----------------------*/
// 100 300 200 200 200 ...
console.log(100)
setInterval(function () {
console.log(200)
}, 1000)
console.log(300)
callback hell(回调地狱)
$.get(url1, (data1) => {
console.log(data1)
$.get(url2, (data2) => {
console.log(data2)
$.get(url3, (data3) => {
console.log(data3)
})
})
})
Promise
promise主要用于解决回调嵌套过多的问题。
function getData(url) {
return new Promise((resolve, reject) => {
$.ajax({
url,
success(data) {
resolve(data)
},
error(err) {
reject(err)
}
})
})
}
const url1 = '/data1.json'
const url2 = '/data2.json'
const url3 = '/data3.json'
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))
引例解答:
1. 同步和异步的区别是什么?
异步不会阻塞代码执行,同步会阻塞代码执行。
2. 手写用promise加载一张图片
function loadImg(src) {
const p = new Promise((resolve, reject) => {
const img = document.createElement('img')
img.onload = () => {
resolve(img)
}
img.onerror = () => {
const err = new Error(`图片加载失败 ${src}`)
reject(err)
}
img.src = src
})
return p
}
const url1 = '1.jpg'
const url2 = '2.jpg'
loadImg(url1).then(img1 => {
console.log(img1.width)
return img1 // then返回普通对象,则立即执行下一个then
}).then(img1 => {
console.log(img1.height)
return loadImg(url2) // then返回新的promise实例,
// 则下一个then会在这个新的promise状态改变后执行
}).then(img2 => {
console.log(img2.width)
return img2
}).then(img2 => {
console.log(img2.height)
}).catch(ex => console.error(ex))
注:then 中如果返回新的promise,那么下一级then会在新的promise状态改变后执行;如果返回其他任何值,则会立即执行下一级then
3. 前端使用异步的场景有哪些?
- 网络请求,如ajax、图片加载
- 定时任务,如setTimeout
4. 读下段代码
console.log(1)
setTimeout(function () {
console.log(2)
}, 1000)
console.log(3)
setTimeout(function () {
console.log(4)
}, 0)
console.log(5)
问:上段代码中,数字的打印顺序是什么?
答:1 3 5 4 2
#DOM
引例:
- DOM是哪种数据结构?
- DOM操作的常用API
- attr和property的区别
- 一次性插入多个DOM节点,考虑性能
知识点:
- DOM本质
- DOM节点操作
- DOM结构操作
- DOM性能
DOM本质
DOM的本质是一棵树,是从HTML文件解析出来的一棵树。
DOM节点操作
- 获取DOM节点
- poperty
- attribute
获取DOM节点
const div1 = document.getElementById('div1')
console.log('div1', div1)
const divList = document.getElementsByTagName('div') // 集合
console.log('divList.length', divList.length)
console.log('divList[1]', divList[1])
const containerList = document.getElementsByClassName('container') // 集合
console.log('containerList.length', containerList.length)
console.log('containerList[1]', containerList[1])
const pList = document.querySelectorAll('p')
console.log('pList', pList)
const pList = document.querySelectorAll('p')
const p1 = pList[0]
poperty
// property 形式
p1.style.width = '100px'
console.log(p1.style.width)
p1.className = 'red'
console.log(p1.className)
console.log(p1.nodeName)
console.log(p1.nodeType) // 1
attribute
// attribute
p1.setAttribute('data-name', 'imooc')
console.log(p1.getAttribute('data-name'))
p1.setAttribute('style', 'font-size: 50px;')
console.log(p1.getAttribute('style'))
property和attribute的异同:property会修改对象的属性,但不会体现到html结构中;attribute会修改html属性,并改变html结构。两者都有可能引起DOM重新渲染。
DOM结构操作
- 新增、插入节点
- 获取父元素,获取子元素列表
- 删除子元素
新增、插入节点
const div1 = document.getElementById('div1')
const div2 = document.getElementById('div2')
// 新建节点
const newP = document.createElement('p')
newP.innerHTML = 'this is newP'
// 插入节点
div1.appendChild(newP)
// 移动节点
const p1 = document.getElementById('p1')
div2.appendChild(p1)
获取父元素,获取子元素列表
// 获取父元素
console.log(p1.parentNode)
// 获取子元素列表
const div1ChildNodes = div1.childNodes
console.log(div1.childNodes)
const div1ChildNodesP = Array.prototype.slice.call(div1.childNodes).filter(child => {
if (child.nodeType === 1) {
return true
}
return false
})
console.log('div1ChildNodesP', div1ChildNodesP)
注:node节点的childNodes属性会返回包括 '#text' 节点在内的所有直接子节点。
删除子元素
div1.removeChild(div1ChildNodesP[0])
DOM性能
- DOM操作非常昂贵,避免频繁的DOM操作
- 对DOM查询做缓存
- 将频繁操作改为一次性操作
DOM操作非常昂贵,避免频繁的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查询
}
将频繁操作改为一次性操作
const list = document.getElementById('list')
// 创建一个文档片段,此时还没有插入到 DOM 结构中
const frag = document.createDocumentFragment()
for (let i = 0; i < 20; i++) {
const li = document.createElement('li')
li.innerHTML = `List item ${i}`
// 先插入文档片段中
frag.appendChild(li)
}
// 都完成之后,再统一插入到 DOM 结构中
list.appendChild(frag)
console.log(list)
引例解答:
1. DOM是哪种数据结构?
答:树(DOM树)
2. DOM操作的常用API
答:DOM节点操作,DOM结构操作,attribute和property
3. attr和property的区别
答:property会修改对象的属性,但不会体现到html结构中;attribute会修改html属性,并改变html结构。两者都有可能引起DOM重新渲染。
4. 一次性插入多个DOM节点,考虑性能
const list = document.getElementById('list')
// 创建一个文档片段,此时还没有插入到 DOM 结构中
const frag = document.createDocumentFragment()
for (let i = 0; i < 20; i++) {
const li = document.createElement('li')
li.innerHTML = `List item ${i}`
// 先插入文档片段中
frag.appendChild(li)
}
// 都完成之后,再统一插入到 DOM 结构中
list.appendChild(frag)
#BOM
引例:
- 如何识别浏览器的类型?
- 分析拆解url各个部分
知识点:
- navigator
- screen
- location
- history
navigator和screen
// navigator
const ua = navigator.userAgent
const isChrome = ua.indexOf('Chrome')
console.log(isChrome)
// screen
console.log(screen.width)
console.log(screen.height)
location和history
// location
console.log(location.href)
console.log(location.protocol)
console.log(location.pathname)
console.log(location.search)
console.log(location.hash)
// history
history.back()
history.forward
引例解答:见上。
#事件
引例:
- 编写一个通用的事件监听函数
- 描述事件冒泡的流程
- 无限下拉的图片列表,如何监听每个图片的点击?
知识点:
- 事件绑定
- 事件冒泡
- 事件代理
事件绑定
const btn = document.getElementById('btn1')
btn.addEventListener('click', event => {
console.log('clicked')
})
// 通用的绑定函数
function bindEvent(elem, type, fn) {
elem.addEventListener(type, fn)
}
const a = document.getElementById('link1')
bindEvent(a, 'click', e => {
e.preventDefault() // 阻止默认行为
alert('clicked')
})
事件冒泡
<body>
<div id="div1">
<p id="p1">激活</p>
<p id="p2">取消</p>
<p id="p3">取消</p>
<p id="p4">取消</p>
</div>
<div id="div2">
<p id="p5">激活</p>
<p id="p6">取消</p>
</div>
</body>
const p1 = document.getElementById('p1')
const body = document.body
bindEvent(p1, 'click', e => {
e.stopPropagation() // 注释掉这一行,来体会事件冒泡
alert('激活')
})
bindEvent(body, 'click', e => {
alert('取消')
})
事件代理
- 代码简介
- 减少浏览器内存使用
- 但是,不要滥用
<div id="div1">
<a href="#">a1</a>
<a href="#">a2</a>
<a href="#">a3</a>
<a href="#">a4</a>
</div>
<button>
点击增加一个 a 标签
</button>
const div1 = document.getElementById('div1')
div1.addEventListener('click', e => {
const target = e.target
if (e.nodeName === 'A') {
alert(target.innerHTML)
}
})
通用事件绑定
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 btn1 = document.getElementById('btn1')
bindEvent(btn1, 'click', function (event) {
// console.log(event.target) // 获取触发的元素
event.preventDefault() // 阻止默认行为
alert(this.innerHTML)
})
// 代理绑定
const div3 = document.getElementById('div3')
bindEvent(div3, 'click', 'a', function (event) {
event.preventDefault()
alert(this.innerHTML)
})
引例解答:
1. 编写一个通用的事件监听函数
见上 “通用事件绑定”
2. 描述事件冒泡的流程
- 基于DOM树形结构
- 事件会顺着触发元素往上冒泡
- 应用场景:代理
3. 无限下拉的图片列表,如何监听每个图片的点击?
- 事件代理
- 用e.target获取触发元素
- 用matches来判断是否是触发元素
#Ajax
引例:
- 手写一个简易的ajax
- 跨域的常用实现方式
知识点:
- XMLHttpRequest
- 状态码
- 跨域:同源策略、跨域解决方案
XMLHttpRequest
// GET
const xhr = new XMLHttpRequest()
xhr.open('GET', '/api', true) // true表示异步
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
alert(xhr.responseText)
}
}
}
xhr.send(null)
/*----------------*/
// POST
const xhr = new XMLHttpRequest()
xhr.open('POST', '/login', true) // true表示异步
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
alert(xhr.responseText)
}
}
}
const postData = {
username: 'zhangsan',
password: '123'
}
xhr.send(JSON.stringify(postData))
状态码
xhr.readystatechange
- 0 -(未初始化)还没有调用send方法
- 1 -(载入)已调用send()方法,正在发送请求
- 2 -(载入完成)send()方法执行完成,已经接受到全部响应内容
- 3 -(交互)正在解析响应内容
- 4 -(完成)响应内容解析完成,可以在客户端调用
xhr.status
- 2xx - 表示成功处理请求,如200
- 3xx - 需要重定向,浏览器直接跳转,如301 302 304
- 4xx - 客户端请求错误,如404 403
- 5xx - 服务器端错误
跨域:同源策略、跨域解决方案
- 什么是跨域(同源策略)
- JSONP
- CORS(服务端支持)
同源策略
- ajax请求时,浏览器要求当前网页和server必须同源(安全)
- 同源:协议、域名、端口,三者必须一致。
- 如前端:
http://a.com:8080
; server:https://b.com/api/xxx
加载图片,css js可无视同源策略
<img rc=跨域的图片地址 />
<link href=跨域的css地址 />
<script src=跨域的js地址></script>
-
<img />
可用于统计打点,可使用第三方统计服务 -
<link /> <script>
可使用CDN,CDNn一般都是外域 -
<script>
可实现JSONP
跨域
- 所有的跨域都必须经过server端允许和配合
- 未经server端允许就实现跨域,说明浏览器有漏洞,危险信号
JSONP
- 访问https://baidu.com,服务端一定返回一个html文件吗?
- 服务器可以任意动态拼接数据返回,只要符合html格式要求
- 同理
<script src="http://baidu.com/getData.js">
-
<script>
可绕过跨域限制 - 服务器可以任意动态拼接数据返回
- 所以
<script>
就可以获得跨域的数据,只要服务端愿意返回
<script>
window.callback = function(data){
this.console.log(data)
}
</script>
<script src="http://www.baidu.com/getData.js"></script>
<!--将返回 callback({x:100, y:200})-->
jQuery实现jsonp
$.ajax({
url: 'http://baicu.com/x-origin.json',
dataType: 'jsonp'
jsonpCallback: 'callback',
success: function (data) {
console.log(data)
}
})
CORS - 服务器设置http header
// 第2个参数填写允许跨域的域名,称不建议直接写 '*'
response.setHeader('Access-Control-Allow-Origin', 'http://baidu.com');
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');
引例解答:
1. 手写一个简易的ajax
// 常规形式
function ajax(url, successFn) {
const xhr = new XMLHttpRequest()
xhr.open('GET', url, true) // true表示异步
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
successFn(xhr.responseText)
}
}
}
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))
2. 跨域的常用实现方式
- JSONP
- CORS
#存储
引例:
- 描述cookie、localStorage、sessionStorage的区别
知识点
- cookie
- localStorage和sessionStorage
cookie
- 本身用于浏览器和server通讯
- 被“借用”到本地存储来
- 可用
document.cookie= '...'
来修改
Cookie的缺点
- 存储大小,最大4KB
- http请求时需要发送到服务端,增加请求数据量
- 只能用
document.cookie='...'
来修改,太过简陋
localStorage和sessionStorage
- HTML5专门为存储而设计,最大可存5M
- API简单易用,setItem, getItem
- 不会随着HTTP请求被发送出去。
- LocalStorage数据会永久存储,除非代码或手动删除
- sessionStorage数据只存在于当前会话,浏览器关闭则清空
- 一般用localStorage会更多一些
引例解答:
描述cookie、localStorage、sessionStorage的区别
- 容量
- API易用性
- 是否跟随http请求发送出去
#开发环境
知识点:
- git
- 调试工具
- 抓包
- webpack
- babel
- Linux常用命令
git
- 最常用的代码版本管理工具
- 大型项目需要多人协作开发,必须服用git
- Mac OS自带git命令,windows可去官网下载安装
- git服务端常见的github coding.net等
- 大公司会搭建自己的内网git服务
常用git命令
git add .
git checkout xxx
git commit -m "xxx"
git push origin master
git pull origin master
git diff/git diff filename
git log
git show commit_id
git branch
git checkout -b xxx/git checkout xxx
git fetch
git merge xxx
git stash
git stash pop
chrome调试工具
- elements
- console
- debugger
- network
- application
抓包
移动端h5页面,查看网络请求,需要用装包工具
windows一般用发到fiddler
Mac OS一般用charles
手机和电脑连同一个局域网
将手机代理到电脑上
手机浏览网页即可找抓包
查看网络请求
网址代理
https
webpack 和babel
体量太大,此处略。
Linux命令
- 公司的线上机器一般都是linux(参考阿里云)
- 测试机也需要保持一致,用Linux
- 测试机或者线上机出了问题,本地又不能复现,需要去排查
#运行环境
- 运行环境即浏览器(server端有nodejs)
- 下载网页代码渲染出页面,期间会执行若干js
- 要保证代码在浏览器中:稳定且高效
知识点
- 网页加载过程
- 性能优化
- 安全
网页加载过程
引例:
从输入url到渲染出页面的整个过程
window.onload和DOMContentLoaded的区别
加载资源的形式
- html代码
- 媒体文件,如图片视频等
- Javascript css
加载资源的过程
- DNS解析:域名 -> ip地址
- 浏览器根据IP地址向服务器发起HTTP请求
- 服务器处理HTTP请求,并返回给浏览器
渲染页面的过程
- 根据html代码生成DOM Tree
- 根据css代码生成CSSOM
- 将DOM Tree和 CSSOM 整合形成Render Tree
- 根据Render Tree渲染页面
- 遇到
<script>
则暂停渲染,优先加载并执行js代码,完成再继续
window.onload和DOMContentLoaded
window.addEventListener('load', function () {
// 页面的全部资源加载完成才会执行,包括图片视频等
})
document.addEventListener('DOMContentLoaded', function () {
// DOM渲染完即可执行,此时图片视频还可能没有加载完
})
引例解答:
1. 从输入url到渲染出页面的整个过程
- 下载资源:各个资源类型,下载过程
- 渲染页面:结合HTML css JavaScript 图片等
2. window.onload和DOMContentLoaded的区别
- window.onload: 页面的全部资源加载完成才会执行,包括图片视频
- DOMContentLoaded:DOM渲染完即可执行,此时图片视频还可能没有加载完
性能优化
- 是一个综合性问题,没有标准答案,要求尽量全面
- 某些细节问题,可能会单独提问:手写防抖、节流
性能优化原则
- 多使用内存缓存或其他方法
- 减少CPU计算量,减少网络加载耗时
- 适用于所有编程的性能优化:空间换时间
从何入手
- 让加载更快
- 减少资源体积:压缩代码
- 减少请求访问次数:合并代码,SSR服务器端渲染,缓存
- 使用更快的网络:CDN
- 让渲染更快
- css放在head, js放在body最下面
- 尽早开始执行js,用DOMContentLoaded触发
- 懒加载(图片懒加载,上滑加载更多)
- 对DOM查询进行缓存
- 频繁DOM操作,合并到一起插入DOM结构
- 节流 throttle 防抖 debounce
防抖 debounce
- 监听一个输入框的文字变化后触发change事件
- 直接用keyup事件则会频繁触发change事件
- 防抖:用户输入结束或暂停时才会触发change事件
const input1 = document.getElementById('input1')
// let timer = null
// input1.addEventListener('keyup', function () {
// if (timer) {
// clearTimeout(timer)
// }
// timer = setTimeout(() => {
// // 模拟触发 change 事件
// console.log(input1.value)
// // 清空定时器
// timer = null
// }, 500)
// })
// 封装成“防抖”函数
function debounce(fn, delay = 500) {
// timer 是闭包中的
let timer = null
return function () {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn.apply(this, arguments)
timer = null
}, delay)
}
}
input1.addEventListener('keyup', debounce(function (e) {
console.log(e.target)
console.log(input1.value)
}, 600))
节流 throttle
- 拖拽一个元素时,要随时拿到该元素被拖拽的位置
- 直接用drag事件,则会频繁触发,很容易导致卡顿
- 节流:无论拖拽速度多快,都会每隔100毫秒触发一次
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)
// })
// 节流
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)
}))
div1.addEventListener('drag', function(event) {
})
安全
问题:
常见的web前端,攻击方式有哪些?
XSS跨站请求攻击
XSRF跨站请求伪造
XSS跨站请求攻击
比如在一个博客网站上发表文章,其中嵌入script脚本。脚本内容是获取cookie,发送到我的服务器(服务器配合跨域)。若有人浏览我的文章,我就可以轻松收割访问者的cookie。
XSS预防:替换特殊字符,如<
变为<
,>
变为>
,则<script>
变为<script>
,直接显示,而不会作为脚本执行。前端要替换,后端也要替换,都做总不会有错。
开源库 xss
XSRF跨站请求伪造
设想一个场景:你正在购物,看中了某个商品,商品ID是100,付费接口是xxx.com/pay?id=100,但有任何验证。我是攻击者,我看中了一个商品,ID是200。我向你发送一封电子邮件,邮件标题很吸引人,但邮件正文隐藏着<img src=xxx.com/pay?id=200 />
,你一查看邮件,就帮我购买了ID是200的商品。
XSRF预防:使用post接口;增加验证,例如密码、短信验证码、指纹等。
参考资料: