常见面试题总结(一)
什么是原型?
- 原型分为两种:构造函数原型,称为prototype;实例(对象)原型称为proto
原型之间的关系?
- 由构造函数创建出来的实例对象上,proto属性指向了构造函数的prototype属性
构造函数和构造器是什么?
- js中,默认每一个(正常定义的)函数,都是构造器,也称为类,那么构造函数上的原型链上有
ƒ Function() { [native code] }
这个属性,所以可以通过new 方法创建实例,构造函数原型prototype上有一个constructor构造器属性,指向了构造函数自身 - 能够称为构造器的关键在于拥有--->prototype属性(实例没有)
function Fa() {}
Fa.constructor
ƒ Function() { [native code] }
- 被new出来的实例,由于不是一个构造器,所以无法通过new创建新的实例--->没有prototype属性
const son = new Fa()
son.constructor
// ƒ Fa() {}
const subSon = new son()
// VM5278:1 Uncaught TypeError: son is not a constructor
at <anonymous>:1:16
- 一张图描述清楚构造器想关
- 有些人可能会把
__proto__
和prototype
搞混淆。从翻译的角度来说,它们都可以叫原型,但是其实是完全不同的两个东西。
__proto__
存在于所有的对象上,prototype
存在于所有的函数上,他俩的关系就是:函数的 prototype
是所有使用 new 这个函数构造的实例的 __proto__
。函数也是对象,所以函数同时有 __proto__
和prototype
。
作者:余腾靖
链接:https://juejin.im/post/5e2ff7dce51d4558021a1a4d
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
原型链是什么?
- 实例拥有的实例原型对象proto属性,指向了构造函数原型对象prototype属性,而构造函数原型对象prototype上的proto属性也指向了父构造函数的原型prototype,以此类推,通过proto建立起来的指向关系就被称为原型链
原型链的作用?
- 实例访问一个属性的时候,先在自身可遍历属性上寻找,再到原型链上逐层向上寻找,如果找不到会返回undefined
- stu.proto.proto.proto,直到遇到null,则返回undefined
- 作用:将类的方法定义在原型上,通过这种方式,所有的实例都可以访问到这个方法,并且这个方法只需要占用一份内存,节省内存,this 的指向还能正确指向类的实例。
- 注意:这种方法定义的属性,由于不是自身属性,所以无法通过
Object.keys()
枚举
function Engineer(workingYears) {
this.workingYears = workingYears;
}
// 不能使用箭头函数,箭头函数的 this 在声明的时候就根据上下文确定了
Engineer.prototype.built = function() {
// this 这里就是执行函数调用者
console.log(`我已经工作 ${this.workingYears} 年了, 我的工作是拧螺丝...`);
};
const engineer = new Engineer(5);
// this 会正确指向实例,所以 this.workingYears 是 5
engineer.built(); // => 我已经工作 5 年了, 我的工作是拧螺丝...
console.log(Object.keys(engineer)); // => [ 'workingYears' ]
- 优点:通过这种方式,所有的实例都可以访问到这个方法,并且这个方法只需要占用一份内存,节省内存,this 的指向还能正确指向类的实例。
- 不可枚举示例:
function Engineer(workingYears) {
this.workingYears = workingYears;
}
// 不能使用箭头函数,箭头函数的 this 在声明的时候就根据上下文确定了
Engineer.prototype.built = function() {
// this 这里就是执行函数调用者
console.log(`我已经工作 ${this.workingYears} 年了, 我的工作是拧螺丝...`);
};
const engineer = new Engineer(5);
// this 会正确指向实例,所以 this.workingYears 是 5
engineer.built(); // => 我已经工作 5 年了, 我的工作是拧螺丝...
console.log(Object.keys(engineer)); // => [ 'workingYears' ]
- 可枚举方法示例:
function Engineer(workingYears) {
this.workingYears = workingYears;
this.built = function() {
console.log(`我已经工作 ${this.workingYears} 年了, 我的工作是拧螺丝...`);
};
}
const engineer = new Engineer(5);
console.log(Object.keys(engineer)); // => [ 'workingYears', 'built' ]
- 常见:Array.prototype.slice,Object.prototype.toString就是定义在了原型上的函数。
<br />
ES6的class本质是什么?
- 其实,ES6 class 就是构造器的语法糖。
- ES6 的 class 就是构造器,class 上的方法定义在构造器的 prototype 上,因此你也可以理解为什么 class 的方法是不可枚举的。
<br />
class extends实现继承的本质?
- 原型继承+组合继承
// 原型继承
function _inheritsLoose(subClass, superClass) {
subClass.prototype = Object.create(superClass.prototype);
subClass.prototype.constructor = subClass;
// 让子类可以访问父类上的静态属性,其实就是定义在构造器自身上的属性
// 例如父类有 Person.say 属性,子类 Student 通过可以通过 Student.say 访问
subClass.__proto__ = superClass;
}
var Shape = function Shape(x, y) {
this.x = x;
this.y = y;
};
var Circle = (function(_Shape) {
_inheritsLoose(Circle, _Shape);
function Circle(x, y, r) {
var _this;
// 组合继承
_this = _Shape.call(this, x, y) || this;
_this.r = r;
return _this;
}
var _proto = Circle.prototype;
_proto.draw = function draw() {
console.log(
'\u753B\u4E2A\u5750\u6807\u4E3A (' +
this.x +
', ' +
this.y +
')\uFF0C\u534A\u5F84\u4E3A ' +
this.r +
' \u7684\u5706'
);
};
return Circle;
})(Shape);
继承有哪几种方式?
(1)借助构造函数+call/apply实现继承
// 借助构造函数实现继承
function Parent(argument) {
this.name='parent';
}
Parent.prototype.say=function(){}
function Child(argument) {
Parent.call(this); // 原理就是call/apply, call和apply改变了父类this中的指向,使this指向了子类,这样就可以把父类的属性挂载到子类里
this.age=11;
}
var child = new Child();
console.log(child); // Child {name: "parent", age: 11}
缺点是只能继承父类实例上的属性,无法继承原型链上的属性。<br />(2)借助原型链实现继承
// 借助原型链实现继承
function Parent1(argument) {
this.name='parent';
this.age=[1,2,3];
}
function Child1(argument) {
this.name='child';
}
Child1.prototype=new Parent1(); // 将父类的实例赋值給子类的原型,这样子类就继承了父类
var child11 = new Child1();
console.log(child11) // Child1 {age: 11 , __proto__: Parent1}
/*******************原型链继承的缺点********************/
var child12=new Child1();
child11.age.push(4); // 往其中一个实例的引用属性添加一个元素
console.log(child11.age,child12.age) // 会发现都是打印出 [1, 2, 3, 4]
缺点:当父类有引用属性时,由于原型对象的特点,多个实例对象的proto都是同一个,而引用属性在new的时候不会开辟新的地址,所以当一个实例对象改变了引用属性的值时,另一个对象也会随之改变。<br />(3)结合构造函数和原型链的方式
// 组合方式,结合上面两种
function Parent3(argument) {
this.name='parent';
this.age=[1,2,3]
}
Parent3.prototype.say=function(){}
function Child3(argument) {
Parent3.call(this); // 结合构造函数
this.type='test';
}
Child3.prototype=new Parent3(); // 结合原型链
var child31 = new Child3();
var child32 = new Child3();
console.log(child31,child32);
child31.age.push(4);
console.log(child31.age,child32.age);.// [1, 2, 3, 4] [1, 2, 3]
这种方式可以解决前两种方式的问题。缺点:父类的构造函数会执行两次。<br />优化:把上面的
Child3.prototype=new Parent3();
换成
Child3.prototype=Parent3.prototype;
以上方法还是存在不足,因为只要是通过原型链继承来的对象,它的constructor打印出来都是父类Parent3,即无法确认child31实例是由父类创造的还是由子类创造的。 原因:Child3和父类Parent3共用了一个原型。Child本身没有constructor,由于继承了父类,就会把父类的constructor作为自己的。<br />解决方案:把上面的
Child3.prototype=Parent3.prototype;
换成
Child3.prototype=Object.create(Parent3.prototype); // 让Child3继承Parent3,由于Object.create会返回一个新对象,该对象继承了Parent3,再让Child3去继承Parent3,这样就起到了隔离并且继承的作用。
Child3.prototype.constructor=Child3;
// 修改Child3的constructor
这样的话就是组合继承+原型继承的完美写法了。
// 组合方式,完美写法
function Parent3(argument) {
this.name='parent';
this.age=[1,2,3];
}
Parent3.prototype.say=function(){}
function Child3(argument) {
Parent3.call(this); // 结合构造函数
this.type='test';
}
Child3.prototype=Object.create(Parent3.prototype);
Child3.prototype.constructor=Child3; // 结合原型链
var child31 = new Child3();
var child32 = new Child3();
console.log(child31,child32);
child31.age.push(4);
console.log(child31.age,child32.age);.// [1, 2, 3, 4] [1, 2, 3]
<br />
Object.create是做什么的?
- Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的proto
const person = {
isHuman: false,
printIntroduction: function () {
console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
}
};
const me = Object.create(person);
me.name = "Matthew"; // "name" is a property set on "me", but not on "person"
me.isHuman = true; // inherited properties can be overwritten
me.printIntroduction();
// expected output: "My name is Matthew. Am I human? true"
me.__proto__===person; // true
一道最近校招面试碰到的和原型相关的面试题
最近面试某大厂碰到下面这道面试题:
function Page() {
return this.hosts;
}
Page.hosts = ['h1'];
Page.prototype.hosts = ['h2'];
const p1 = new Page();
const p2 = Page();
console.log(p1.hosts);
console.log(p2.hosts);
复制代码
运行结果是:先输出 undefiend
,然后报错 TypeError: Cannot read property 'hosts' of undefined
。<br />为什么 console.log(p1.hosts)
是输出 undefiend
呢,前面我们提过 new 的时候如果 return 了对象,会直接拿这个对象作为 new 的结果,因此,p1
应该是 this.hosts
的结果,而在 new Page()
的时候,this 是一个以 Page.prototype
为原型的 target
对象,所以这里 this.hosts
可以访问到 Page.prototype.hosts
也就是 ['h2']
。这样 p1
就是等于 ['h2']
,['h2']
没有 hosts
属性所以返回 undefined
。<br />为什么 console.log(p2.hosts)
会报错呢,p2
是直接调用 Page
构造函数的结果,直接调用 page
函数,这个时候 this
指向全局对象,全局对象并没 hosts
属性,因此返回 undefined
,往 undefined
上访问 hosts
当然报错。<br />
<br />作者:余腾靖<br />链接:https://juejin.im/post/5e2ff7dce51d4558021a1a4d<br />来源:掘金<br />著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。<br />
proxy是什么?
proxy表单校验实例
proxy实现观察者模式
const observerQueue = new Set()
const observe = fn => observerQueue.add(fn)
const observable = obj => new Proxy(obj,
set(tgt, key, val, receiver) {
const result = Reflect.set(tgt, key, val, recevier)
observerQueue.forEach(v => v())
return result
}
)
const person = observerable({age: 25, name: 'Mike'})
const print = () => console.log(`${person.name} is ${person.age} years old`)
observe(print)
person.name = 'LiHua'
// Lihua is 25 years old
person.age = 45
// Lihua is 45 years old
Common.js和ES6的module比较
- CommonJS 输出是值的拷贝,即原来模块中的值改变不会影响已经加载的该值,ES6静态分析,动态引用,输出的是值的引用,值改变,引用也改变,即原来模块中的值改变则该加载的值也改变。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
- CommonJS 加载的是整个模块,即将所有的接口全部加载进来,ES6 可以单独加载其中的某个接口(方法),
- CommonJS this 指向当前模块,ES6 this 指向undefined
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
————————————————<br />版权声明:本文为CSDN博主「冰雪为融」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。<br />原文链接:https://blog.csdn.net/lhjuejiang/article/details/80274212<br />
yield传值与不传值的区别是什么?
- yield每次调用next的时候传值,将作为上一次yield语句后的返回值
- 如果不传值则值为undefined
// 传值
function *foo(x) {
let y = 2 * (yield (x + 1))
let z = yield (y / 3)
return (x + y + z)
}
let it = foo(5)
console.log(it.next()) // => {value: 6, done: false}
console.log(it.next(12)) // => {value: 8, done: false}
console.log(it.next(13)) // => {value: 42, done: true}
// 不传值
function *foo(x) {
let y = 2 * (yield (x + 1))
let z = yield (y / 3)
return (x + y + z)
}
let it = foo(5)
console.log(it.next()) // => {value: 6, done: false}
console.log(it.next()) // => {value: 8, done: false}
console.log(it.next()) // => {value: 42, done: true}
VM5628:7 {value: 6, done: false}
VM5628:8 {value: NaN, done: false}
VM5628:9 {value: NaN, done: true}
yield常见用途
function *fetch() {
yield ajax(url, () => {})
yield ajax(url1, () => {})
yield ajax(url2, () => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()
async函数的执行顺序
- async中await修饰的语句会同步执行
- async函数执行之后返回一个promise,里面执行的函数会作为回调函数在外部执行栈结束之后执行
let c = 0
let d = async () => {
c = c + await 10
console.log('函数内部1')
await console.log('函数内部2', c)
console.log('函数内部3')
}
undefined
d()
c++
console.log('函数外部')
VM2692:3 函数外部
VM2618:4 函数内部1
VM2618:5 函数内部2 10
VM2618:6 函数内部3
经典例题实现Promise.race和Promise.all
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('p1准备执行')
resolve('p1执行了')
}, 1000)
})
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('p2执行了')
}, 2000)
})
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('p3执行失败了')
}, 3000)
})
Promise.race([p1, p2, p3]).then(res => console.log(res), err => console.log(err))
p1准备执行
p1执行了
Promise.all([p1, p2, p3]).then(res => console.log(res), err => console.log(err))
p1准备执行
p3执行失败了
手写简版的Promise
- 参考地址:Promise/A+规范
- 简版实现链接
// <!-- 简易版的promise -->
const PENDING = "pending"
const RESOLVE = "resolve"
const REJECT = "reject"
function MyPromise(fn) {
const that = this
that.status = PENDING // MyPromise 内部状态
that.value = null // 传入 resolve 和 reject 的值
that.resolveCallbacks = [] // 保存 then 中resolve的回调函数
that.rejectCallbacks = [] // 保存 then 中reject的回调函数
// resolve 函数 Promise内部调用 resolve 函数 例:new MyPromise((resolve,reject)=>{resolve(1)})
function resolve(val) {
if (that.status === PENDING) {
that.status = RESOLVE
that.value = val
that.resolveCallbacks.forEach(cb => cb(that.value))
}
}
// reject 函数 Promise内部调用的 reject 函数 例:new MyPromise((resolve,reject)=>{reject(1)})
function reject(val) {
if (that.status === PENDING) {
that.status = REJECT
that.value = val
that.rejectCallbacks.forEach(cb => cb(that.value))
}
}
// 调用传入 MyPromise 内的方法 例:new MyPromise((resolve,reject)=>{}) fn=(resolve,reject)=>{}
try {
fn(resolve, reject)
} catch (error) {
reject(error)
}
}
// 在原型上添加then方法
MyPromise.prototype.then = function (onFulfilled, onRejected) {
const that = this
// 判断传入的是否为函数
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
onRejected = typeof onRejected === 'function' ? onRejected : r => {
throw r
}
//如果 Promise 内部存在异步代码,调用then方法时,此时 promise 内部还是 PENDING 状态,
将 then 里面的函数添加进回调数组,当异步处理完成后调用 MyPromise 内部的 resolve 或者
reject 函数
if (that.status === PENDING) {
that.resolveCallbacks.push(onFulfilled)
that.rejectCallbacks.push(onRejected)
}
// 当 Promise 内部的状态已经为 resolve,则调用 then 里面的函数并传递值
if (that.status === RESOLVE) {
onFulfilled(that.value)
}
// 当 Promise 内部状态为 reject,则调用then里的回调函数并传递值
if (that.status === REJECT) {
onRejected(that.value)
}
}
// 自己实现的Promise
new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve(1)
reject(2)
}, 0)
}).then(res => {
console.log(res)
}, err => {
console.log(err)
})
// MyPromise.resolve(4).then().then(res => console.log(4)) // 透传,尚未实现
async和await遇见EventsLoop的注意事项
一般而言,我们可以把
async function f() {
await p
console.log('ok')
}
简化理解为:
function f() {
return RESOLVE(p).then(() => {
console.log('ok')
})
}
『RESOLVE(p)』接近于『Promise.resolve(p)』,不过有微妙而重要的区别:
p 如果本身已经是 Promise 实例,Promise.resolve 会直接返回 p 而不是产生一个新 promise。
【个人认为 Promise.resolve 的这一『优化』行为可能是弊大于利的,不过因为已经写进标准了,
也不太可能修改了。】老版本V8的问题是当 p 是一个已经 settled 的 promise,会进行类似的激进优化,
导致执行时序与非 settled 的 promise 结果不同。比如把你的 async2 中加入一个 await 语句,
老版本行为就和新版本一致了。
例题:
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
// script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout
解析:
当await 后面是一个async函数并且执行之后,也就是:
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
由于async2执行后返回一个promise,则相当于
async function async1() {
return new Promise((res, rej) => {
console.log('async2 end')
}).then(() => {
console.log('async1 end')
})
}
结果一样
null和undefined有何区别
- null表示有值,但是为空,Number(null) === 1 false
- undefined表示没有值,还没有定义,Number(undefined) === NaN false
如何正确判断业务中遇到的undefined值
- 注意:避免使用==,因为
null == undefined
true
0 == undefined
false
'' == undefined
false
- 正确判断方法
1、使用====
if(backgroundAudioManger === undefined) ...
2、使用typeof
if(typeof backgroundAdudioManager == 'undefined')
手写实现call,apply,bind
- call
Function.prototype.myCall = function(context) {
if ( typeof this !== 'function' ) throw new TypeError('Error')
// 完善部分,如果传入context是个基础类型是无法绑定fn函数的,所以
if (typeof context === 'object') {
context = context || window;
} else {
context = Object.create(null)
}
context = context || window
// 如果context中有fn则会被覆盖并清除
// newContext.fn = this
// 使用Symbol()独一无二数据类型避免fn冲突
let fn = Symbol('fn')
context[fn] = this
let args
let result
if ([...arguments][1]) {
args = [...arguments].slice(1)
result = newContext.fn(args)
} else {
result = newContext.fn()
}
delete context[fn]
return result
}
function fn () {
console.log(this.a, this)
}
const obj = {
a: 21
}
fn.myCall(obj)
- apply
Function.prototype.myApply = function(context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
context = context || window
context.fn = this
let result
// 处理参数和 call 有区别
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
delete context.fn
return result
}
- bind
Function.prototype.myBind = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
const _this = this
const args = [...arguments].slice(1)
// 返回一个函数
return function F() {
// 因为返回了一个函数,我们可以 new F(),所以需要判断
if (this instanceof F) {
return new _this(...args, ...arguments)
}
return _this.apply(context, args.concat(...arguments))
}
}
手写实现一个new方法
- new原理那部分,链接到原型,obj = Object.create(Con.prototype),这种方式会比较好
function _new(fn,...arg){
let obj = {}
let con = [].slice.call(arguments)
obj.__proto__ = con.prototype //链接原型
const ret = fn.call(obj, ...arg); //改变this的指向
return ret instanceof Object ? ret : obj;
}
- 参考地址:链接
Symbol()和Symbol.for()和Symbol.keyFor()区别
- 参考链接:链接
- Symbol()返回一个独一无二的标识符类型值
- Symbol.for(),将传入值生成一个标识符类性值,并将其作为key存入到注册表中
- Symbol.keyFor(),传入一个Symbol.for()生成的标识符类型值,返回注册表中对应该key的value值(Symbol.for(‘value’))
巧用parseFloat避免js浮点数精度转换问题
(0.1 + 0.2).toFixed(10)
"0.3000000000"
parseFloat((0.1 + 0.2).toFixed(10))
0.3
更简便的手写call
Function.prototype.myCall = function(context,...args){
context = context || window
const symbol = Symbol()
context[symbol] = this
const result = context[symbol](...args)
delete context[symbol]
return result
}
[].shift.call(arguments)是什么原理呢?
改变绑定的 this:
let a = [1,2,3]
const myshift = [].shift.bind(a)
console.log(myshift(a))
- Arguments是一个类数组对线,不能直接进行数组想关的操作,使用[].shifit.call(arguments)则可以转化为数组之后进行一些操作
function fn3 (a, b) {
console.log(arguments)
console.log([...arguments])
console.log([].shift.call(arguments))
}
<br />
<br />
事件代理应用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>手写函数测试</title>
</head>
<body>
<ul id="ul">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<script>
let ul = document.querySelector('#ul')
ul.addEventListener('click', (event) => {
console.log(event.target);
})
</script>
</body>
</html>
阻止事件冒泡
- 操作:在子DOM节点的event执行事件上绑定stopPropagation
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>手写函数测试</title>
</head>
<body>
<ul id="ul">
<li>1</li>
<li>2</li>
<li>3</li>
<li id="li">4</li>
<li>5</li>
</ul>
<script>
let ul = document.querySelector('#ul')
ul.addEventListener('click', (event) => {
console.log(event);
}) // 如果在后面配置项为true, 则先捕获(父节点事件)后冒泡(子节点事件)
let li = document.querySelector('#li')
li.addEventListener('click', (event) => {
console.log(event, '4点击了')
event.stopPropagation() // 阻止其余捕获
})
</script>
</body>
</html>
冒泡事件和捕获事件顺序调换的配置
- 默认false
<div>
外层div
<p>内层p</p>
<div>
var div = document.querySelector('div');
var p = document.querySelector('p');
div.addEventListener('click', function() {console.log("冒泡")}, false);
div.addEventListener('click', function() {console.log("捕获")}, true);
点击div, 输出 冒泡,捕获
点击p, 输出捕获,冒泡
<br />
浏览器同源策略要干啥的, 为什么要跨域请求?
- 当我们在www.a.com这个域下用ajax提交一个请求到www.b.com这个域的时候,默认情况下,浏览器是不允许的,因为违反了浏览器的同源策略。
- 参考地址:https://www.cnblogs.com/anai/p/4227157.html
跨域解决方案
- Jsonp
- Cors白名单
- document.domain(适用于二级域名相同--->a.test.com 和 b.test.com)
- postMessage
- 前端proxy正向代理
- 后端proxy反向代理
cookie,localStorage,sessionStorage区别
- cookie:多为网络请求过程中服务器通过请求信息在浏览器客户端种下,可以用来保存用户登录状态,规避http请求无状态缺陷,缺陷是每一个name对应的value值大小只有4k,可存储量太小,超过会被裁剪,而且每次请求都会携带,造成额外的带宽占用
- localStorage:除非手动清空,否则用于存在于本地存储中,可在同源标签页下共享,适用于一些长久不变的数据,比如一些电商网站的base64数据,大小约为5兆左右
- sessionStorage:只在当前窗口下存在,同源标签下不共享,浏览器窗口关闭会被自动清空,适用于需要每次打开都重新登录的网站存储JWT的token数据。大小约为5兆左右
- 区别:sessionStorage保存的数据用于浏览器的一次会话,当会话结束(通常是该窗口关闭),数据被清空;sessionStorage 特别的一点在于,即便是相同域名下的两个页面,只要它们不在同一个浏览器窗口中打开,那么它们的 sessionStorage 内容便无法共享;localStorage 在所有同源窗口中都是共享的;cookie也是在所有同源窗口中都是共享的。除了保存期限的长短不同,SessionStorage的属性和方法与LocalStorage完全一样。
- 参考:https://github.com/ljianshu/Blog/issues/25
<br />
知道哪些新的缓存方案?
- IndexDB:浏览器提供的用途类似localStorage的NoSQL型数据库,除非手动清除否则一直存在,类似MongoDB的异步存取,
- 参考:阮一峰IndexDB入门
- 参考:简书IndexDB实例
- ServiceWorker:地址https://jakearchibald.com/2014/offline-cookbook/
- 可以劫持请求和响应,做缓存,并可以离线运行,不可以在ServiceWorker下定义全局变量
- ServiceWorker类似的webWorker:http://www.ruanyifeng.com/blog/2018/07/web-worker.html
代理的工作原理解析
- 正向代理和反向代理优缺点
- proxy正向代理原理:
- [图片上传失败...(image-e2b726-1611839965980)]
<br />
- webpack中的proxy本地代理原理
- 使用了express框架搭建本web项目服务的时候,引入了
http-proxy-middleware
的中间件,参考链接https://www.jianshu.com/p/8fd5d7347c57
node服务后台接收预检请求防止报错
if(res.method == 'OPTIONS') {
res.statusCode = 204
res.setHeader('Content-Length', '0')
res.end()
} else {
next()
}
浏览器缓存机制(后端向)
[图片上传失败...(image-f02655-1611839965980)]<br />
浏览器渲染原理
参考地址:https://github.com/ljianshu/Blog/issues/51
- 接收信息解析代码过程:
[图片上传失败...(image-f04011-1611839965980)]
- 接收HTML文件,构建DOM树
- 接收CSS文件,构建CSSOM树
- 接收jS文件(加载js脚本),等到Javascript 脚本文件加载后, 通过 DOM API 和 CSSOM API 来操作 DOM Tree 和 CSS Rule Tree。
<br />
- 注意:在构建DOM时,HTML解析器若遇到了JavaScript,那么它会暂停构建DOM,将控制权移交给JavaScript引擎,等JavaScript引擎运行完毕,浏览器再从中断的地方恢复DOM构建。
- 浏览器会先下载和构建CSSOM,然后再执行JavaScript,最后在继续构建DOM。
<br />
性能优化建议:
- 还有js中操作dom元素的样式时,如果修改多条style,不如把多条style写成一个class,对元素进行添加class的操作。如果是分步添加元素样式,可以将元素赋值给一个变量,避免多次获取一个元素,因为获取元素也会造成回流。
- 不要一条一条的修改 DOM 样式,尽量提前设置好 class,后续增加 class,进行批量修改。
- 使用 documentFragment 对象在内存里操作 DOM
- 不一定说非要用visibility: hidden替换display:none,完全可以把修改频繁的元素先 display: none,修改完之后显示。
RAF讨论
- 屏幕每16.6ms才刷新一次,比如这一帧有个人站着,下一帧他应该要迈开左腿,如果都已经下一帧了,浏览器还没渲染出迈开腿的画面,这个人还站着,看着就卡了。然后event loop和渲染这边,我的理解是,在短短16.6ms内,要保证一个tick能执行完并将这个tick里对dom的操作渲染出来(也因此同个tick里的多个dom操作会被合并),这样下一帧才能有迈开腿的画面出现在屏幕上。如果当前的tick要花好久才能执行完,就说40ms吧,那至少两帧都只有人站着的画面,第三帧还剩短短10ms用来重新渲染,如果能渲染出来还好,下一帧就能看,否则下一帧还看不到变化。
- 所以,假设一个tick里js先往某个<ul>里添加了几个<li>子节点,接着还要做一件非常耗时的事情,两件事合在一起要好久才能完成,不如把两件事拆开,放到两个requestAnimationFrame的回调里执行,这样就能先把对dom的修改重新渲染处理,下一帧至少能看到变化了。
<br />
前端安全
参考:XSS攻击和CSRF攻击
XSS:https://juejin.im/post/5bad9140e51d450e935c6d64<br />CSRF:https://juejin.im/post/5bc009996fb9a05d0a055192#heading-32<br />
了解webpack原理,手写小型webpack
流程浅析:参考
- createAsset:读取入口文件,通过babylon的parse方法将文件内容字符串转化为AST语法树(包含id,filename,dependencies,code信息)的一种数据结构
- createGraph:遍历入门文件AST语法树上的dependencies数组,生成一个新的数组,数组每一项和createAsset方法创建出来的AST语法树非常相像,并且多了一个mapping属性指定文件依赖,数组中将依赖文件根据依赖顺序id递增排列。
- bundle:接收graph数组,生成bundle结构的自执行函数
- 代码地址:https://github.com/dykily/simple_webpack/blob/327618d4e2/bundler.js