转自JavaScript手写代码无敌秘籍
转自面试题锦(大厂面试前夕的挣扎)
【前端面试】同学,你会手写代码吗?
手写路径导航
- 实现一个new操作符
- 实现一个JSON.stringify
- 实现一个JSON.parse
- 实现一个call或 apply
- 实现一个Function.bind
- 实现一个继承
- 实现一个JS函数柯里化
- 手写一个Promise(中高级必考)
- 手写防抖(Debouncing)和节流(Throttling)
- 手写一个JS深拷贝
- 实现一个instanceOf
1. 实现一个new
操作符
来源:「你不知道的javascript」 英文版
new
操作符做了这些事:
- 它创建了一个全新的对象。
- 它会被执行
[[Prototype]]
(也就是__proto__
)链接。 - 它使
this
指向新创建的对象。。 - 通过
new
创建的每个对象将最终被[[Prototype]]
链接到这个函数的prototype
对象上。 - 如果函数没有返回对象类型
Object
(包含Functoin, Array, Date, RegExg, Error
),那么new
表达式中的函数调用将返回该对象引用。
function New(func) {
var res = {};
if (func.prototype !== null) {
res.__proto__ = func.prototype;
}
var ret = func.apply(res, Array.prototype.slice.call(arguments, 1));
if ((typeof ret === "object" || typeof ret === "function") && ret !== null) {
return ret;
}
return res;
}
var obj = New(A, 1, 2);
// equals to
var obj = new A(1, 2);
复制代码
2. 实现一个JSON.stringify
JSON.stringify(value[, replacer [, space]])
:
Boolean | Number| String
类型会自动转换成对应的原始值。undefined
、任意函数以及symbol
,会被忽略(出现在非数组对象的属性值中时),或者被转换成null
(出现在数组中时)。- 不可枚举的属性会被忽略
- 如果一个对象的属性值通过某种间接的方式指回该对象本身,即循环引用,属性也会被忽略。
function jsonStringify(obj) {
lettype = typeof obj;
if (type !== "object" || type === null) {
if (/string|undefined|function/.test(type)) {
obj = '"' + obj + '"';
}
return String(obj);
} else {
let json = []
arr = (obj && obj.constructor === Array);
for (let k in obj) {
let v = obj[k];
lettype = typeof v;
if (/string|undefined|function/.test(type)) {
v = '"' + v + '"';
} elseif (type === "object") {
v = jsonStringify(v);
}
json.push((arr ? "" : '"' + k + '":') + String(v));
}
return (arr ? "[" : "{") + String(json) + (arr ? "]" : "}")
}
}
jsonStringify({x : 5}) // "{"x":5}"
jsonStringify([1, "false", false]) // "[1,"false",false]"
jsonStringify({b: undefined}) // "{"b":"undefined"}"
复制代码
3. 实现一个JSON.parse
JSON.parse(text[, reviver])
用来解析JSON字符串,构造由字符串描述的JavaScript值或对象。提供可选的reviver函数用以在返回之前对所得到的对象执行变换(操作)。
3.1 第一种:直接调用 eval
function jsonParse(opt) {
returneval('(' + opt + ')');
}
jsonParse(jsonStringify({x : 5}))
// Object { x: 5}
jsonParse(jsonStringify([1, "false", false]))
// [1, "false", falsr]
jsonParse(jsonStringify({b: undefined}))
// Object { b: "undefined"}
复制代码
避免在不必要的情况下使用
eval
,eval() 是一个危险的函数, 他执行的代码拥有着执行者的权利。如果你用 eval()运行的字符串代码被恶意方(不怀好意的人)操控修改,您最终可能会在您的网页/扩展程序的权限下,在用户计算机上运行恶意代码。
它会执行JS代码,有XSS漏洞。
如果你只想记这个方法,就得对参数json做校验。
var rx_one = /^[\],:{}\s]*$/;
var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
var rx_four = /(?:^|:|,)(?:\s*\[)+/g;
if (
rx_one.test(
json
.replace(rx_two, "@")
.replace(rx_three, "]")
.replace(rx_four, "")
)
) {
var obj = eval("(" +json + ")");
}
复制代码
3.2 第二种:Function
来源 神奇的eval()与new Function()
核心:Function
与eval
有相同的字符串参数特性。
var func = new Function(arg1, arg2, ..., functionBody);
在转换JSON的实际应用中,只需要这么做。
var jsonStr = '{ "age": 20, "name": "jack" }'
var json = (new Function('return ' + jsonStr))();
复制代码
eval
与 Function
都有着动态编译js代码的作用,但是在实际的编程中并不推荐使用。
这里是面向面试编程,写这两种就够了。至于第三,第四种,涉及到繁琐的递归和状态机相关原理,具体可以看:
《JSON.parse 三种实现方式》
4. 实现一个call
或 apply
call
语法:
fun.call(thisArg, arg1, arg2, ...)
,调用一个函数, 其具有一个指定的this值和分别地提供的参数(参数的列表)。
apply
语法:
func.apply(thisArg, [argsArray])
,调用一个函数,以及作为一个数组(或类似数组对象)提供的参数。
4.1 Function.call
按套路实现
call
核心:
- 将函数设为对象的属性
- 执行&删除这个函数
- 指定
this
到函数并传入给定参数执行函数 - 如果不传入参数,默认指向为 window
为啥说是套路实现呢?因为真实面试中,面试官很喜欢让你逐步地往深考虑,这时候你可以反套路他,先写个简单版的:
4.1.1 简单版
var foo = {
value: 1,
bar: function() {
console.log(this.value)
}
}
foo.bar() // 1
复制代码
4.1.2 完善版
当面试官有进一步的发问,或者此时你可以假装思考一下。然后写出以下版本:
Function.prototype.call2 = function(content = window) {
content.fn = this;
let args = [...arguments].slice(1);
let result = content.fn(...args);
delect content.fn;
return result;
}
var foo = {
value: 1
}
function bar(name, age) {
console.log(name)
console.log(age)
console.log(this.value);
}
bar.call2(foo, 'black', '18') // black 18 1
复制代码
4.2 Function.apply
的模拟实现
apply()
的实现和call()
类似,只是参数形式不同。直接贴代码吧:
Function.prototype.apply2 = function(context = window) {
context.fn = this
let result;
// 判断是否有第二个参数
if(arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
delete context.fn()
return result
}
复制代码
5. 实现一个Function.bind()
bind()
方法:
会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。(来自于 MDN )
此外,bind
实现需要考虑实例化后对原型链的影响。
Function.prototype.bind2 = function(content) {
if(typeof this != "function") {
throw Error("not a function")
}
// 若没问参数类型则从这开始写
let fn = this;
let args = [...arguments].slice(1);
let resFn = function() {
return fn.apply(this instanceof resFn ? this : content,args.concat(...arguments) )
}
functiontmp() {}
tmp.prototype = this.prototype;
resFn.prototype = new tmp();
return resFn;
}
复制代码
6. 实现一个继承
寄生组合式继承
一般只建议写这种,因为其它方式的继承会在一次实例中调用两次父类的构造函数或有其它缺点。
核心实现是:用一个 F
空的构造函数去取代执行了 Parent
这个构造函数。
function Parent(name) {
this.name = name;
}
Parent.prototype.sayName = function() {
console.log('parent name:', this.name);
}
function Child(name, parentName) {
Parent.call(this, parentName);
this.name = name;
}
function create(proto) {
functionF(){}
F.prototype = proto;
return new F();
}
Child.prototype = create(Parent.prototype);
Child.prototype.sayName = function() {
console.log('child name:', this.name);
}
Child.prototype.constructor = Child;
var parent = new Parent('father');
parent.sayName(); // parent name: father
var child = new Child('son', 'father');
复制代码
7. 实现一个JS函数柯里化
什么是柯里化?在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。
函数柯里化的主要作用和特点就是参数复用、提前返回和延迟执行。
7.1 通用版
functioncurry() {
var args = Array.prototype.slice.call(arguments);
var fn = function() {
var newArgs = args.concat(Array.prototype.slice.call(arguments));
return multi.apply(this, newArgs);
}
fn.toString = function() {
return args.reduce(function(a, b) {
return a * b;
})
}
return fn;
}
function multiFn(a, b, c) {
return a * b * c;
}
var multi = curry(multiFn);
multi(2)(3)(4);
multi(2,3,4);
multi(2)(3,4);
multi(2,3)(4);
复制代码
7.2 ES6
骚写法
const curry = (fn, arr = []) => (...args) => (
arg => arg.length === fn.length
? fn(...arg)
: curry(fn, arg)
)([...arr, ...args])
let curryTest=curry((a,b,c,d)=>a+b+c+d)
curryTest(1,2,3)(4) //返回10
curryTest(1,2)(4)(3) //返回10
curryTest(1,2)(3,4) //返回10
复制代码
8. 手写一个Promise
(中高级必考)
我们来过一遍Promise/A+
规范:
- 三种状态
pending| fulfilled(resolved) | rejected
- 当处于
pending
状态的时候,可以转移到fulfilled(resolved)
或者rejected
状态 - 当处于
fulfilled(resolved)
状态或者rejected
状态的时候,就不可变。
- 必须有一个
then
异步执行方法,then
接受两个参数且必须返回一个promise:
// onFulfilled 用来接收promise成功的值
// onRejected 用来接收promise失败的原因
promise1=promise.then(onFulfilled, onRejected);
复制代码
8.1 Promise
的流程图分析
来回顾下
Promise
用法:
var promise = new Promise((resolve,reject) => {
if (操作成功) {
resolve(value)
} else {
reject(error)
}
})
promise.then(function (value) {
// success
},function (value) {
// failure
})
复制代码
8.2 面试够用版
来源:实现一个完美符合Promise/A+规范的Promise
function myPromise(constructor){
let self=this;
self.status="pending" //定义状态改变前的初始状态
self.value=undefined;//定义状态为resolved的时候的状态
self.reason=undefined;//定义状态为rejected的时候的状态
function resolve(value){
//两个==="pending",保证了状态的改变是不可逆的
if(self.status==="pending"){
self.value=value;
self.status="resolved";
}
}
function reject(reason){
//两个==="pending",保证了状态的改变是不可逆的
if(self.status==="pending"){
self.reason=reason;
self.status="rejected";
}
}
//捕获构造异常
try{
constructor(resolve,reject);
}catch(e){
reject(e);
}
}
复制代码
同时,需要在myPromise
的原型上定义链式调用的then
方法:
myPromise.prototype.then=function(onFullfilled,onRejected){
let self=this;
switch(self.status){
case"resolved":
onFullfilled(self.value);
break;
case"rejected":
onRejected(self.reason);
break;
default:
}
}
复制代码
测试一下:
var p=new myPromise(function(resolve,reject){resolve(1)});
p.then(function(x){console.log(x)})
//输出1
复制代码
8.3 大厂专供版
直接贴出来吧,这个版本还算好理解
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
function Promise(excutor) {
let that = this; // 缓存当前promise实例对象
that.status = PENDING; // 初始状态
that.value = undefined; // fulfilled状态时 返回的信息
that.reason = undefined; // rejected状态时 拒绝的原因
that.onFulfilledCallbacks = []; // 存储fulfilled状态对应的onFulfilled函数
that.onRejectedCallbacks = []; // 存储rejected状态对应的onRejected函数
function resolve(value) { // value成功态时接收的终值
if(value instanceof Promise) {
return value.then(resolve, reject);
}
// 实践中要确保 onFulfilled 和 onRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。
setTimeout(() => {
// 调用resolve 回调对应onFulfilled函数
if (that.status === PENDING) {
// 只能由pending状态 => fulfilled状态 (避免调用多次resolve reject)
that.status = FULFILLED;
that.value = value;
that.onFulfilledCallbacks.forEach(cb => cb(that.value));
}
});
}
function reject(reason) { // reason失败态时接收的拒因
setTimeout(() => {
// 调用reject 回调对应onRejected函数
if (that.status === PENDING) {
// 只能由pending状态 => rejected状态 (避免调用多次resolve reject)
that.status = REJECTED;
that.reason = reason;
that.onRejectedCallbacks.forEach(cb => cb(that.reason));
}
});
}
// 捕获在excutor执行器中抛出的异常
// new Promise((resolve, reject) => {
// throw new Error('error in excutor')
// })
try {
excutor(resolve, reject);
} catch (e) {
reject(e);
}
}
Promise.prototype.then = function(onFulfilled, onRejected) {
const that = this;
let newPromise;
// 处理参数默认值 保证参数后续能够继续执行
onFulfilled =
typeof onFulfilled === "function" ? onFulfilled : value => value;
onRejected =
typeof onRejected === "function" ? onRejected : reason => {
throw reason;
};
if (that.status === FULFILLED) { // 成功态
return newPromise = new Promise((resolve, reject) => {
setTimeout(() => {
try{
let x = onFulfilled(that.value);
resolvePromise(newPromise, x, resolve, reject); // 新的promise resolve 上一个onFulfilled的返回值
} catch(e) {
reject(e); // 捕获前面onFulfilled中抛出的异常 then(onFulfilled, onRejected);
}
});
})
}
if (that.status === REJECTED) { // 失败态
return newPromise = new Promise((resolve, reject) => {
setTimeout(() => {
try {
let x = onRejected(that.reason);
resolvePromise(newPromise, x, resolve, reject);
} catch(e) {
reject(e);
}
});
});
}
if (that.status === PENDING) { // 等待态
// 当异步调用resolve/rejected时 将onFulfilled/onRejected收集暂存到集合中
return newPromise = new Promise((resolve, reject) => {
that.onFulfilledCallbacks.push((value) => {
try {
let x = onFulfilled(value);
resolvePromise(newPromise, x, resolve, reject);
} catch(e) {
reject(e);
}
});
that.onRejectedCallbacks.push((reason) => {
try {
let x = onRejected(reason);
resolvePromise(newPromise, x, resolve, reject);
} catch(e) {
reject(e);
}
});
});
}
};
复制代码
emmm,我还是乖乖地写回进阶版吧。
9. 手写防抖(Debouncing
)和节流(Throttling
)
scroll
事件本身会触发页面的重新渲染,同时scroll
事件的handler
又会被高频度的触发, 因此事件的handler
内部不应该有复杂操作,例如DOM
操作就不应该放在事件处理中。 针对此类高频度触发事件问题(例如页面scroll
,屏幕resize
,监听用户输入等),有两种常用的解决方法,防抖和节流。
9.1 防抖(Debouncing
)实现
典型例子:限制 鼠标连击 触发。
一个比较好的解释是:
当一次事件发生后,事件处理器要等一定阈值的时间,如果这段时间过去后 再也没有 事件发生,就处理最后一次发生的事件。假设还差
0.01
秒就到达指定时间,这时又来了一个事件,那么之前的等待作废,需要重新再等待指定时间。
// 防抖动函数
function debounce(fn,wait=50,immediate) {
let timer;
returnfunction() {
if(immediate) {
fn.apply(this,arguments)
}
if(timer) clearTimeout(timer)
timer = setTimeout(()=> {
fn.apply(this,arguments)
},wait)
}
}
复制代码
9.2 节流(Throttling
)实现
简单的节流函数:可以理解为事件在一个管道中传输,加上这个节流阀以后,事件的流速就会减慢。实际上这个函数的作用就是如此,它可以将一个函数的调用频率限制在一定阈值内,例如 1s,那么 1s 内这个函数一定不会被调用两次
function throttle(fn, wait) {
let prev = new Date();
returnfunction() {
const args = arguments;
const now = new Date();
if (now - prev > wait) {
fn.apply(this, args);
prev = new Date();
}
}
复制代码
9.3 结合实践
通过第三个参数来切换模式。
const throttle = function(fn, delay, isDebounce) {
let timer
let lastCall = 0
returnfunction (...args) {
if (isDebounce) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn(...args)
}, delay)
} else {
const now = new Date().getTime()
if (now - lastCall < delay) return
lastCall = now
fn(...args)
}
}
}
复制代码
10. 手写一个JS深拷贝
有个最著名的乞丐版实现,在《你不知道的JavaScript(上)》里也有提及:
10.1 乞丐版
var newObj = JSON.parse( JSON.stringify( someObj ) );
复制代码
### 10.2 面试够用版
function deepCopy(obj){
//判断是否是简单数据类型,
if(typeof obj == "object"){
//复杂数据类型
var result = obj.constructor == Array ? [] : {};
for(let i in obj){
result[i] = typeof obj[i] == "object" ? deepCopy(obj[i]) : obj[i];
}
}else {
//简单数据类型 直接 == 赋值
var result = obj;
}
return result;
}
复制代码
关于深拷贝的讨论天天有,这里就贴两种吧,毕竟我...
11.实现一个instanceOf
function instanceOf(left,right) {
let proto = left.__proto__;
let prototype = right.prototype
while(true) {
if(proto === null) returnfalseif(proto === prototype) returntrue
proto = proto.__proto__;
}
}
复制代码
欢迎评论区纠错完善
css部分
实现三栏布局
(两侧定宽,中间自适应)
-
采用了 absolute,导致父元素脱离了文档流,那所有的子元素也需要脱离文档流。如果页面复杂,那开发的难度可想而知
-
利用浮动 当中间内容高于两侧时,两侧高度不会随中间内容变高而变高
-
弹性盒子布局
-
利用负边距和浮动,实现起来比较复杂
-
利用网格布局
.container { display: grid; grid-template-columns: 100px auto 200px; } 复制代码
BFC(块级格式化上下文)
- BFC 的原理 其实也就是 BFC 的渲染规则(能说出以下四点就够了)。包括:
- BFC 内部的子元素,在垂直方向,边距会发生重叠。
- BFC在页面中是独立的容器,外面的元素不会影响里面的元素,反之亦然。
- BFC区域不与旁边的float box区域重叠。(可以用来清除浮动带来的影响)。
- 计算BFC的高度时,浮动的子元素也参与计算。
- 如何生成BFC
- 方法1:overflow: 不为vidible,可以让属性是 hidden、auto。【最常用】
- 方法2:浮动中:float的属性值不为none。意思是,只要设置了浮动,当前元素就创建了BFC。
- 方法3:定位中:只要posiiton的值不是 static或者是relative即可,可以是absolute或fixed,也就生成了一个BFC。
- 方法4:display为inline-block, table-cell, table-caption, flex, inline-flex
flex(面试常问,略)
js部分
call, apply, bind区别? 怎么实现call,apply方法
js继承,构造函数,原型链,构造函数、原型链组合式继承,寄生式组合继承,Object.create polyfill;
数组去重
[...new Set(arr]
复制代码
var arr = [1,2,1,2,3,5,4,5,3,4,4,4,4],
init=[]
var result = arr.sort().reduce((init, current)=>{
console.log(init,current)
if(init.length===0 || init[init.length-1]!==current){
init.push(current);
}
return init;
}, []);
console.log(result);//1,2,3,4,5复制代码
复制代码
防抖节流
var deBounce=function(fn,wait=300){
let timer
returnfunction(){
if(timer){
clearTimeOut(timer)
}
timer=setTimeOut(()=>{
fn.apply(this,arguments)
},wait)
}
}
var throttle=function(fn,wait=300){
let prev=+newDate();
returnfunction(){
const args=argument,
now=+newDate();
if(now>last+wait){
last=now;
fn.apply(this,args)
}
}
}
复制代码
复制代码
实现Promise思路
//0 pending , 1 resolve,2 reject functionPromise(fn) {
...
this._state = 0// 状态标记
doResolve(fn, this)
}
functiondoResolve(fn, self) {
var done = false// 保证只执行一个监听try {
fn(function(value) {
if (done) return
done = true
resolve(self, value)
},
function(reason) {
if (done) return;
done = true
reject(self, value)
})
} catch(err) {
if (done) return
done = true
reject(self, err)
}
}
functionresolve(self, newValue) {
try {
self._state = 1;
...
}
catch(err) {
reject(self, err)
}
}
functionreject(self, newValue) {
self._state = 2;
...
if (!self._handled) {
Promise._unhandledRejectionFn(self._value);
}
}
复制代码
复制代码
正则实现千位分隔符
functioncommafy(num) {
return num && num
.toString()
.replace(/(\d)(?=(\d{3})+\.)/g, function($0, $1) {
return $1 + ",";
});
}
console.log(commafy(1312567.903000))
复制代码
复制代码
js事件循环
javascript是单线程语言,任务设计成了两类,同步任务和异步任务 同步和异步任务分别进入不同的执行“场所”,同步进入主线程,异步进入Event Table并注册函数。当指定的事情完成时,Event Table会将这个函数移入Event Queue。主线程内的任务执行完毕为空,回去了Event Queue读取对应的函数,进入主线程。 上述过程会不断重复,也就是常说的Event Loop(事件循环)。 但是,JS异步还有一个机制,就是遇到宏任务,先执行宏任务,将宏任务放入event queue,然后再执行微任务,将微任务放入eventqueue,但是,这两个queue不是一个queue。当你往外拿的时候先从微任务里拿这个回调函数,然后再从宏任务的queue拿宏任务的回调函数 宏任务一般包括:整体代码script,setTimeout,setInterval。 微任务:Promise,process.nextTick
事件流机制,事件委托 event.targe和event.currentTarget的区别
- 事件捕获,
- 处于目标阶段,
- 事件冒泡阶段 event.target返回触发事件的元素 event.currentTarget返回绑定事件的元素
new的过程以及实现new
functioncreate(){
//1.创建一个空对象let obj={}
//2.获取构造函数let Con=[].shift.call(arguments)
//3.设置空对象的原型
obj._proto_=Con.prototype
//4.绑定this并执行构造函数,给新对象添加属性和方法let result=Con.apply(obj,arguments)
//5.确保返回值为对象return result instanceofObject?result:obj
}
复制代码
复制代码
前端路由的两种实现原理
- Hash模式
- window对象提供了onhashchange事件来监听hash值的改变,一旦url中的hash值发生改变,便会触发该事件。
- History 模式
- popstate监听历史栈信息变化,变化时重新渲染
- 使用pushState方法实现添加功能
- 使用replaceState实现替换功能
封装ajax
/* 封装ajax函数
* @param {string}opt.type http连接的方式,包括POST和GET两种方式
* @param {string}opt.url 发送请求的url
* @param {boolean}opt.async 是否为异步请求,true为异步的,false为同步的
* @param {object}opt.data 发送的参数,格式为对象类型
* @param {function}opt.success ajax发送并接收成功调用的回调函数
*/functionmyAjax(opt){
opt = opt || {};
opt.method = opt.method.toUpperCase() || 'POST';
opt.url = opt.url || '';
opt.async = opt.async || true;
opt.data = opt.data || null;
opt.success = opt.success || function () {}
let xmlHttp = null;
if (XMLHttpRequest) {
xmlHttp = new XMLHttpRequest();
}else{
xmlHttp =new ActiveXObject('Microsoft.XMLHTTP')
}
let params;
for (var key in opt.data){
params.push(key + '=' + opt.data[key]);
}
let postData = params.join('&');
if (opt.method.toUpperCase() === 'POST') {
xmlHttp.open(opt.method, opt.url, opt.async);
xmlHttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=utf-8');
xmlHttp.send(postData);
}elseif (opt.method.toUpperCase() === 'GET') {
xmlHttp.open(opt.method, opt.url + '?' + postData, opt.async);
xmlHttp.send(null);
}
xmlHttp.onreadystatechange= function () {
if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
opt.success(xmlHttp.responseText);//如果是json数据可以在这使用opt.success(JSON.parse( xmlHttp.responseText))
}
};
}
复制代码
复制代码
url拿参数
var url = "http://www.taobao.com/index.php?key0=0&key1=1&key2=2";
functionparseQueryString(url){
var str = url.split("?")[1], //通过?得到一个数组,取?后面的参数
items = str.split("&"); //分割成数组var arr,name,value;
for(var i=0; i复制代码
HTTP部分
http协议
HTTP协议(超文本传输协议) 主要特点
- 简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。
- 灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。
- 无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
- 无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。
- 支持B/S及C/S模式。
HTTP之请求消息Request
- 请求行(request line)、请求头部(header)、空行和请求数据四个部分组成。
- 请求行,用来说明请求类型,要访问的资源以及所使用的HTTP版本.
- 请求头部,紧接着请求行(即第一行)之后的部分,用来说明服务器要使用的附加信息
- 空行,请求头部后面的空行是必须的
- 请求数据也叫主体,可以添加任意的其他数据。
HTTP之响应消息Response
HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。
- 状态行,由HTTP协议版本号, 状态码, 状态消息 三部分组成。
- 消息报头,用来说明客户端要使用的一些附加信息
- 第三部分:空行,消息报头后面的空行是必须的
- 第四部分:响应正文,服务器返回给客户端的文本信息。
在浏览器地址栏键入URL,按下回车之后会经历以下流程:
- 浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址;
- 建立TCP连接;
- 浏览器发出读取文件(URL 中域名后面部分对应的文件)的HTTP 请求,该请求报文作为 TCP 三次握手的第三个报文的数据发送给服务器;
- 服务器对浏览器请求作出响应,并把对应的 html 文本发送给浏览器;
- 释放 TCP连接(四次挥手);
- 浏览器将该 html 文本并显示内容;
三次握手
SYN (同步序列编号)ACK(确认字符)
- 第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。
- 第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。
- 第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。
四次挥手
- 第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。
- 第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。
- 第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。
- 第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。
为什么建立连接是三次握手,而关闭连接却是四次挥手呢?
这是因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方也未必全部数据都发送给对方了,所以己方可以立即close,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送。
网页生成的过程,大致可以分为五步:
- html代码转化为dom
- css代码转化为cssom
- 结合dom和cssom,生成一颗渲染树
- 生成布局layout,即将所有的渲染树的节点进行平面合成
- 将布局绘制paint在屏幕上(可以拓展讲一下减少浏览器渲染的重排和重绘)
浏览器缓存
当浏览器再次访问一个已经访问过的资源时,它会这样做:
- 看看是否命中强缓存,如果命中,就直接使用缓存了。
- 如果没有命中强缓存,就发请求到服务器检查是否命中协商缓存。
- 如果命中协商缓存,服务器会返回 304 告诉浏览器使用本地缓存。
- 否则,返回最新的资源。
vue
vue Virtual DOM
其实 VNode 是对真实 DOM 的一种抽象描述,它的核心定义无非就几个关键属性,标签名、数据、子节点、键值等,其它属性都是都是用来扩展 VNode 的灵活性以及实现一些特殊 feature 的。由于 VNode 只是用来映射到真实 DOM 的渲染,不需要包含操作 DOM 的方法,因此它是非常轻量和简单的。 Virtual DOM 除了它的数据结构的定义,映射到真实的 DOM 实际上要经历 VNode 的 create、diff、patch 等过程。那么在 Vue.js 中,VNode 的 create 是通过之前提到的 createElement 方法创建的,我们接下来分析这部分的实现。
vue的响应式原理
-
Object.defineProperty(obj, prop, descriptor)
-
obj 是要在其上定义属性的对象;prop 是要定义或修改的属性的名称;descriptor 是将被定义或修改的属性描述符。 比较核心的是 descriptor,它有很多可选键值,具体的可以去参阅它的文档。这里我们最关心的是 get 和 set,get 是一个给属性提供的 getter 方法,当我们访问了该属性的时候会触发 getter 方法;set 是一个给属性提供的 setter 方法,当我们对该属性做修改的时候会触发 setter 方法。一旦对象拥有了 getter 和 setter,我们可以简单地把这个对象称为响应式对象
-
对象递归调用
-
数组变异方法的解决方法:代理原型/实例方法
-
observe
-
observe 方法的作用就是给非 VNode 的对象类型数据添加一个 Observer,如果已经添加过则直接返回,否则在满足一定条件下去实例化一个 Observer 对象实例。
-
observe 的功能就是用来监测数据的变化.
-
Observer 是一个类,它的作用是给对象的属性添加 getter 和 setter,用于依赖收集和派发更新:
-
依赖收集和派发更新
-
收集依赖的目的是为了当这些响应式数据发生变化,触发它们的 setter 的时候,能知道应该通知哪些订阅者去做相应的逻辑处理,我们把这个过程叫派发更新,其实 Watcher 和 Dep 就是一个非常经典的观察者设计模式的实现
-
派发更新就是数据发生变化的时候,触发 setter 逻辑,把在依赖过程中订阅的的所有观察者,也就是 watcher,都触发它们的 update 过程,这个过程又利用了队列做了进一步优化,在 nextTick 后执行所有 watcher 的 run,最后执行它们的回调函数
-
vue编译Compile的过程主要分以下几步 parse(生成AST)=> optimize(优化静态节点) => generate(生成render function)
// 解析模板字符串生成 ASTconst ast = parse(template.trim(), options) //优化语法树 optimize(ast, options) //生成代码const code = generate(ast, options) 复制代码
对vuex的理解,单向数据流
前端安全
XSS和CSRF
- XSS:跨站脚本攻击,是一种网站应用程序的安全漏洞攻击,是代码注入的一种。常见方式是将恶意代码注入合法代码里隐藏起来,再诱发恶意代码,从而进行各种各样的非法活动。
预防:
使用XSS Filter
- 输入过滤,对用户提交的数据进行有效性验证,仅接受指定长度范围内并符合我们期望格式的的内容提交,阻止或者忽略除此外的其他任何数据。
- 输出转义,当需要将一个字符串输出到Web网页时,同时又不确定这个字符串中是否包括XSS特殊字符,为了确保输出内容的完整性和正确性,输出HTML属性时可以使用HTML转义编码(HTMLEncode)进行处理,输出到
中,可以进行JS编码。
使用 HttpOnly Cookie 将重要的cookie标记为httponly,这样的话当浏览器向Web服务器发起请求的时就会带上cookie字段,但是在js脚本中却不能访问这个cookie,这样就避免了XSS攻击利用JavaScript的document.cookie获取cookie。
CSRF:跨站请求伪造,也称 XSRF,是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。与 XSS 相比,XSS利用的是用户对指定网站的信任,CSRF利用的是网站对用户网页浏览器的信任。
- 预防:用户操作限制——验证码机制
- 方法:添加验证码来识别是不是用户主动去发起这个请求,由于一定强度的验证码机器无法识别,因此危险网站不能伪造一个完整的请求。
- 优点:简单粗暴,低成本,可靠,能防范99.99%的攻击者。
- 缺点:对用户不友好。
- 请求来源限制——验证 HTTP Referer 字段
- 方法:在HTTP请求头中有一个字段叫Referer,它记录了请求的来源地址。 服务器需要做的是验证这个来源地址是否合法,如果是来自一些不受信任的网站,则拒绝响应。
- 优点:零成本,简单易实现。
- 缺点:由于这个方法严重依赖浏览器自身,因此安全性全看浏览器。
- 额外验证机制——token的使用
- 方法:使用token来代替验证码验证。由于黑客并不能拿到和看到cookie里的内容,所以无法伪造一个完整的请求。基本思路如下:
- 服务器随机产生token(比如把cookie hash化生成),存在session中,放在cookie中或者以ajax的形式交给前端。
- 前端发请求的时候,解析cookie中的token,放到请求url里或者请求头中。
- 服务器验证token,由于黑客无法得到或者伪造token,所以能防范csrf
正在整理中的问题
写过webpack loader吗
vue怎么监听数组
手写实现json
实现一个发布订阅模式;
手写快排,时间复杂度,优化
vue里面的虚拟dom
HTTPS的工作原理
那通过对象.方法调用箭头函数,里面的this指向什么
webpack用过吗?摇树是什么,什么场景下用过?
你遇到过最难的问题是什么
react 虚拟 dom实现,diff算法;
手写快排,怎么优化;说下sort实现原理;
解释一个你最近遇到的技术挑战
没有套路,只是面试前攒人品,有错误请指出,谢谢。
CSS 部分
两栏布局
要求:垂直两栏,左边固定右边自适应。
查看代码
三栏布局
要求:垂直三栏布局,左右两栏宽度固定,中间自适应
查看代码
圣杯布局 和 双飞翼布局
和三栏布局要求相同,不过中间列要写在前面保证优先渲染。
查看代码
三角形
实现一个三角形
常见题目,通过 border
实现 查看代码
正方形
使用 css 实现一个宽高自适应的正方形
查看代码
扇形
实现一个 1/4 圆、任意弧度的扇形
有多种实现方法,这里选几种简单方法(我看得懂的)实现。 查看代码
水平垂直居中
实现子元素的水平垂直居中
查看代码
水平垂直居中清除浮动
要求:清除浮动
可以通过 clear:both
或 BFC 实现 查看代码
弹出框
使用 CSS 写一个弹出框效果
查看代码
导航栏
要求:一个
div
内部放很多水平div
,并可以横向滚动。
查看代码
CSS 部分完,总结,Flex 无敌。
JavaScript 部分
手写 bind、call 和 apply
Function.prototype.bind = function(context, ...bindArgs) {
// func 为调用 bind 的原函数const func = this;
context = context || window;
if (typeof func !== 'function') {
thrownewTypeError('Bind must be called on a function');
}
// bind 返回一个绑定 this 的函数returnfunction(...callArgs) {
let args = bindArgs.concat(callArgs);
if (thisinstanceof func) {
// 意味着是通过 new 调用的 而 new 的优先级高于 bindreturnnew func(...args);
}
return func.call(context, ...args);
}
}
// 通过隐式绑定实现Function.prototype.call = function(context, ...args) {
context = context || window;
context.func = this;
if (typeof context.func !== 'function') {
thrownewTypeError('call must be called on a function');
}
let res = context.func(...args);
delete context.func;
return res;
}
Function.prototype.apply = function(context, args) {
context = context || window;
context.func = this;
if (typeof context.func !== 'function') {
thrownewTypeError('apply must be called on a function');
}
let res = context.func(...args);
delete context.func;
return res;
}
复制代码
复制代码
实现一个继承
// 参考 You Dont Know JavaScript 上卷// 基类functionBase() {
}
// 派生类functionDerived() {
Base.call(this);
}
// 将派生类的原型的原型链挂在基类的原型上Object.setPrototypeOf(Derived.prototype, Base.prototype);
复制代码
复制代码
实现一个 new
// 手动实现一个 new 关键字的功能的函数 _new(fun, args) --> new fun(args)function_new(fun, ...args) {
if (typeof fun !== 'function') {
returnnewError('参数必须是一个函数');
}
let obj = Object.create(fun.prototype);
let res = fun.call(obj, ...args);
if (res !== null && (typeof res === 'object' || typeof res === 'string')) {
return res;
}
return obj;
}
复制代码
复制代码
实现一个 instanceof
// a instanceof bfunction_instanceof(a, b) {
while (a) {
if (a.__proto__ === b.prototype) returntrue;
a = a.__proto__;
}
returnfalse;
}
复制代码
复制代码
手写 jsonp 的实现
// foo 函数将会被调用 传入后台返回的数据functionfoo(data) {
console.log('通过jsonp获取后台数据:', data);
document.getElementById('data').innerHTML = data;
}
/**
* 通过手动创建一个 script 标签发送一个 get 请求
* 并利用浏览器对