适合初中级前端面试(0~2年)梳理JS知识体系
一、变量类型和计算
1、typeof 能判断哪些类型
值类型
//常见值类型
let a //undefined
const s = 'abc' //string
const n = 100 //number
const b = true //boolean
const s = symbol('s') //symbol
//值类型
let a = 100
let b = a
a = 200
console.log(b) //100
引用类型
//常见引用类型
const obj = { x: 100 }
const arr = ['a', 'b', 'c']
const n = null //特殊引用类型,指针指向空地址
//特殊引用类型,但不用于存储数据,没有"拷贝"、"复制函数"这一说
function fn(){}
//引用类型
let a = { age: 20 }
let b = a
b.age = 21
console.log(a.age) //21
typeof运算符
- 识别所有值类型(undefined、string、number、boolean、symbol)
- 识别函数 function
console.log(typeof console.log) //function
console.log(typeof function(){}) //function
- 判断是否是引用类型(不可再细分
console.log(typeof null) //object
console.log(typeof {x:100}) //object
2、何时使用 === 何时使用 ==
3、值类型和引用类型的区别
值类型 vs 引用类型 (堆栈模型)
// 值类型
var a=10
var b=a
a=11
console.log(b) //10
// 引用类型
var obj1={x:100}
var obj2=obj1
obj1.x=200
console.log(obj2.x) // 200
值类型和引用类型的区别
const obj1={x:100,y:200}
const obj2=obj1
let x1=obj1.x
obj2.x=101
x1=102
console.log(obj1)// {x:101}
4、手写深拷贝
- 注意判断值类型和引用类型
- 注意判断数组还是对象
- 递归
/**
* 深拷贝
* @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
}
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])
知识点:
变量计算——类型转换
- 字符串拼接
console.log(100 + '20') //10020
100 + 10 // 110
true + '10' // 'true10'
100=='100' // true
0=='' // true
0==false // true
false == '' // true
null == undefined // true
任何类型内容+字符串类型内容相当于字符串拼接,注意转换类型
- ==
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语句和逻辑运算
if语句
- truly变量:!!a === true的变量
if语句中只有truly变量才会执行 - falsely变量:!!a === false的变量
除了!!0、!!NaN、!!’’、!!null、!!undefined、!!false,其他都是truly变量
逻辑判断
- &&
- ||
- !
console.log(10 && 0) //0
console.log(0 && 10) //0
console.log('' || 'abc') //'abc'
console.log('abc' || '') //'abc'
console.log(!window.abc) //true
额外
说一下对变量提升的理解
console.log(a)
var a=100
fn('zhangsan')
function fn(name){
age=20;
console.log(name,age)
var age;
bar(100)
function bar(num){
console.log(num)
}
}
// 输出
// zhangsan 20
// 100
this指的是window
arguments指的是 这个集合体
二、原型和原型链
1、如何准确判断一个变量是不是数组
a instanceof Array
Array.isArray([]);
Object.prototype.toString.apply(b);//"[object Object]"
....
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)
})
}
// 扩展很多 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'))
3、class的原型本质,怎么理解
- 原型和原型链的图示
- 属性和方法的执行规则
知识点:
class
class是一个类,相当于模板,可以new一个类得到对象/实例
包含constructor、属性、方法
// 类
class Student {
constructor(name, number) {
this.name = name
this.number = number
}
sayHi() {
console.log(
`姓名 ${this.name} ,学号 ${this.number}`
)
}
}
// class是一个类,相当于模板,可以new一个类得到对象/实例
// 通过类 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()
扩展或重写方法
//手写简易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'))
类型判断(instanceof)
判断变量属于哪个class,属于哪个构造函数
xialuo instanceof Student //true
xialuo instanceof People //true
xialuo instanceof Object //true
xialuo instanceof Array //false
[] instanceof Array //true
[] instanceof Object //true
{} instanceof Object //true
原型
- typeof People === ‘function’ //class实际上是函数,可见是语法糖
- hasOwnProperty 判断是不是自己的属性
xialuo.hasOwnProperty('name') //true
xialuo.hasOwnProperty('saiHi') //false
xialuo.hasOwnProperty('hasOwnProperty') //false
- 隐式原型(proto ),显式原型(prototype)
原型关系:
- 每个 class 都有显示原型 prototype
- 每个实例都有隐式原型 proto
- 实例的__proto__指向对应 class 的 prototype
基于原型的执行规则:
- 获取属性 xiaoluo.name或执行方法 xialuo.sayHo()时
- 先在自身属性和方法寻找
- 如果找不到则自动去__proto__ 中查找
原型链
People.prototype === Student.prototype.__proto__
instanceof判断技巧:顺着变量的隐式原型一直往上找,看能不能对应到class的显式原型,能instanceof成立,不能返回false
额外
描述new一个对象的过程
// 构造函数
function Foo(name,age) {
this.name=name;
this.age=age;
this.class='class-1';
// return this // 默认有一行
}
var f=new Foo('zhangsan',20);
// var f1=new Foo('list',21) // 可以创建多个对象console.log(f);
构造函数—扩展
Var a={} 其实是var a=new Object()的语法糖
Var a=[] 其实是var a=new Array()的语法糖
Function Foo(){…} 其实是var Foo =new Function(…)
所有的引用类型(数组,对象,函数),都具有对象特性,即可自由扩展属性(除了”null”以外)
所有的引用类型(数组、对象、函数),都有一个__proto__属性,属性值是一个普通的对象
隐式原型
所有的函数,都有一个prototype属性,属性值也是一个普通的对象
显式原型
所有的引用类型(数组、对象、函数),__proto__属性(隐式原型)值指向它的构造函数的”prototype”属性值(显式原型)
当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的__proto__(即它的构造函数的prototype)中寻找
console.log(obj.__proto__)===Object.prototype
Zhangsan
弹出 Zhangsan
循环对象自身的属性
循环对象自身的属性
var item
for(item in f){
// 高级浏览器已经在for in中屏蔽了来自原型的属性
// 但是这里建议大家还是加上这个判断,保证程序的健壮性
if(f.hasOwnproperty(item)){
console.log(item)
}
}
写一个封装DOM查询的例子
面试时候问原型链实现的方法和方式(代码实现)实战中原型是怎么用的:
type:是click还是别的事件 fn:元素
写一个原型链继承的例子 这个写不出来面试肯定过不了
hashiqi 本身的prototype就有bark属性,在它的隐士原型 new Animal(),有eat属性,所以有两个属性
三、作用域和闭包
1、this 的不同应用场景,如何取值?(函数执行的时候确定,定义的时候不能确定)
- 当作普通函数被调用(返回Window)
- 使用 call apply bind (传入什么绑定什么)
- 作为对象方法调用 (返回对象本身)
- 在class 的方法中调用 (当前实例本身)
- 箭头函数 (找上级作用域中this的值)
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)
}
}
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工具
应用场景:实现一个简单的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>标签 点击时候弹出来对应的序号*/
/*这是一个错误的写法*/
var 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)
})
document.body.appendChild(a)
}
原因:i 为全局变量,click 点击时间只有点击才触发函数,此时for循环早已结束,i = 10, 所以点击任何一个都弹出 10.
/*创建10个<a>标签 点击时候弹出来对应的序号*/
/*这是正确的写法*/
var i
for(var i=0;i<10;i++){
(function (i) {
var a=document.createElement('a')
a.innerHTML=i+'<br>'
a.addEventListener('click',function (e) {
e.preventDefault()
alert(i)
})
document.body.appendChild(a)
})(i)
}
或者
// let 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)
})
document.body.appendChild(a)
}
此时 i 定义为for中块级作用域,每次for 循环都会形成一个块级作用域,每次点击在相应的块级作用域中找值。
知识点:
作用域
作用域指变量的合法的使用范围,包含
- 全局作用域
- 函数作用域
- 块级作用域( ES6 新增)
// es6块级作用域
if(true){
let x=100
}
console.log(x)// 会报错
自由变量
自由变量:当前作用域没有定义的变量
- 一个变量在当前作用域没有定义,但被使用了
- 向上级作用域,一层一层依次寻找,直至找到为止
- 如果到全局作用域都没有找到,则报错 xx is not defined
闭包
作用域应用的特殊情况,有两种表现:
- 函数作为参数被传递
- 函数作为返回值被返回
// 函数作为返回值
function create() {
const a = 100
//返回一个函数(函数作为返回值)
return function () {
console.log(a)/*自由变量,父作用域寻找100*/
}
}
// 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
// 所有的自由变量的查找,是在函数定义的地方,向上级作用域查找 // 不是在执行的地方!!!
自由变量查找规则:所有的自由变量的查找,是在函数定义的地方,向上级作用域查找,不是在执行的地方!!!
额外
// /*实际开发中闭包的应用*/
//闭包实际应用中主要用于封装变量、收敛权限
function isFirstLoad() {
var _list=[]
return function (id) {
if(_list.indexOf(id)>=0){
return false
}else{
_list.push(id)
return true
}
}
}
//使用
var firstLoad=isFirstLoad()
firstLoad(10)//true
firstLoad(10)//false
firstLoad(20)//true
firstLoad(20)//false
//你在isfirstLoad函数外面,根本不可能修改掉_list的值
// 作用域链
var a=100
function F1(){
var b=200
function F2(){
var c=300
console.log(a)
console.log(b)
console.lg(c)
}
F2()
}
F1()
// 作用链就是一个作用域 一直往上找。
// 输出:
// 100
// 200
// 300
function Foo(name){
this.name=name
}
var f=new Foo('zhangsan')
var obj={
name:'A',
printName:function(){
console.log(this.name)
}
obj.printName()
function fn(){
console.log(this)
}
fn()
//obj.printName()指向obj
// fn() 指向window
// call apply bind
function fn1(name,age){
alert(name)
console.log(this)
}
fn1.call({x:100},'zhangsan',20)
// 弹出zhangsan,this是{x:100}
var fn2=function(name,age){
alert(name)
console.log(this)
}.bind({y:200})
fn2('zhangsan'.20)
// 弹出 zhangsan this是Object{y:200}
// 无块级作用域
if(true){
var name='zhangsan'
}
console.log(name)
// 第5行 输出 zhangsan 。不建议将声明写在if判断里。写在里面和写在外边是一样的。
// es6块级作用域
if(true){ let x=100 }
console.log(x)
// 会报错
// 函数和全局作用域
var a=100
function fn(){
var a=200
console.log('fn',a)
}
console.log('global',a)
fn()
// fn 200
// global 100
this
场景:
- 作为普通函数去调用时值为window
- 使用call bind apply去调用,传入什么值为什么
- 作为对象方法被调用,返回对象本身
- 在class方法中调用,创建实例的本身
- 箭头函数:this永远取它上级作用域的this,它自己本身不会决定this值
this要在执行时才能确认值,定义时无法确认
// this
function fn1(){
console.log(this)
}
fn1()// window
fn1.call({x:100})// {x:100}
const fn2=fn1.bind({x:200})
fn2()// {x:200}
var a={
name:'A',
fn:function(){
console.log(this.name)
}
}
a.fn() // this===a
a.fn.call({name:'B'}) // this==={name:'B'}
var fn1=a.fn
fn1() // this===window
四、异步和单线程
1、手写用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 url = 'https://img.mukewang.com/5a9fc8070001a82402060220-140-140.jpg'
// loadImg(url).then(img => {
// console.log(img.width)
// return img
// }).then(img => {
// console.log(img.height)
// }).catch(ex => console.error(ex))
// 2张图片
const url1 = 'https://img.mukewang.com/5a9fc8070001a82402060220-140-140.jpg'
const url2 = 'https://img3.mukewang.com/5a9fc8070001a82402060220-100-100.jpg'
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(ex => console.error(ex))
2、前端使用异步的场景有哪些?
- 网络请求,如 ajax 图片加载,,动态img加载
- 定时任务,如 setTimeout setInverval
- 事件绑定
<!--img加载示例-->
console.log('start')
var img=document.createElement('img')
img.onload=function(){
console.log('loaded')
}
img.src='/xxx.png';
console.log('end')
// start
// end
// loaded
<!--事件绑定示例->
console.log('start') document.getElementById('btn1').addEventListener('click',function(){
alert('clicked')
})
console.log('end')
// start
// end
// clicked
知识点:
单线程和异步,异步和同步区别
- JS是单线程语言,只能同时做一件事
- 浏览器和NodeJs已支持JS启动进程,如Web Worker
- JS和DOM渲染共用同一个线程,因为JS可修改DOM结构
- 遇到等待(网络请求,定时任务)不能卡住
- 同步会阻塞代码的执行,所以需要异步,解决单线程等待的问题,不会阻塞后面代码的执行(Alert是同步,setTimeout是异步)
- 回调callback函数形式,setTimeout回调就是异步
// 异步 (callback 回调函数)
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
// 同步,alert点击确定之后才会输出300
console.log(100)
alert(200)
console.log(300)
前端异步的应用场景:
- 网络请求,如ajax、图片加载img.onload
- 定时任务,如setTimeout、setInterval
- 网络请求,如 ajax 图片加载
- 定时任务,如 setTimeout
callback hell 和 Promise (Promise 解决 callback hell)
- callback hell是回调嵌套的形式
- promise实现非嵌套的形式,管道串联的形式,解决callback hell问题,callback嵌套问题
//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)
}
})
})
}
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))
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 url = 'https://img.mukewang.com/5a9fc8070001a82402060220-140-140.jpg'
// loadImg(url).then(img => {
// console.log(img.width)
// return img
// }).then(img => {
// console.log(img.height)
// }).catch(ex => console.error(ex))
const url1 = 'https://img.mukewang.com/5a9fc8070001a82402060220-140-140.jpg'
const url2 = 'https://img3.mukewang.com/5a9fc8070001a82402060220-100-100.jpg'
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(ex => console.error(ex))
额外
日期和math:
获取2017-06-10格式的日期
获取随机数,要求是长度一致的字符串格式
写一个能遍历对象和数组的通用forEach函数
获取2017-06-10格式的日期
/*2017-06-10*/
function formatDate(dt) {
if (!dt) {
dt = new Date()
}
var year = dt.getFullYear();
var month = dt.getMonth() + 1;
var date = dt.getDate();
if (month < 10) {
month = '0' + month
}
if (date < 10) {
date = '0' + date
}
return year + '_' + month + '_' + date}
console.log(formatDate(new Date()))
获取随机数,要求是长度一致的字符串格式
var random = Math.random();
random = random + '0000000000';
random = random.slice(0, 10);
console.log(random)
写一个能遍历对象和数组的通用forEach函数
function forEach(obj,fn) {
if(obj instanceof Array){
obj.forEach(function(item,index){
fn(index,item)
})
}else{
for(key in obj){
if(obj.hasOwnProperty(key)){
fn(key,obj[key])
}
}
}}
var arr=[1,2,3];
forEach(arr,function(index,item){
console.log(index,item)})
var obj={x:100,y:200}
forEach(obj,function(key,val){
console.log(key,val)
})
五、JS异步进阶
1、请描述event loop(事件循环/事件轮询)的机制,可画图
2、什么是宏任务和微任务,两者有什么区别
3、Promise有哪三种状态?如何变化
4、promise then和catch的连接
// 第一题
Promise.resolve().then(() => {
console.log(1)
}).catch(() => {
console.log(2)
}).then(() => {
console.log(3)
})
// 1 3
// 第二题
Promise.resolve().then(() => {
console.log(1)
throw new Error('erro1')
}).catch(() => {
console.log(2)
}).then(() => {
console.log(3)
})
// 1 2 3
// 第三题
Promise.resolve().then(() => {
console.log(1)
throw new Error('erro1')
}).catch(() => {
console.log(2)
}).catch(() => { // 注意这里是 catch
console.log(3)
})
// 1 2
5、async/await 语法问题
async function fn() {
return 100
}
(async function () {
const a = fn() // ?? // promise对象
const b = await fn() // ?? // 100
})()
(async function () {
console.log('start')
const a = await 100
console.log('a', a)
const b = await Promise.resolve(200)
console.log('b', b)
const c = await Promise.reject(300)
console.log('c', c)
console.log('end')
})() // 执行完毕,打印出那些内容?//start a,100 b,200
6、Promise 和 setTimeout 顺序
console.log(100)
setTimeout(() => {
console.log(200)
})
Promise.resolve().then(() => {
console.log(300)
})
console.log(400)
// 100 400 300 200
7、async/await的执行顺序问题
async function async1 () {
console.log('async1 start') //2
await async2() // 这一句会同步执行,返回 Promise ,其中的 `console.log('async2')` 也会同步执行
console.log('async1 end') //6 await 后面的都作为回调内容---微任务
}
async function async2 () {
console.log('async2') //3
}
console.log('script start') //1
setTimeout(function () { // 异步,宏任务
console.log('setTimeout') //8
}, 0)
async1()
// 初始化 promise 时,传入的函数会立刻被执行
new Promise (function (resolve) {
console.log('promise1') // 4
resolve()
}).then (function () { // 异步,微任务
console.log('promise2') //7
})
console.log('script end') //5
// 同步代码执行完之后,屡一下现有的异步未执行的,按照顺序
// 1. async1 函数中 await 后面的内容 —— 微任务
// 2. setTimeout —— 宏任务
// 3. then —— 微任务
// 同步代码执行完毕 (event loop - call stack 被清空)
// 执行微任务
// (尝试触发DOM渲染)
// 触发Event Loop,执行宏任务
8、event loop
- JS是单线程运行的
- 异步要基于回调来实现
- event loop就是异步回调的实现原理
JS如何执行
- 从前到后,一行一行执行
- 如果某一行执行报错,则停止下面代码的执行
- 先把同步代码执行完,再执行异步
示例
console.log('Hi')
setTimeout(function cb1() {
console.log('cb1') // cb 即 callback
}, 5000)
console.log('Bye')
总结event loop的过程
- 同步代码,一行一行放在Call Stack调用栈执行
- 遇到异步,会先“记录”下,等待时机(定时、网络请求等)
- 时机到了,就会移动到Callback Queue
- 如Call Stack为空(即同步代码执行完)Event Loop开始工作
- 轮询查找Callback Queue,如有则移动到Call Stack执行
- 然后继续轮询查找(永动机一样)
DOM事件和event loop
- JS是单线程的
- 异步(setTimeOut,ajax等)使用回调,基于event loop
- Dom事件不是异步,但DOM事件也使用回调,基于event loop
//DOM 事件,也用 event loop
<button id="btn1">提交</button>
<script>
console.log('Hi')
$('#btn1').click(function (e) {
console.log('button clicked')
})
console.log('Bye')
</script>
9、promise进阶
三种状态
- pending resolved rejected
- pending 》resolved 或 pending 》rejected
- 变化不可逆
// 刚定义时,状态默认为 pending
const p1 = new Promise((resolve, reject) => {
})
console.log('p1',p1) // pending
// 执行 resolve() 后,状态变成 resolved
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
})
})
console.log('p2',p2) // pending - 开始打印时
setTimeout(() => console.log('p2-setTimeout',p2)) //resolved
// 执行 reject() 后,状态变成 rejected
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
reject()
})
})
console.log('p3',p3) // pending - 开始打印时
setTimeout(() => console.log('p3-setTimeout',p3)) //rejected
状态的表现和变化
- pending状态,不会触发then和catch
- resolved状态,会触发后续的then回调函数
- rejected状态,会触发后续的catch回调函数
const p1 = Promise.resolve(100)
console.log('p1',p1)
const p2 = Promise.reject('err')
console.log('p2',p2)
const p1 = Promise.resolve(100) //resolved
//console.log('p1',p1)
p1.then(data => {
console.log('data',data)
}).catch(err => {
console.error('err',err)
})
const p2 = Promise.reject('err') //rejected
//console.log('p2',p2)
p2.then(data => {
console.log('data2',data)
}).catch(err => {
console.error('err2',err)
})
then和catch对状态的影响(then和catch改变状态)
then catch 会继续返回 Promise ,此时可能会发生状态变化!!!
- then正常返回resolved,里面有报错则返回rejected
- catch正常返回resolved,里面有报错则返回rejected
const p1 = Promise.resolve().then(() => {
return 100
})
console.log('p1',p1) //resolved
const p2 = Promise.resolve().then(() => {
throw new Error('then error')
})
console.log('p2',p2) //rejected
const p1 = Promise.resolve().then(() => {
return 100
})
console.log('p1',p1) //resolved 触发后续then回调
p1.then(() => {
console.log('123')
})
const p2 = Promise.resolve().then(() => {
throw new Error('then error')
})
console.log('p2',p2) //rejected 触发后续catch回调
p2.then(() => {
console.log('456')
}).catch(err => {
console.error('err100',err)
})
const p3 = Promise.reject('my error').catch((err) => {
console.error('err)
})
console.log('p3',p3) //resolved 注意!触发then函数
p3.then(() => {
console.log(100)
})
const p4 = Promise.reject(('my error').catch((err) => {
throw new Error('catch err')
})
console.log('p4',p4) //rejected 触发后续catch回调
p4.then(() => {
console.log(200)
}).catch(()=> {
console.error('some err')
})
// 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')
})
// 第一题
Promise.resolve().then(() => {
console.log(1) //1
}).catch(() => {
console.log(2)
}).then(() => {
console.log(3) //3
})
// 第二题
Promise.resolve().then(() => { // 返回 rejected 状态的 promise
console.log(1) //1
throw new Error('erro1')
}).catch(() => { // 返回 resolved 状态的 promise
console.log(2) //2
}).then(() => {
console.log(3) //3
})
// 第三题
Promise.resolve().then(() => { // 返回 rejected 状态的 promise
console.log(1) //1
throw new Error('erro1')
}).catch(() => { // 返回 resolved 状态的 promise
console.log(2) //2
}).catch(() => {
console.log(3)
})
Promise总结
- 三种状态,状态的表现和变化
- then和catch对状态的影响
- then和catch的链式调用
10、async/await
- 异步调用callback hell
- Promise then catch链式调用,但也是基于回调函数
- async/await是同步语法实现异步,彻底消灭回调函数
function loadImg(src) {
const promise = new Promise((resolve, reject) => {
const img = document.createElement('img')
img.onload = () => {
resolve(img)
}
img.onerror = () => {
reject(new Error(`图片加载失败 ${src}`))
}
img.src = src
})
return promise
}
const src1 = 'http://www.imooc.com/static/img/index/logo_new.png'
const src2 = 'https://avatars3.githubusercontent.com/u/9583120'
(async function () {
// 注意:await 必须放在 async 函数中,否则会报错
// 加载第一张图片
const img1 = await loadImg(src1)
console.log(img1)
// 加载第二张图片
const img2 = await loadImg(src2)
console.log(img2)
})()
因为src2后面没加分号,和后面的()连在一起被当做函数,类似图示alert也会弹出弹框,解决办法:(async前面加!号
const src1 = 'http://www.imooc.com/static/img/index/logo_new.png'
const src2 = 'https://avatars3.githubusercontent.com/u/9583120'
!(async function () {
// 注意:await 必须放在 async 函数中,否则会报错
// 加载第一张图片
const img1 = await loadImg(src1)
console.log(img1)
// 加载第二张图片
const img2 = await loadImg(src2)
console.log(img2)
})()
//用同步的方式,编写异步。
async function loadImg1() {
const src1 = 'http://www.imooc.com/static/img/index/logo_new.png'
const img1 = await loadImg(src1)
return img1
}
async function loadImg2() {
const src2 = 'https://avatars3.githubusercontent.com/u/9583120'
const img2 = await loadImg(src2)
return img2
}
!(async function () {
// 注意:await 必须放在 async 函数中,否则会报错
try {
// 加载第一张图片
const img1 = await loadImg1() //await后面不仅可以加promise对象也可以加async函数
console.log(img1)
// 加载第二张图片
const img2 = await loadImg2()
console.log(img2)
} catch (ex) {
console.error(ex)
}
})()
async-await和Promise有什么关系
- async/await是消灭异步回调的终极武器
- 但和Promise并不互斥
- 反而,两者相辅相成
- 执行async函数,返回的是Promise对象,如果函数内没返回 Promise ,则自动封装成Promise对象
async function fn1() {
return new Promise(200)
}
const res1 = fn1()//执行async函数,返回的是一个Promise对象
res1.then(data => {console.log('data',data)}) //200
async function fn2() {
return 100 // 相当于 Promise.resolve(100)
}
console.log( fn2() ) //Promise对象
- await 相当于Promise的then,await 后面可以加promise对象、值、async函数的执行结果
// await 后面跟 Promise 对象:会阻断后续代码,等待状态变为 resolved ,才获取结果并继续执行
// await 后续跟非 Promise 对象:会直接返回
!(async function () {
const p1 = Promise.resolve(100)
const data = await p1 // await 相当于Promise的then
console.log('data',data) // 100
})()
!(async function () {
const data1 = await 400 // await Promise.resolve(400)
console.log('data1',data1)
})()
!(async function () {
const data2 = await fn1()
console.log('data2',data2)
})()
!(async function () {
const p1 = new Promise(() => {})
await p1
console.log('p1') // 不会执行
})()
!(async function () {
const p4 = Promise.reject('some err') //rejected状态
const res = await p4 // await -> then
console.log(res) // 不会执行
})()
- try…catch可捕获异常,代替了Promise的catch
!(async function () {
const p4 = Promise.reject('some err') //rejected状态
try {
const res = await p4
console.log(res)
} catch (ex) {
console.error(ex) // try...catch 相当于 promise catch
}
})()
//总结
async 封装 Promise
await 处理 Promise 成功
try...catch 处理 Promise 失败
async/await是语法糖,异步的本质还是回调函数
- async/await是消灭异步回调的终极武器
- JS是单线程,还得是有异步,还得是基于event loop
- async/await只是一个语法糖,但这颗糖真香
//只要遇到了 await ,后面的代码都相当于放在 callback 里
async function async1 () {
console.log('async1 start') // 2
await async2() //undefined
// await 的后面都可以看做是calllback里的内容,即异步
// 类似,event loop,setTimeout(cb1)
// setTimeout(function() { console.log('async1 end') })
// Promise.resolve().then(() => { console.log('async1 end') } ) //微任务/宏任务
console.log('async1 end') // 5 关键在这一步,它相当于放在 callback 中,最后执行
}
async function async2 () {
console.log('async2') // 3
}
console.log('script start') //1
async1() //立马执行async1函数体
console.log('script end') //4
// 同步代码已经执行完(event loop)
async function async1 () {
console.log('async1 start') // 2
await async2()
// 下面三行都是异步回调 callback的内容
console.log('async1 end') // 5
await async3()
console.log('async1 end 2')} //7
async function async2 () {
console.log('async2') // 3
}
async function async3 () {
console.log('async3') // 6
}
console.log('script start') //1
async1()
console.log('script end') //4
// 同步代码已经执行完(event loop)
for…of
- for…in(以及forEach for) 是常规的同步遍历
- for…of常用于异步的遍历
// 定时算乘法
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()
11、微任务microTask和宏任务macroTask
- 什么是宏任务,什么是微任务
- event loop和DOM渲染
- 微任务和宏任务的区别
console.log(100)
setTimeout(() => {
console.log(200)
})
Promise.resolve().then(() => {
console.log(300)
})
console.log(400)
// 100 400 300 200
宏任务和微任务
- 宏任务:setTimeout,setInterval,Ajax,DOM事件
- 微任务:Promise async/await
- 微任务执行时机比宏任务要早(先记住)
event loop和DOM渲染
- 再次回归一遍event loop的过程
- JS是单线程的,而且和DOM渲染共用一个线程
- JS执行的时候,得留一些时机供DOM渲染
- 回顾event loop过程(增加DOM渲染时机)
1.每次Call Stack清空(即每次轮询结束),即同步任务执行完,或者说异步代码推到Call Stack执行结束
2.都是DOM重新渲染的机会,DOM结构如有改变则重新渲染,(不一定非得渲染,就是给一次 DOM 渲染的机会!!!)
3.然后再去触发下一次Event Loop
const $p1 = $('<p>一段文字</p>')
const $p2 = $('<p>一段文字</p>')
const $p3 = $('<p>一段文字</p>')
$('#container')
.append($p1)
.append($p2)
.append($p3)
console.log('length', $('#container').children().length )
alert('本次 call stack 结束,DOM 结构已更新,但尚未触发渲染')
// (alert 会阻断 js 执行,也会阻断 DOM 渲染,便于查看效果)
// 到此,即本次 call stack 结束后(同步任务都执行完了),浏览器会自动触发渲染,不用代码干预
// 微任务:DOM渲染前触发
Promise.resolve().then(() => {
console.log('length1',$('#container').children().length) //3
alert('Promise then') //DOM渲染了吗?NO
})
// 宏任务:DOM渲染后触发
setTimeout(() => {
console.log('length2',$('#container').children().length) //3
alert('setTimeout') //DOM渲染了吗?yes
})
宏任务和微任务的区别
- 宏任务:DOM渲染后触发,如setTimeout
- 微任务:DOM渲染前触发,如Promise
从event loop解释,为何微任务执行更早
- 宏任务(浏览器规定的)
- 微任务(ES6语法规定的)
微任务:ES 语法标准之内,JS 引擎来统一处理。即,不用浏览器有任何关于,即可一次性处理完,更快更及时。
宏任务:ES 语法没有,JS 引擎不处理,浏览器(或 nodejs)干预处理。
六、JS-WEB-API
JS基础知识,规定语法(ECMA 262 标准)
JS Web API ,网页操作的API (W3C标准)
前者是后者的基础,两者结合才能真正实际应用
1、DOM
前言
- vue 和 React 框架应用广泛, 封装了 DOM 操作
- 但 DOM 操作一直都会是前端工程师的基础,必备知识
- 只会 vue 而不懂 DOM 操作的前端程序员,不会长久
DOM 操作文档对象模型
1)、DOM 是哪种数据结构
从HTML文件解析出来的一棵树
2)、DOM 操作的常用API
获取DOM节点,以及节点的property和Attribute
获取父节点,获取子节点
新增节点,删除节点
3)、DOM节点的Attribute和property有何区别
Property修改对象属性,不会体现到html结构中(尽量使用Property,而非Attribute)
Attribute修改html属性,会改变html结构
两者都有可能引起 DOM 重新渲染 (推荐用property,有重复操作可能不会引起不必要的渲染。DOM的重新渲染耗费性能)
4)、一次性插入多个DOM 节点,考虑性能
DOM操作非常‘昂贵’,避免频繁DOM操作
对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)
知识点:
DOM 节点操作
<body>
<div id="div1" class="container">
<p id="p1">一段文字 1</p>
<p>一段文字 2</p>
<p>一段文字 3</p>
</div>
<div id="div2">
<img src="https://img3.mukewang.com/5a9fc8070001a82402060220-100-100.jpg"/>
</div>
<ul id="list">
</ul>
<script src="./dom-3.js"></script>
</body>
// 1.获取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])
// 2.attribute
//通过修改或者获取js的属性来改变页面样式、页面渲染结构的一种形式,Dom结构js变量的修改
// const pList = document.querySelectorAll('p')
// console.log('pList', pList)
// const pList = document.querySelectorAll('p')
// const p1 = pList[0]
// 修改标签的属性,Dom结构节点属性修改
// p1.style.width = '100px'
// console.log( p1.style.width )// 获取样式
// p1.style.width = '100px' // 修改样式
// p1.className = 'red' // 修改 class
// console.log( p1.className ) // 获取 class
// console.log(p1.nodeName)
// console.log(p1.nodeType) // 1
// // 3.property 形式
// const pList = document.querySelectorAll('p')
// const p1 = pList[0]
// p1.setAttribute('data-name', 'baidu')
// console.log( p1.getAttribute('data-name') )
// p1.setAttribute('style', 'font-size: 50px;')
// console.log( p1.getAttribute('style') )
- 总结
property:修改对象属性,不会体现到html结构中
attribute:修改html属性,会改变html结构
两者都有可能引起DOM重新渲染
最好使用property
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); //还会含有text文本标签,nodeType 等于3
// 子节点会包括标签和文本, 利用 filter() 过滤
// text 的 nodeText = 3
const div1ChildNodesP = Array.prototype.slice.call(div1.childNodes)
.filter((child) => {
if (child.nodeType === 1) {
return true;
}
return false;
});
console.log("div1ChildNodesP", div1ChildNodesP);
// 删除节点
div1.removeChild(div1ChildNodesP[0]);
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)
2、BOM
BOM 操作(Browser Object Model)l浏览器对象模型
常考:
如何检测浏览器的类型 /navigator(浏览器信息)
Var ua=navigator.userAgent
Var isChrome=ua.indexOf(‘Chrome’)
Console.log(isChrome)
screen:屏幕大小 /(屏幕信息,例如宽度高度等)
console.log(screen.width);
console.log(screen.height);
// 分析拆解 url 各个部分/location(地址信息)
console.log(location.href)
console.log(location.protocol) //http:或https:
console.log(location.pathname) // '/learn/1234'
console.log(location.search)
console.log(location.hash)
console.log(location.host) 域名
// history:(前进后退信息)
history.back()
history.forward()
代码演示:
navigator.userAgent
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36'
location.protocol---协议
https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=baidu&wd=react&fenlei=256&rsv_pq=8a1172e50002b015&rsv_t=0115DKEg3U177v8B%2Fl9X8F%2B7mKJpeR8iorcE4T%2BNwOsH9sG0ATAVZIksDN1N&rqlang=en&rsv_enter=1&rsv_dl=tb&rsv_sug3=6&rsv_sug1=3&rsv_sug7=100&rsv_sug2=0&rsv_btype=i&inputT=2772&rsv_sug4=3033
// 输出:'https:'
location.host--域名
www.baidu.com
location.pathname
'/s'
3、事件
1) 事件绑定
const btn = document.getElementById('btn1')
btn.addEventListener('click', event => {
console.log('clicked')
})
//通用的绑定函数
function bindEvent(elem,type,fn){
elem.addEventListener(type,fn)
}
const btn1 = document.getElementById('btn1')
bindEvent(btn1, 'click', (e)=> {
// console.log(event.target) // 获取触发的元素
event.preventDefault() // 阻止默认行为
alert('clicked')
})
2)事件冒泡---从下向上进行冒泡
描述事件冒泡的流程
//描述事件冒泡的流程
基于DOM树形结构
事件会顺着触发元素往上冒泡
应用场景:代理----工作中经常用到
代理的好处
1.代码简洁
2、减少浏览器内存占用
<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('取消')
})
3)事件代理/编写一个通用的事件监听函数
面试让写了
- 代码简洁、减少浏览器内存占用、但是不要滥用
// event.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>事件 演示</title>
<style>
div {
border: 1px solid #ccc;
margin: 10px 0;
padding: 0 10px;
}
</style>
</head>
<body>
<button id="btn1">一个按钮</button>
<!-- <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> -->
<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 src="./event.js"></script>
</body>
</html>
// event.js
// 通用的事件绑定函数
// function bindEvent(elem, type, fn) {
// elem.addEventListener(type, fn)
// }
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)
})
// const p1 = document.getElementById('p1')
// bindEvent(p1, 'click', event => {
// event.stopPropagation() // 阻止冒泡
// console.log('激活')
// })
// const body = document.body
// bindEvent(body, 'click', event => {
// console.log('取消')
// // console.log(event.target)
// })
// const div2 = document.getElementById('div2')
// bindEvent(div2, 'click', event => {
// console.log('div2 clicked')
// console.log(event.target)
// })
无限下拉的图片列表,如何监听每个图片的点击
//无限下拉的图片列表,如何监听每个图片的点击
使用事件代理
用e.target获取触发元素
用matches来判断是否是触发元素
4、Ajax
1)手动编写一个ajax(面试常问)
function ajax(url,successFn) {
const xhr = new XMLHttpRequest()
xhr.open('GET', url, true)
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
successFn(xhr.responseText)
}
}
}
xhr.send(null)
}
// get 请求
const xhr = new XMLHttpRequest()
// /api 如 /data/text.json
// 第三个参数 表示是否异步,true 为异步请求 false 为同步
xhr.open("GET", "/api", true)
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// 转换成 json 格式
// console.log(
// JSON.parse(xhr.responseText)
// )
console.log(xhr.responseText)
} else {
console.log("其他情况")
}
}
}
xhr.send(null)
// post 请求
const xhr = new XMLHttpRequest()
xhr.open("POST", "/login", true)
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
console.log(xhr.responseText)
} else {
console.log("其他情况")
}
}
}
cosnt postData = {
userName: 'zhangsan',
passworad: 'xxx'
}
// post 请求发送字符串
xhr.send(JSON.stringify(postData))
// ajax.html文件---完整
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>ajax 演示</title>
</head>
<body>
<p>一段文字 1</p>
<script src="./ajax.js"></script>
</body>
</html>
// ajax.js文件
// XMLHttpRequest示例: 手动编写一个ajax
// const xhr = new XMLHttpRequest()
// xhr.open('GET', '/data/test.json', true)
// xhr.onreadystatechange = function () {
// 这里的函数异步执行,可参考之前JS基础的异步模板
// if (xhr.readyState === 4) {
// if (xhr.status === 200) {
// // console.log(
// // JSON.parse(xhr.responseText)
// // )
// alert(xhr.responseText)
// } else if (xhr.status === 404) {
// console.log('404 not found')
// }
// }
// }
// xhr.send(null)
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
跨域:同源策略,跨域解决方案
- 什么是跨域(同源策略)
//同源策略
//1.ajax请求时,浏览器要求当前网页和server必须同源(安全)
//2.同源:协议、域名、端口,三者必须一致
//3.前端:http://a.com:8080/;server:https://b.com/api/xxx(线上默认80端口)
//加载图片 css js 可无视同源策略
//1.<img src=跨域的图片地址 />
//2.<link href=跨域的css地址 />
//3.<script src=跨域的js地址></script>
//加载图片 css js 可无视同源策略
//1.<img />可用于统计打点,可使用第三方统计服务
//2.<link /><script />可使用CDN,CDN一般都是外域
//3.<script>可实现JSONP
//跨域
//1.所有的跨域,都必须经过server端允许和配合
//2.未经server端允许就实现跨域,说明浏览器有漏洞,危险信号
- JSONP
//访问https://www.taobao.com/,服务端一定返回一个html文件吗?
//服务器可以任意动态拼接数据返回,只要符合html格式要求
//同理于<script src="https://www.taobao.com/getData.js">
//<script>可绕过跨域限制
//服务器可以任意动态拼接数据返回
//所以,<script>就可以获得跨域的数据,只要服务端愿意返回
// jsonp.html
<body>
<p>一段文字 1</p>
<script>
window.abc = function (data) {
//这是我们跨域得到的信息
console.log(data)//{ name: 'xxx' }
}
</script>
<script src="http://localhost:8002/jsonp.js?username=xxx&callback=abc"></script>
</body>
// jsonp.js
abc(
{ name: 'xxx' }
)
//jQuery实现jsonp
$.ajax({
url:'http://localhost:8882/x-origin.json',
dataType:"jsonp",
jsonpCallback:"callback",
success:function(data){
console.log(data);
}
});
- CORS(服务端支持)--服务器设置http header
//第二个参数填写允许跨域的域名称,不建议直接写“*”
response.setHeader("Access-Control-Allow-Origin","http://locaohost: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")
3)状态码
readyState
- 0 - (未初始化) 还没有调用send() 方法
- 1 - (载入) 已调用 send() 方法,正在发送请求
- 2 - (载入完成) send() 方法执行完成,已经接受到全部响应内容
- 3 - (交互) 正在解析响应内容
- 4 - (完成) 响应内容解析完成,可以在客户端调用
状态码说明
- 1xx 服务器收到请求
- 2xx - 表示成功处理请求,如 200
- 3xx - 需要重定向,浏览器直接跳转,如 301 302 304
- 4xx - 客户端请求错误,如404 403
- 5xx - 服务器端错误
常见状态码
- 200 成功
- 301 永久重定向(配合location,浏览器自动处理)
- 302 临时重定向(配合location,浏览器自动处理)
- 304 资源未被修改,请求资源和之前资源一样
- 404 资源未找到
- 403 没有权限
- 500 服务器错误
- 504 网关超时,一台服务器请求另一台服务器时出错
5、存储
描述 cookie localStorage sessionStorage 区别
- 容量
- API 易用性
- 是否跟随 http 请求发送出去
| 特性 | cookie | localStorage | sessionStorage | indexDB |
|---|---|---|---|---|
| 数据生命周期 | 一般由服务器生成,可以设置过期时间 | 除非被清理,否则一直存在 | 页面关闭就清理 | 除非被清理,否则一直存在 |
| 数据存储大小 | 4K | 5M | 5M | 无限 |
| 与服务端通信 | 每次都会携带在 header 中,对于请求性能影响 | 不参与 | 不参与 | 不参与 |
从上表可以看到,cookie 已经不建议用于存储。如果没有大量数据存储需求的话,可以使用 localStorage 和 sessionStorage 。对于不怎么改变的数据尽量使用 localStorage 存储,否则可以用 sessionStorage 存储。
对于 cookie,我们还需要注意安全性。
| 属性 | 作用 |
|---|---|
| value | 如果用于保存用户登录态,应该将该值加密,不能使用明文的用户标识 |
| http-only | 不能通过 JS 访问 Cookie,减少 XSS 攻击 |
| secure | 只能在协议为 HTTPS 的请求中携带 |
| same-site | 规定浏览器不能在跨域请求中携带 Cookie,减少 CSRF 攻击 |
cookie:
本身用于浏览器和server通讯
被“借用”到本地存储来
可用document.cookie = '...'来修改
document.cookie = 'a=100;b=200;'
document.cookie //a=100
document.cookie = 'b=200;'
document.cookie //a=100; b=200
document.cookie = 'a=300;'
document.cookie //b=200; a=300
document.cookie = 'd=400;'
document.cookie //b=200; a=300;d=400;
每次添加一条数据,相同的key 会覆盖,不同的key 会追加。
有限制,不太好用
//cookie缺点
存储大小,最大4KB
http请求时需要发送到服务端,增加请求数据量
只能用document.cookie = '...'来修改,太过简陋
localStorage 和 sessionStorage
HTML5专门为存储而设计,最大可存5M
API简单易用setItem,getItem
不会随着http请求被发送出去
七、HTTP协议
http methods
传统的methods
- get请求服务器数据
- post向服务器提交数据
- 简单的网页功能,就这两个操作
现在的methods
- get获取数据
- post新建数据
- patch/put更新数据
- delete删除数据
什么是Restful API
一种新的API设计方法
传统API设计:把每个url当做一个功能
Restful API设计:把每个url当做一个唯一的资源
如何设计成一个资源
1.尽量不用url参数
传统的API设计:/api/list?pageIndex=2
Restful API设计:/api/list/2
2.用method表示操作类型
- 传统的API设计:
post请求:/api/create-blog
post请求:/api/update-blog?id=100
get请求:/api/get-blog?id=100
- Restful API设计:
post请求:/api/blog
patch请求:/api/blog/100
get请求:/api/blog/100
http常见的header有哪些
常见的Request Headers
Accept 浏览器可接收的数据格式
Accept-Encoding 浏览器可接收的压缩算法,如gzip
Accept-Languange 浏览器可接收的语言 ,如zh-CN
Connection:keep-alive一次TCP连接重复使用
cookie
Host
User-Agent(简称UA)浏览器信息
Content-type 发送数据的格式,如application/json
常见的Response Headers
Content-type 返回数据的格式,如application/json
Content-length 返回数据的大小,多少字节
Content-Encoding 返回数据的压缩算法,如gzip
Set-Cookie
自定义header
headers:{"X-Requested-With":"XMLHttpRequest"}
缓存相关的headers
Cache-Control
Expires
Last-Modified
If-Modified-Since
Etag
If-None-Match
http缓存
http缓存攻略(强制缓存 + 协商缓存)
刷新操作方式,对缓存的影响
静态资源(js、css、img)可以被缓存
描述一下http的缓存机制(重要)
强制缓存
缓存过期
请求缓存的情况
Cache-Control
Response Headers中
控制强制缓存的逻辑
例如Cache-Control:max-age=31536000(单位是秒)
Cache-Control的值
max-age
no-cache:不用强制缓存,我们到服务端去请求,服务端怎么处理我们不管
no-store:我们不用强制缓存,而且我们也不用服务端的一些缓存措施,让服务端简单粗暴的将资源再返回我一份
private:最终用户做缓存,比如电脑,浏览器,手机
public:中间的路由,中间的代理也可以作为缓存
关于Expires
同在Response Headers中
同为控制强制缓存的过期
已被Cache-Control代替
协商(对比)缓存
- 服务器端缓存策略:服务端来判断一个资源是不是被缓存
- 服务器判断客户端资源,是否和服务端资源一样
- 一致则返回304,否则返回200和最新的资源
资源标识
在Response Headers中,有两种
Last-Modified 资源的最后修改时间
Etag 资源的唯一标识 (一个字符串,类似人类的指纹)
Last-Modified
Etag
Headers示例
请求示例
Last-Modified和Etag
会优先使用Etag
Last-Modified只能精确到秒级,而计算机大多毫秒级以内
如果资源被重复生成,而内容不变,则Etag更精确
http缓存综述流程图
刷新页面对http缓存的影响
三种刷新操作
- 正常操作:地址栏输入url,跳转链接,前进后退等
- 手动刷新:F5,点击刷新按钮,点击菜单刷新
- 强制刷新:Ctrl + F5
不同刷新操作,不同的缓存策略
- 正常操作:强制缓存有效,协商缓存有效
- 手动刷新:强制缓存失效,协商缓存有效
- 强制刷新:强制缓存失效,协商缓存失效
八、运行环境
1、页面加载
- 即浏览器
- 下载网页代码,渲染出页面,期间会执行若干JS
- 要保证代码在浏览器中,稳定且高效
1)、从输入 url 到渲染出页面的整个过程
浏览器根据DNS服务器得到域名的IP地址
向这个IP的机器发送http请求
服务器收到、处理并返回http请求
浏览器得到返回内容
2)、window.onload 和 DOMContentLoaded 的区别
Window.onload:页面的全部资源加载完才会执行,包括图片、视频等
DOMContentLoaded:DOM渲染完即可执行,此时图片、视频还没有加载完
window.addEventListener('load', function () {
// 页面的全部资源加载完才会执行,包括图片、视频等
console.log('window loaded')
})
document.addEventListener('DOMContentLoaded', function () {
// DOM渲染完即可执行,此时图片、视频还没有加载完
console.log('dom content loaded')
})
知识点
加载资源的过程
- DNS解析(域名服务解析):域名->IP地址,域名统一好记,IP地址不同区域因代理可能不一致不好标识,要转IP地址是因为浏览器真正访问时访问的是IP地址
- 浏览器根据IP地址向服务器发起http请求
- 服务器处理http请求,并返回给浏览器
渲染页面的过程
- 根据 HTML 代码生成 DOM Tree
- 根据 CSS 代码生成 CSSOM
- 将 DOM Tree 和CSSOM 整合生成 Render Tree
- 根据 Render Tree 渲染页面
- 遇到 < script> 则暂停渲染,优先加载并执行 JS 代码,完成在继续
- 直至把 Render Tree 渲染完成
2、性能优化
是一个综合性问题,没有标准答案,但要求尽量全面
细节问题:手写防抖、节流
1)性能优化原则
- 多使用内存、缓存或其他方法
- 减少CPU计算量,减少网络加载耗时
- 适用于所有编程的性能优化,空间换时间
2)方法 ( 两个方面)
a.让加载更快
- 减少资源体积,压缩代码,例如:webpack,服务器端也会进行gzip压缩,大约会压缩三分之一
- 减少访问次数,合并代码(精灵图、雪碧图、webpack),SSR服务端渲染,缓存(webpack的output加contenthash产生数字文件主要有这个效果)
- 使用更快的网络,CDN
缓存
- 静态资源加hash后缀,根据文件内容计算hash;
- 文件内容不变,则hash不变,则url不变
- url和文件不变,则会自动触发http缓存机制,返回304
SSR
- 服务端渲染:将网页和数据一起加载,一起渲染
- 非SSR(前后端分离):先加载网页,再加载数据,再渲染数据
- 早先的JSP、ASP、PHP,现在的Vue React SSR借助一些Node的能力来做
b.让渲染更快
- css放在head中,js放在body最后
- 尽早开始执行JS,用DOMContentLoaded触发
- 懒加载 (图片懒加载,上滑加载更多)
- 对 DOM 查询进行缓存,for循环中,先缓存DOM查询结果,缓存length
- 频繁 DOM 操作,合并到一起插入 DOM 结构
懒加载:
懒加载
// const img = document.getElementById('img1')
// img1.src=img1.getAttribute('data-realsrc')
缓存 DOM 查询
多个 DOM 操作一起插入到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)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>运行环境 演示</title>
</head>
<body>
<p>一段文字 1</p>
<p>一段文字 2</p>
<p>一段文字 3</p>
<img
id="img1"
src="https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1570191150419&di=37b1892665fc74806306ce7f9c3f1971&imgtype=0&src=http%3A%2F%2Fimg.pconline.com.cn%2Fimages%2Fupload%2Fupc%2Ftx%2Fitbbs%2F1411%2F13%2Fc14%2F26229_1415883419758.jpg"
/>
<script src="./index.js"></script>
</body>
</html>
const img1 = document.getElementById('img1')
img1.onload = function () {
console.log('img loaded')
}
//页面的全部资源加载完才会执行,包括图片、视频等
window.addEventListener('load', function () {
console.log('window loaded')
})
//DOM渲染完即可执行,此时图片、视频可能还没有加载完
document.addEventListener('DOMContentLoaded', function () {
console.log('dom content loaded')
})
3)节流 throttle 防抖 debounce (用户体验)
当一个函数被频繁、无限制的被调用的时候,会加重浏览器的负担,造成浏览器卡顿的现象。
例如:常用浏览器滚动scroll,鼠标移动onmousemove事件触发的事件。因此引入了防抖和节流两个概念来解决这个问题。
防抖 debounce
场景
- 监听一个输入框,文字变化后触发change事件
- 直接用keyup事件,会频发触发change事件
- 防抖:用户输入结束或暂停时,才会触发change事件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>debounce 演示</title>
</head>
<body>
<input type="text" id="input1">
<script src="./debounce.js"></script>
</body>
</html>
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事件,则会频繁触发,很容易导致卡顿
- 节流:无论拖拽速度多快,都会每个100ms触发一次
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>throttle 演示</title>
<style>
#div1 {
border: 1px solid #ccc;
width: 200px;
height: 100px;
}
</style>
</head>
<body>
<div id="div1" draggable="true">可拖拽<div>
<script src="./throttle.js"></script>
</body>
</html>
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) {
})
节流:当持续触发事件时,每隔指定时间触发一次事件处理函数。当然,是你触发事件的时间小于你设置的每隔指定时间。
通俗解决:“匀速等距离”触发事件处理函数
总结:
性能优化的方案有哪些?
1.加载资源优化
2.静态资源的压缩合并 (多个js压缩合并成一个)
3.静态资源缓存 (本地缓存)
4.使用CDN让资源加载更快(比如在北京访问,会访问北京的机房。没有CDN在北京也得去访问杭州的机房)
5.使用SSR(服务端)后端渲染,数据直接输出到HTML中
渲染优化
CSS放前面,JS放后面
懒加载(图片懒加载,下拉记载更多)
减少DOM查询,对DOM查询做缓存
减少DOM操作,多个操作尽量合并在一起执行(比如查询做缓存,插入也别一次性插入)
事件节流
尽早执行操作(如DOMContentLoaded)
渲染&加载
加载一个资源的过程
浏览器根据DNS服务器得到域名的IP地址
向这个IP的机器发送http请求
服务器收到、处理并返回http请求
浏览器得到返回内容
浏览器渲染页面的过程 会考
根据HTML结构生成DOM Tree
根据CSS生成CSSOM
将DOM和CSSOM整合形成RenderTree
根据RenderTree开始渲染和展示
遇到<script>时,会执行并阻塞渲染
为什么css要放在head中?
如果放在body中,要降低用户的体验,那样html会自己渲染一遍之后再去发现css再去渲染,
这样就需要考虑到电脑的配置、网络的情况以及文件内容结构的大小等情况,有可能会出现几毫秒的卡顿情况。
为什么js要放在body中?
1、 不会阻塞,让页面更快渲染出来
2、 放上面的话,会阻塞。
3、安全
问题:常见的 web 前端攻击方式有哪些?
XSS跨站请求攻击
XSRF跨域请求伪造
XSS
再新浪微博写一篇文章,同时偷偷插入一段`<script>` 攻击代码中,获取cookie,
发送自己的服务器 发布博客,有人查看博客内容
会把查看着者的cookie发送到攻击者的服务器
解决: 前端替换关键字,例如替换< 为< > 为> -------前端替换会稍微影响性能。 后端替换(后端替换比较好)
安全性XSRF 跨站请求伪造
XSFR
你已登录一个购物网站,正在浏览商品 该网站付费接口是xxx.com/pay?id=100 但是没有任何验证
然后你收到一封邮件,隐藏着<img src=xxx.com/pay?id=100> 你查看邮件的时候,就已经悄悄的付费购买了
解决方法:
增加验证流程,如输入指纹、密码、短信验证码
1. XSS
场景
1、一个博客网站,我发表一篇博客,其中嵌入<script>脚本
2、脚本内容:获取cookie,发布到我的服务器,(服务器配合跨域)
3、发布这篇博客,有人查看它,我轻松收割访问者的cookie
XSS预防
1、替换特殊字符,如<变成<>变为>
2、<script>变为<script>,直接显示,而不会作为脚本执行
3、前端要替换,后端也要替换,都做总不会有错
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>xss 演示</title>
</head>
<body>
<p>一段文字1</p>
<p>一段文字2</p>
<p>一段文字3</p>
<script>alert(document.cookie);</script>
</body>
</html>
2. XSRF跨站请求伪造
场景:
1.你正在购物,看中了某个商品,商品id是100
2.付费接口是xxx.com/pay?id=100,但没有任何验证
3.我是攻击者,看中了一个商品,id是200
4.我向你发送一封电子邮件,邮件标题很吸引人
5.但邮件正文隐藏着<img src=xxx.com/pay?id=200 />
6.你一查看邮件就帮我购买了id是200的商品
XSRF预防
1.使用POST接口
2.增加验证,例如密码、短信验证码、指纹等
九、模块化
1.使用模块化有什么优势?
2.AMD 和CMD区别?
AMD和CommonJS的使用场景
需要异步加载JS,使用AMD
使用了npm之后建议使用CommonJS
3.构建工具有什么区别?为什么用webpack?
4.webpack的plugins有哪些,如何使用 什么意思?
5.webpack如何配置
6.webpack的各种hash有什么区别?
比如contenthash
十、JS初级面试真题
1.何为变量提升
var和let const的区别
- var是ES5语法,let const是ES6语法,var有变量提升
- var和let是变量,可修改;const是常量,不可修改
- let const有块级作用域,var没有
/ // 变量提升 ES5
// console.log(a) // undefined
// var a = 200
// var a
// console.log(a) // undefined
// a = 200
// 块级作用域
for (let i = 0; i < 10; i++) {
let j = i + 1
}
console.log(j)
typeof返回哪些类型
- undefined string number boolean symbol
- object(注意,typeof null === ‘object’)
- function
列举强制类型转换和隐式类型转换
- 强制:parseInt parseFloat toString等
- 隐式:if、逻辑运算、==、+拼接字符串
2.手写深度比较 isEqual
手写深度比较,模拟lodash isEqual
// 判断是否是对象或数组
function isObject(obj) {
return typeof obj === 'object' && obj !== null
}
// 全相等(深度)
function isEqual(obj1, obj2) {
if (!isObject(obj1) || !isObject(obj2)) {
// 值类型(注意,参与 equal 的一般不会是函数)
return obj1 === obj2
}
if (obj1 === obj2) {
return true
}
// 两个都是对象或数组,而且不相等
// 1. 先取出 obj1 和 obj2 的 keys ,比较个数
const obj1Keys = Object.keys(obj1)
const obj2Keys = Object.keys(obj2)
if (obj1Keys.length !== obj2Keys.length) {
return false
}
// 2. 以 obj1 为基准,和 obj2 一次递归比较
for (let key in obj1) {
// 比较当前 key 的 val —— 递归!!!
const res = isEqual(obj1[key], obj2[key])
if (!res) {
return false
}
}
// 3. 全相等
return true
}
// 测试
const obj1 = {
a: 100,
b: {
x: 100,
y: 200
}
}
const obj2 = {
a: 100,
b: {
x: 100,
y: 200
}
}
// console.log( obj1 === obj2 )
console.log( isEqual(obj1, obj2) )
const arr1 = [1, 2, 3]
const arr2 = [1, 2, 3, 4]
split() 和join()的区别
'1-2-3'.split('-') //[1,2,3]
[1,2,3].join('-')
数组pop push unshift shift分别做什么
// const arr = [10, 20, 30, 40]
// // pop
// const popRes = arr.pop()
// console.log(popRes, arr) 40 [10, 20, 30]
// // shift
// const shiftRes = arr.shift()
// console.log(shiftRes, arr) 10 [20, 30, 40]
// // push
// const pushRes = arr.push(50) // 返回 length
// console.log(pushRes, arr) 5 [10, 20, 30, 40, 50]
// // unshift
// const unshiftRes = arr.unshift(5) // 返回 length
// console.log(unshiftRes, arr) 5 [5, 10, 20, 30, 40]
数组的API,有哪些是纯函数
// // 纯函数:1. 不改变源数组(没有副作用);2. 返回一个数组
// 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
3.你是否真的会用数组map
数组slice和splice的区别
- 功能区别(slice - 切片,splice - 剪接)
- 参数和返回值
- 是否纯函数
// const arr = [10, 20, 30, 40, 50]
// // slice 纯函数
// const arr1 = arr.slice()
//console.log(arr1,arr) [10, 20, 30, 40, 50],[10, 20, 30, 40, 50]
// const arr2 = arr.slice(1, 4)
//console.log(arr2,arr) [20, 30, 40],[10, 20, 30, 40, 50]
// const arr3 = arr.slice(2)
//console.log(arr3,arr) [30, 40, 50],[10, 20, 30, 40, 50]
// const arr4 = arr.slice(-3)
// console.log(arr4,arr) [30, 40, 50],[10, 20, 30, 40, 50]
// // splice 非纯函数
// const spliceRes = arr.splice(1, 2, 'a', 'b', 'c')
//console.log(spliceRes,arr) [20, 30],[10, "a", "b", "c", 40, 50]
// const spliceRes1 = arr.splice(1, 2)
// console.log(spliceRes1, arr) [20, 30], [10, 40, 50]
// const spliceRes2 = arr.splice(1, 0, 'a', 'b', 'c')
// console.log(spliceRes2, arr) [],[10, "a", "b", "c", 20, 30, 40, 50]
[10,20,30].map(parseInt)返回结果是什(网红题)
- map的参数和返回值
- parseInt参数和返回值
const res = [1, 2, 3].map(parseInt)
console.log(res) //[1, NaN, NaN]
// 拆解
[1, 2, 3].map((num, index) => {
return parseInt(num, index)
})
//parseInt(string, radix) 函数将给定的字符串以指定基数(radix/base)解析成为整数。就是 你想把string当成radix进制数解析成10进制
//radix传入0时会把1当成是10进制数,所以“1”成功了。
//radix传入1时...没有1进制数,所以不可能转换成功,返回NaN
//radix传入2时,"3"不能当作2进制数处理所以也返回NaN
ajax请求get和post的区别
- get一般用于查询操作,post一般用户提交操作
- get参数拼接在url上,post放在请求体内(数据体积可更大)
- 安全性:post易于防止CSRF
4.再学闭包
函数call和apply的区别
fn.call(this,p1,p2,p3)
fn.apply(this,arguments)
事件代理(委托)是什么
闭包是什么?有什么特效?有什么负面影响
- 回顾作用域和自由变量
- 回顾闭包应用场景:作为参数被传入,作为返回值被返回
- 回顾:自由变量的查找,要在函数定义的地方(而非执行的地方)
- 影响:变量会常驻内存,得不到释放,闭包不要乱用
// // 自由变量示例 —— 内存会被释放
// let a = 0
// function fn1() {
// let a1 = 100
// function fn2() {
// let a2 = 200
// function fn3() {
// let a3 = 300
// return a + a1 + a2 + a3
// }
// fn3()
// }
// fn2()
// }
// fn1()
// // 闭包 函数作为返回值 —— 内存不会被释放
// function create() {
// let a = 100
// return function () {
// console.log(a)
// }
// }
// let fn = create()
// let a = 200
// fn() // 100
function print(fn) {
let a = 200
fn()
}
let a = 100
function fn() {
console.log(a)
}
print(fn) // 100
5.回顾DOM操作和优化
如何阻止时间冒泡和默认行为
event.stopProppagation()
event.preventDefault()
查找、添加、删除、移动DOM节点的方法
基础中的基础,不再演示,可回顾之前的章节
如何减少DOM操作
- 缓存DOM查询结果
- 多次DOM操作,合并到一次插入
6.jsonp本质是ajax吗
解释jsonp的原理,为何它不是真正的ajax
- 浏览器的同源策略(服务端没有同源策略)和跨域
- 哪些html标签能绕过跨域
- jsonp的原理
document load和ready的区别
两等和三等的不同
- 两等会尝试类型转换
- 三等严格相等
- 哪些场景才用两等?
7.是否用过Object.create()
函数声明和函数表达式的区别
- 函数声明function fn(){…}
- 函数表达式 const fn = function(){…}
- 函数声明会在代码执行前预加载,而函数表达式不会
// // 函数声明
// const res = sum(10, 20)
// console.log(res) 30
// function sum(x, y) {
// return x + y
// }
// // 函数表达式
// var res = sum(10, 20) sum is not a function
// console.log(res)
// var sum = function (x, y) {
// return x + y
// }
new Object() 和 Object.create()的区别(网红题)
- {}等同于new Object(),原型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
}
})
//console.log(obj1===obj2) false
const obj21 = new Object(obj1)
//console.log(obj21===obj1) true
const obj3 = Object.create(null)
//console.log(obj3) 返回空对象,没有原型,没有属性
const obj4 = new Object()
//console.log(obj4) 返回空对象{},有原型
const obj5 = Object.create({
a: 10,
b: 20,
sum() {
return this.a + this.b
}
})//创建一个空对象,把空对象原型指向传入的对象
//console.log(obj5===obj1) false
const obj6 = Object.create(obj1)
//console.log(obj6===obj1) false
//console.log(obj6.__proto__===obj1) true
obj1.c = 1000;
console.log(obj6.c) //1000
关于this的场景题
const User = {
count:1,
getCount:function(){
return this.count;
}
}
console.log(User.getCount()) //1
const func = User.getCount
console.log(func()) //undefined,this指window
8.常见的正则表达式
关于作用域和自由变量的场景题 - 1
let i;
for(i=1;i<=3;i++){
setTimeout(function(){
console.log(i)
},0)
}
判断字符串以字母开头,后面字母数字下划线,长度6-30
const reg = /^[a-zA-Z]\w{5, 29}$/
学习正则表达式的规则
手写常见的正则表达式
// 邮政编码
/\d{6}/
// 小写英文字母
/^[a-z]+$/
// 英文字母
/^[a-zA-Z]+$/
// 日期格式 2019.12.1
/^\d{4}-\d{1,2}-\d{1,2}$/
// 用户名
/^[a-zA-Z]\w{5, 17}$/
// 简单的 IP 地址匹配
/\d+\.\d+\.\d+\.\d+/
关于作用域和自由变量的场景题 - 2
let a = 100
function test(){
alert(a) //100
a = 10
alert(a) //10
}
test()
alert(a) //10
9.如何获取最大值
手写字符串trim方法,保证浏览器兼容性
String.prototype.trim = function(){
return this.replace(/^\s+/,'').replace(/\s+$/,'')
}
//原型、this、正则表达式
如何获取多个数字中的最大值
//方式一
function max(){
const nums = Array.prototype.slice.call(arguments) //变为数组
let max = 0
nums.forEach(n => {
if(n > max){
max = n
}
})
return max
}
//方式二
Math.max(10,30,20,40)
//以及Math.min
如何用JS实现继承
- class继承
- prototype继承
10.解析url参数
如何捕获JS程序中的异常
try{
//todo
}catch(ex){
console.log.error(ex) //手动捕获catch
}finally{
//todo
}
//自动捕获
window.onerror = function(message,source,lineNum,colNum,error){
//第一,对跨域的js,如CDN的,不会有详细的报错信息
//第二,对于压缩的js,还要配合sourceMap反查到未压缩代码的行、列
}
什么是JSON
- json是一种数据格式,本质是一段字符串
- json格式和JS对象结构一致,对JS语言更友好
- window.JSON是一个全局对象:JSON.stringify JSON.parse
key和value必须用双引号(除非是bool或者数字)
获取当前页面url参数
- 传统方式,查找location.search
- 新API,URLSearchParams
// // 传统方式
// function query(name) {
// const search = location.search.substr(1) // 类似 array.slice(1)
// // search: 'a=10&b=20&c=30'
// const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`, 'i')
// const res = search.match(reg)
// if (res === null) {
// return null
// }
// return res[2]
// }
// query('d')
// URLSearchParams 浏览器兼容性问题
function query(name) {
const search = location.search
const p = new URLSearchParams(search)
return p.get(name)
}
console.log( query('b') )
10.数组去重有几种方式
将url参数解析为JS对象
//传统方式,分析search
function queryToObj(){
const res = {}
const search = location.search.substr(1) //去掉前面的?号
search.split('&').forEach(paramStr => {
const arr = paramStr.split('=')
const key = arr[0]
const val = arr[1]
res[key] = val
})
return res
}
//使用URLSearchParams
function queryToObj(){
const res = {}
const pList = new URLSearchParams(location.search)
pList.forEach((val,key) => {
res[key] = val
})
return res
}
手写数组flatern,考虑多层级
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)
return flat(res) // 递归
}
const res = flat( [1, 2, [3, 4, [10, 20, [100, 200]]], 5] )
console.log(res) //[1,2,3,4,10,20,100,200,5]
数组去重
- 传统方式,遍历元素挨个比较、去重
- 使用Set
- 考虑计算效率
// // 传统方式
// function unique(arr) {
// const res = []
// arr.forEach(item => {
// if (res.indexOf(item) < 0) {
// res.push(item)
// }
// })
// return res
// }
// 使用 Set (无序,不能重复)
function unique(arr) {
const set = new Set(arr)
return [...set]
}
const res = unique([30, 10, 20, 30, 40, 10])
console.log(res)
11.是否用过requestAnimationFrame
手写深拷贝
/**
* 深拷贝
* @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
}
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])
//注意Object.assign不是深拷贝
const obj = {a:10,b:20,c:30}
Object.assign(obj,{d:40})
{a:10,b:20,c:30,d:40}
console.log(obj) //{a:10,b:20,c:30,d:40}
const obj1 = Object.assign({},obj,{e:50})
console.log(obj) //{a:10,b:20,c:30,d:40}
console.log(obj1) //{a:10,b:20,c:30,d:40,e:50}
obj.a = 100
console.log(obj1) //{a:10,b:20,c:30,d:40,e:50}
const obj = {a:10,{x:100,y:100}}
const obj1 = Object.assign({},obj,{c:30})
console.log(obj) //{a:10,{x:100,y:100}}
console.log(obj1) //{a:10,{x:100,y:100},c:30}
obj.a = 100
console.log(obj) //{a:100,{x:100,y:100}}
nsole.log(obj1) //{a:10,{x:100,y:100},c:30}
obj.b.x = 101
console.log(obj1.b.x) //101
介绍一下RAF requestAnimationFrame
- 要想动画流畅,更新频率要60帧/s,即16.67ms更新一次视图
- setTimeout要手动控制频率,而RAF浏览器会自动控制
- 后台标签或隐藏iframe中,RAF会暂停,而setTimeout依然执行
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>JS 真题演示</title>
<style>
#div1 {
width: 100px;
height: 50px;
background-color: red;
}
</style>
</head>
<body>
<p>JS 真题演示</p>
<div id="div1"></div>
<script src="https://cdn.bootcss.com/jquery/3.4.0/jquery.js"></script>
<script src="./RAF.js"></script>
</body>
</html>
// 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()
前端性能如何优化?一般从哪几个方面考虑?
- 原则:多使用内存、缓存,减少计算、减少网络请求
- 方向:加载页面,页面渲染,页面操作流畅度