☆☆☆☆一、js中的数据类型
基本类型:number string boolean null undefined symbol bigint
bigint(BigInt
数据类型的目的是比 Number
数据类型支持的范围更大的整数值。在对大整数执行数学运算时,以任意精度表示整数的能力尤为重要。使用 BigInt
,整数溢出将不再是问题。)
引用数据类型:object (包含,Date,RegExp,Function,Array,Math..)
☆☆☆☆二、symbol的作用
首先说明symbol是基本类型之一,symbols 是一种无法被重建的基本类型
。这时 symbols 有点类似与对象创建的实例互相不相等的情况,但同时 symbols又是一种无法被改变的基本类型数据
。
const s1 = Symbol();
const s2 = Symbol();
console.log(s1 === s2);//false
可以看到symbol创建出来的值互不相同,即使传入相同的参数,也不相同,同时要注意,symbol不是被实例化出来的,不用new创建
。
所以symbol可以用作
作为对象的属性名
,可以保证属性名不会重复
。但要注意,symbol不能过通过for... in...遍历出来
const s1 = Symbol('a');
const s2 = Symbol('a');
var obj = {};
obj['a'] = "aaa"
obj[s1] = "asjdkl"
obj[s2] = "u can not see me"
for (const key in obj) {
console.log(key);
}
//a
如果想获取,可以通过Object.getOwnPropertySymbols()
来获取
for (const key of Object.getOwnPropertySymbols(obj)) {
console.log(key);
}
//Symbol(a)
三、类型转换
基本就是看代码得出结果,但是要完全理解背后的原理,推荐看冴羽的博客 github.com/mqyqingfeng…
☆☆☆☆四、如何判断变量的类型
typeof对于原始类型(除了Null)是可以检测到的,但是引用类型就统一返回object
instance of
用于检测构造函数的原型是否出现在某个实例函数的原型链上
最好的方法是使用Object.prototype.toString.call( )
进行类型判断,它可以检测到任何类型,返回的结果是[object Type]
的形式,基本可以实现所有类型的检测,我们用下面的代码来演示一下。
//实现一个检测类型的公共接口
function detectType(type) {
return function(obj) {
return {}.toString.call(obj) === `[object ${type}]`
}
}
//根据自己的需求进行扩展,记住类型的首字母要大写
const isArray = detectType("Array")
const isFunc = detectType("Function")
const isRegExp = detectType("RegExp")
console.log(isArray([1, 2, 3])); //true
console.log(isArray("[1,2,3]")); //false
console.log(isFunc(detectType)); //true
console.log(isRegExp(/test/)); //true
五、this的指向
js中this 的指向大致可以分为以下四个场景
1、在对象的方法中使用,this指向当前的对象
var obj = {
a: "hhh",
test() {
return this.a;
}
}
console.log(obj.test());//'hhh'
复制代码
2、在独立的函数中使用
在严格模式下,this指向undefined
非严格模式下,this指向全局对象,比如window
var a = "jjj"
var obj = {
a: "hhh",
test() {
return this.a;
}
}
const test = obj.test;
console.log(test());//"jjj"
复制代码
3、通过call\apply\bind来指定
三者都可传入一个要改变的this的值,来改变this指向,区别就是
call\\apply改变的同时执行函数
,bind
不执行
,而是返回这个函数
call\apply第一个参数就是要改变的this的值
,区别就是call传入的
是参数列表
,apply传入的是参数数组
4、构造函数
如果一个函数是构造函数
,那么this就指向它实例化出来的对象
5、箭头函数
箭头函数不会创建自己的this
,它只会从自己的作用域链的上一层继承this
,另外箭头函数里也不能
使用call\\apply\\bind修改this的指向
☆☆☆☆六、说一下什么是闭包?
闭包是一个可以访问其他作用域的变量
的函数
产生的原因
首先要了解作用域链的概念,函数的作用域就是它所创建的地方
,也就是说,函数在它被创建的时候就已经确定好它的作用域了。
函数在执行的时候遇到一个变量,他会先看看自己的作用域里有没有该变量,没有的话就会向上从父级作用域里去查找,直到找到位置,否则报错undefined
function f1() {
var a = 1;
function f2() {
console.log(a);
}
f2();
}
f1()//1
所以闭包的本质就是 存在对父级作用域的引用
,这里注意,上面的代码并不是闭包,我们并没有通过调用f1()来访问到不属于它作用域的变量,因为a本来就属于f1,我们只是通过f1调用了f2而已。 我们把上面的例子改造成闭包的形式,我们要在外面调用f2
function f1() {
var a = 1;
var f2 = function() {
console.log(a);
}
return f2
}
const clousure = f1();
clousure();//1
我们把f2作为返回值,在外部进行调用,可以看到我们可以访问到f1的变量,这就是闭包,也说明f2的作用域就是它所创建时的地方,而不是调用时的地方。
☆☆☆☆七、解释一下原型链
每一个函数有一个prototype
的属性,当他作为构造函数的时候
,它实例化出来的函数会有一个_ _proto_ _
的属性,它指向构造函数的prototype,函数通过prototype
来访问其父元素的属性和方法
,依此迭代访问,构成原型链,直到Object的原型为止,它位于原型链的顶端
访问某个属性或方法
的时候,会先从当前对象中查找
,没有的话就顺着原型链
开始寻找,直到找到最顶端
,也就是Object的原型(null)为止。另外每一个原型上都有一个constructor属性,指向相关联的构造函数
☆☆☆☆八、 DOM事件流和事件委托
DOM事件流分为三个阶段:
- 捕获阶段
- 目标阶段
- 冒泡阶段
捕获阶段:在事件冒泡的模型中,
捕获阶段不会响应任何事件
;目标阶段:目标阶段就是指
事件响应到触发事件的最底层元素上
;冒泡阶段:冒泡阶段就是事件的触发响应会从最底层目标一层层地向外到最外层(根节点),
事件代理即是利用事件冒泡的机制把里层所需要响应的事件绑定到外层
事件流描述的是从页面中接受事件的顺序,IE和网景推出了两个正好相反的概念,IE推出的是冒泡流,从下到上,网景则是事件捕获流,从上到下。
首先通过addEventListener
方法给元素添加点击事件,前两个参数分别是点击事件的名称和执行的回调,第三个参数就是是否开启捕获,确定事件发生的阶段,默认是false,也就是冒泡流。
事件委托,一般来说,会把一个或一组元素的事件委托到它的父元素上或者更外层元素上
,当事件响应到需要绑定的元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后在外层元素上去执行函数。在一些场景下,可以让性能得到优化。 比如给所有的列表li添加点击事件,如果采用冒泡流,那么我们需要给每个元素添加点击事件,而采用事件委托的话,只需要在ul上绑定一个事件即可。
☆☆☆☆九、js如何实现继承
- 通过原型链继承
function Parent() {
this.lastName = "wang"
}
Parent.prototype.asset = ['house', 'car']
function Child() {
}
Child.prototype = new Parent();
var child = new Child();
var child1 = new Child();
child.asset.push("plane")
console.log(child.lastName);//"wang"
console.log(child1.asset);//[ 'house', 'car', 'plane' ]
复制代码
优点:可以访问父类的属性和方法和原型上的属性和方法缺点:继承方法如果是引用类型,其中一个子类进行修改,那么全部都会受到影响
- 通过call来继承
function Parent() {
this.lastName = "wang";
this.hobby = ['a', 'b']
}
Parent.prototype.asset = ['house', 'car']
function Child() {
Parent.call(this)
}
var child = new Child();
var child1 = new Child();
child.hobby.push("c")
console.log(child.lastName);//“wang"
console.log(child1.hobby);//['a', 'b']
console.log(child1.asset);//undefined
优点:可以保证每个子类维护自己的属性 缺点:无法访问原型链上的属性和方法
- 组合继承
将前面二者结合
function Parent() {
this.lastName = "wang";
this.hobby = ['a', 'b']
}
Parent.prototype.asset = ['house', 'car']
function Child() {
Parent.call(this)
}
Child.prototype = new Parent();
var child = new Child();
var child1 = new Child();
child.hobby.push("c")
console.log(child.lastName);
console.log(child1.hobby);
//wang
//[ 'a', 'b' ]
//[ 'house', 'car' ]
复制代码
优点:既可以访问原型上的属性和方法,又可以每个子类维护自己属性 缺点:每次创建一个子类实例,父类都会被执行一次(涉及到new的内部实现,详情请看下一话题)
- 优化组合继承
将原型赋值语句修改如下
Child.prototype = Parent.prototype;
打印child会发现另一个问题
child的构造函数怎么能是Parent呢,所以我们还需要手动的修改一下
Child.prototype = Parent.prototype;
Child.prototype.constructor = Child;
另外也可以使用Object.create()
这个方法来创建一个指定原型的对象
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
十、new操作符都做了些什么
创建一个空对象
- 将空对象的
_ _proto_ _ 属性指向构造函数的原型
- 将
this指向这个对象
返回这个对象
代码实现:
function newFactory(constructor) {
var obj = {};
obj.__proto__ = constructor.prototype;
constructor.apply(obj, [].slice.call(arguments, 1));
return obj
}
function Test(name) {
this.name = name;
}
var t = newFactory(Test, "tom")
console.log(t.name);//"tom"
复制代码
十一、 let const
let 和 const都是es6的语法,之前的var是没有块级作用域
的,所以很容易造成全局变量污染
,而let 和 const都是有块级作用域
的。 let可以理解为带有块级作用域的var const则是指定常量的,一旦定义就不能再改变。
let 和 const声明的变量必须之前没有被使用过
,否则会报错,即let和const声明的变量,只能在声明之后引用
。另外
要注意const的一个点就是,当他定义为一个引用类型时,可以往里面添加内容。
经过验证,我们是可以接着向里面添加内容的。 但是不能替换它,因为原始值的话,变量直接引用的是这个值,只要值不相等就不能赋值,而引用类型,
变量只是拥有它在内存地址的引用,只要这个引用的地址值没变,我们还是可以对其进行操作的。
☆☆☆☆十二、 异步编程 promise,async/await
- promise 的介绍(状态。。)
- async await的使用
异步编程的方法
有
- 回调函数
- promise
- async/await等
回调函数在复杂场景下会造成回调地狱,影响代码的可读性和执行率。
promise是es6对异步编程的一个方案,它有三种状态,pending(挂起),fullfilled(成功),rejected(拒绝),状态一旦改变就不可逆。对应的变化有两种:
pending------> fullfilled (resolved 解决)
pending------> rejected (rejected 拒绝)
通过
then这个方法来实现异步调用之后的逻辑
,另外还支持链式调用。async/await是es7的语法,也是用来实现异步编程的,
语法是在function关键字前加上async
,代表是异步函数,await只能在async函数里使用。
async将任何函数转换为promise
,这时异步函数的特征之一。 await 可以使用在任何返回Promise函数的函数之前,并且会暂停在这里,直到promise返回结果才往下进行。async/awaite基本做到了用同步代码的风格实现异步逻辑
,让代码更简洁。
☆☆☆☆十三、Event Loop 机制
- 原理
- 看代码说出执行结果
js是一个单线程语言,所以所有的任务只能排队一个一个去做,这样效率明显很低。 所以event loop就是为解决这个问题而提出的。
在主程序中,分为两个线程
,一个运行程序本身,称作主线程
,另一个负责主线程和其它线程进行通信
(主要是I/O操作),被称作event loop线程
在运行程序
的时候,当遇到I/O
操作的时候,主线程就让Event loop 线程通知相应的I/O模块去执行
,然后主线程接着执行之后的代码
,等到I/O结束后
,event loop线程再把运行结果交给主线程
,然后主线程再执行相应的回调,整个任务结束。
宏任务 微任务
为了让这些任务在主线程上有条不紊的进行,V8采用队列的方式对这些任务进行存储,然后一一取出执行,其中包含了两种任务队列,除了上述提到的任务队列, 还有一个延迟队列,它专门处理诸如setTimeout/setInterval这样的定时器回调任务。 这两种任务队列里的任务都是**宏任务**
微任务 通常为 应当发生在当前脚本执行完后的事情 做安排,比如对一系列操作做出反应,或者让某些事情异步但是不承担宏任务的代价
微任务的执行有两种方案,一种是
等所有宏任务实行完毕然后依次执行微任务,
另一种是在
执行完一个宏任务之后
,检查微任务
队列,如果不为空则依次执行完微任务
,然后再执行宏任务
。
显然后者更满足需求,否则回调迟迟得不到执行,会造成应用卡顿。
常见的宏任务有:setTimeout setTimeInterval
常见的微任务有:"MutationObserver、Promise.then(或.reject) 以及以 Promise 为基础开发的其他技术(比如fetch API), 还包括 V8 的垃圾回收过程"
nextTick
process.nextTick 是一个独立于 eventLoop 的任务队列。在每一个 eventLoop 阶段完成后会去检查这个队列,如果里面有任务,会让这部分任务优先于微任务执行。
也就是说 new Promise在
实例化的过程
中所执行的代码都是同步进行
的,而then中注册的回调才是异步执行
的。
在同步代码执行完成后才会去检查是否有异步任务完成,并执行对应的回调,而微任务又会在宏任务之前执行
十四 防抖和节流
防抖:事件被调用后,在执行之前无论被调用多少次都会从头开始计时
节流:不管事件被调用多少次,总是
按规定时间间隔执行
代码实现:
//防抖
function debounce(fn, time) {
var timer;
return function() {
if (timer) {
clearTimeout(timer)
timer = null;
}
timer = setTimeout(() => {
clearTimeout(timer)
timer = null;
fn();
}, time);
}
}
//节流
function throttle(fn, time) {
var timer;
return function() {
if (timer) return;
timer = setTimeout(() => {
clearTimeout(timer)
timer = null;
fn();
}, time);
}
}
十五、requestAnimationFrame的优势
requestAnimationFrame不需要指定间隔时间
,它采用的是系统间隔
,一般是1秒60帧,每个16ms刷新一次
。 好处:
将更新在一次回流中全部提交,提升性能
- 当
页面处于未激活状态
时,requestAnimationFrame也会停止渲染,
当再次激活时,就会接着上一部分继续执行
十五、虚拟DOM的优缺点
优点:
- 保证性能下限。 操作真实的dom结构是一件非常昂贵的事情,虚拟Dom
利用js对象来模拟真实的dom
,从而降低了逻辑层面对dom结构操作的成本
- 无需操作真实的dom。 通过
双向数据绑定
,当数据发生改变
的时候,dom结构中的节点自动更新
,无需我们手动处理- 可移植性高,跨平台性好。 无论是vue、react还是weex等,我们都能看到虚拟dom的身影,通过各自的渲染进制进行将Dom结构渲染出来
缺点:
- 无法进行极致优化: 虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但
在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。
- 当VNODE几乎全部改变的时候,使用虚拟DOM进行重新渲染的时候就会白白浪费一次diff,不如直接进行渲染,
十六、 箭头函数
箭头函数有几个使用注意点。
(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
(2)
不可以当作构造函数
,也就是说,不可以使用new命令
,否则会抛出一个错误。(3)
不可以使用arguments对象
,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。(4)
不可以使用yield命令
,因此箭头函数不能用作 Generator 函数。
其中第一条非常重要,通过几个例子结合上文的this指向 来看一下
function Timer() {
this.s1 = 0;
this.s2 = 0;
// 箭头函数
setInterval(() => this.s1++, 1000);
// 普通函数
setInterval(function () {
this.s2++;
}, 1000);
}
var timer = new Timer();
setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1: 3
// s2: 0
复制代码
在上面这个例子中,定义了一个构造函数Timer,里面分别有两个定时器,第一个用箭头函数,第二个用普通函数,然后我们在外面定义了两个延时函数来获取s1,s2的值,
首先:箭头函数的this是在它定义时所在对象的值,也就是timer,经过了3.1s结果为3,
然后普通函数的this指向了全局对象,所以timer的s2,没有变还是0。
this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。
十七、手写curry柯里化
科里化函数在函数式编程中有很重要的应用,大致类似于链式调用,通过把函数科里化我们可以很好的把传参分开从而达到函数的功能复用,可以通过下面经典的了解一下
function add(a, b) {
return a + b
}
let addTen = curry(add,10)
addTen(1)//11
addTen(5)//15
let res = curry(add,1,2)//3
上面的add函数接受两个参数,我们把它科里化之后,可以将参数拆开进行传递,我们想创建一个工具函数addTen
,目的就是把传入的数字+10。当然这个功能完全没有必要通过科里化来实现,但是可以大致了解科里化的作用
下面来编写科里化函数
科里化的原理也很简单,如果我们传入的参数不少于函数的参数
,那么会直接执行并返回结果
,否则返回一个函数
,以供我们后续调用
,所以我们可以很快的实现一个简易的curry函数了
function curry(fn,...arg1){
return arg1.length===fn.length ? fn(...arg1) : function(...arg2){ return fn(...arg1,...arg2)}
}
复制代码
十八、手写异步串行、并行
from 前端面试常见的手写题
// 字节面试题,实现一个异步加法
function asyncAdd(a, b, callback) {
setTimeout(function () {
callback(null, a + b);
}, 500);
}
// 解决方案
// 1. promisify
const promiseAdd = (a, b) => new Promise((resolve, reject) => {
asyncAdd(a, b, (err, res) => {
if (err) {
reject(err)
} else {
resolve(res)
}
})
})
// 2. 串行处理
async function serialSum(...args) {
return args.reduce((task, now) => task.then(res => promiseAdd(res, now)), Promise.resolve(0))
}
// 3. 并行处理
async function parallelSum(...args) {
if (args.length === 1) return args[0]
const tasks = []
for (let i = 0; i < args.length; i += 2) {
tasks.push(promiseAdd(args[i], args[i + 1] || 0))
}
const results = await Promise.all(tasks)
return parallelSum(...results)
}
// 测试
(async () => {
console.log('Running...');
const res1 = await serialSum(1, 2, 3, 4, 5, 8, 9, 10, 11, 12)
console.log(res1)
const res2 = await parallelSum(1, 2, 3, 4, 5, 8, 9, 10, 11, 12)
console.log(res2)
console.log('Done');
})()
复制代码
☆☆☆☆十九、深拷贝
注意点:
- 对象循环引用的处理
- 日期、正则等对象的处理
- 数组、对象的处理
function deepCopy(obj, cache = new WeakMap()) {
if (typeof obj !== 'object' || obj === null) return obj
// 正则 日期等包装类对象 参考 https://blog.csdn.net/liwusen/article/details/78759373
if ({}.toString.call(obj) === '[object Date]') return new Date(obj.valueOf())
if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags)
// 防止循环引用
if (cache.get(obj)) return cache.get(obj)
const res = Array.isArray(obj) ? [] : {}
cache.set(obj, res)
for (const key in obj) {
if (typeof obj[key] === 'object') {
res[key] = deepCopy(obj[key], cache)
} else {
res[key] = obj[key]
}
}
return res
}
// 测试
const source = {
name: 'Jack',
meta: {
age: 12,
birth: new Date('1997-10-10'),
ary: [1, 2, { a: 1 }],
say() {
console.log('Hello')
}
}
}
source.source = source
const newObj = deepCopy(source)
console.log(newObj.meta.birth == source.meta.birth)
二十、手写随机生成16进制颜色与rgb互转
function generateRamdomColor() {
const dic = []
for (let i = 0; i < 16; i++) {
dic.push(i.toString(16))
}
const type = [3, 6]
let count = random(type)
return (
'#' +
(function tt(res) {
return (res += random(dic)) && res.length === count ? res : tt(res)
})('')
)
}
function random(arr) {
return arr[Math.floor(Math.random() * arr.length)]
}
function hex2rgb(hex) {
const str = hex.slice(1),
res = []
if (str.length === 3) {
for (const w of str) {
res.push(parseInt(w + w, 16))
}
} else {
for (let i = 0; i < 6; i += 2) {
res.push(parseInt(str[i] + str[i + 1], 16))
}
}
return res
}
let color = generateRamdomColor()
let rgb = hex2rgb(color)
console.log(color) //random hex color
console.log(rgb)
二十一、 BOM 之 location.hash & History
用过vue的同学都知道,路由 常用的有hash
和history
两种模式(abstract
不管,相信没啥人用吧),其实他们就对应着BOM的api,location.hash和History,下面我们简【单讨论一下两者的区别:
locatoin.hash
就是url的散列值,#
后跟零或多个字符,没有就是空字符串
这个
哈希值代表客户端的一种状态
,向服务器发送请求的时候
,哈希部分不会被发送
哈希值的改变不会触发页面变化
哈希值改变
会在浏览器中增加一条记录
可以通过
hasChange
事件来监听哈希值的变化
History
history对象表示当前窗口首次使用以来用户的导航历史记录
history通常用来创建“前进”“后退”按钮,如果url变化
(包括哈希值的改变),都会增加一条历史纪录
,这个行为通常被SPA用来模拟前进和后退
,这样做是因为不会触发页面刷新.
history通过状态管理api来改变url但不触发页面刷新,如pushState,这个方法执行后,就会在历史记录中增加一条记录,url也随之改变,除了这些以外,浏览器页不会向服务器发送请求。
需要注意的是,要确保通过发v创建的每个“假”url背后都对应着服务器上的一个真实物理url,否则手动刷新会导致为404
浏览器的回流与重绘 (Reflow & Repaint)
写在前面
在讨论回流与重绘之前,我们要知道:
- 浏览器使用流式布局模型 (Flow Based Layout)。
- 浏览器会把
HTML
解析成DOM
,把CSS
解析成CSSOM
,DOM
和CSSOM
合并就产生了Render Tree
。- 有了
RenderTree
,我们就知道了所有节点
的样式
,然后计算
他们在页面上的大小和位置
,最后把节点绘制到页面
上。- 由于浏览器使用流式布局,对
Render Tree
的计算通常只需要遍历一次就可以完成,但table
及其内部元素除外,他们可能需要多次计算,通常要花3倍于同等元素的时间,这也是为什么要避免使用table
布局的原因之一。一句话:回流必将引起重绘,重绘不一定会引起回流。
回流 (Reflow)
当Render Tree
中部分或全部元素的尺寸、结构、或某些属性发生改变时
,浏览器重新渲染部分或全部文档的过程称为回流
。
会导致回流的操作:
页面首次渲染
- 浏览器
窗口大小
发生改变元素尺寸或位置
发生改变元素内容
变化(文字数量或图片大小等等)元素字体大小
变化添加或者删除
可见的DOM
元素- 激活
CSS
伪类(例如::hover
)查询某些属性或调用某些方法
一些常用且会导致回流的属性和方法:
clientWidth
、clientHeight
、clientTop
、clientLeft
offsetWidth
、offsetHeight
、offsetTop
、offsetLeft
scrollWidth
、scrollHeight
、scrollTop
、scrollLeft
scrollIntoView()
、scrollIntoViewIfNeeded()
getComputedStyle()
getBoundingClientRect()
scrollTo()
重绘 (Repaint)
当页面中元素样式的改变
并不影响它在文档流中的位置
时(例如:color
、background-color
、visibility
等),浏览器会将新样式赋予给元素并重新绘制
它,这个过程称为重绘
。
性能影响
回流比重绘的代价要更高。
有时即使仅仅回流一个单一的元素,它的父元素以及任何跟随它的元素也会产生回流。
现代浏览器会对频繁的回流或重绘操作进行优化:
浏览器会维护一个队列,把所有引起回流和重绘的操作放入队列中,如果队列中的任务数量或者时间间隔达到一个阈值的,浏览器就会将队列清空,进行一次批处理,这样可以把多次回流和重绘变成一次。
当你访问以下属性+方法时,浏览器会立刻清空队列:
clientWidth
、clientHeight
、clientTop
、clientLeft
offsetWidth
、offsetHeight
、offsetTop
、offsetLeft
scrollWidth
、scrollHeight
、scrollTop
、scrollLeft
width
、height
getComputedStyle( )
getBoundingClientRect( )
因为队列中可能会有影响到这些属性或方法返回值的操作,即使你希望获取的信息与队列中操作引发的改变无关,浏览器也会强行清空队列,确保你拿到的值是最精确的。
如何避免
CSS
- 避免使用
table
布局。- 尽可能在
DOM
树的最末端改变class
。- 避免设置多层内联样式。
- 将动画效果应用到
position
属性为absolute
或fixed
的元素上。- 避免使用
CSS
表达式(例如:calc()
)。
JavaScript
- 避免
频繁操作样式
,最好一次性重写style
属性,或者将样式列表定义为class
并一次性更改class
属性。- 避免频繁操作
DOM
,创建一个documentFragment
,在它上面应用所有DOM操作
,最后再把它添加到文档中。- 也可以
先为元素设置display: none
,操作结束后再把它显示出来
。因为在display
属性为none
的元素上进行的DOM
操作不会引发回流和重绘。- 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
- 对具有
复杂动画的元素使用绝对定位
,使它脱离文档流
,否则会引起父元素及后续元素频繁回流
。
常见前端安全问题及解决方案
1.XSS
XSS全称(Cross Site Scripting)跨站脚本攻击,是前端最常见的安全问题。XSS是一种在web应用中的计算机安全漏洞,它允许恶意web用户将代码植入到提供给其它用户使用的页面中,攻击者通过注入非法的html标签或者javascript代码,从而当用户浏览该网页时,控制用户浏览器。
类别
1. DOM型xss
利用DOM本身存在的缺陷进行攻击。
如下代码,页面中某个图片获取路径。其中,返回的{{img.src}}='/xxx' onerror='/xxx'
,img标签就变成了<img src="/xxx" onerror=xxx">
。src肯定会加载失败,然后会执行onerror中注入的恶意代码
,达到攻击效果
。
<img src="{{img.src}}">
2. 反射型xss
反射型XSS也被称为非持久性XSS,是现在最容易出现的一种XSS漏洞。XSS代码出现在URL中
,通过引诱用户点击一个链接到目标网站的恶意链接来实施攻击
。
如下恶意链接,其中,xxx是恶意代码
。传到服务器的参数data,被服务器接收
之后,响应的页面包含data这个变量的
,会将恶意代码注入到页面上面
,进行攻击。
http://www.abc.com?data=xxx
3. 存储型xss
存储型XSS又被称为持久性XSS,它是最危险的一种跨站脚本,相比反射型XSS和DOM型XSS具有更高的隐蔽性,所以危害更大,它不需要用户手动触发。
当攻击者提交一段XSS代码
后,被服务器端接收并存储
,当所有浏览者访问某个页面时都会被XSS
,其中最典型的例子就是留言板
。
解决方案
1.过滤。
对用户的输入进行过滤,通过将<>
''
""
等字符进行转义,移除用户输入的Style节点、Script节点、Iframe节点。
function filterXss(str){
var s = "";
if(str.length == 0) return "";
s = str.replace(/&/g,"&");
s = s.replace(/</g,"<");
s = s.replace(/>/g,">");
s = s.replace(/ /g," ");
s = s.replace(/\'/g,"'");
s = s.replace(/\"/g,""");
return s;
}
2.编码
根据输出数据所在的上下文来进行相应的编码
。数据放置于HTML元素中,需进行HTML编码,放置于URL中,需要进行URL编码。此外,还有JavaScript编码、CSS编码、HTML属性编码、JSON编码等等。
3.httpOnly
在cookie中设置HttpOnly
属性,使js脚本无法读取到cookie信息。
2.CSRF
CSRF全称(Cross-Site Request Forgeries)跨站请求伪造。指攻击者冒充用户发起请求
(在用户不知情的情况下),完成一些违背用户意愿的事情。攻击过程如下图所示:
解决方案
1.使用token
服务器产生一个token存到session
中,同时将token发送给客户端
,客户端提交表单时带上该token,服务器验证token与session是否一致
,一致就允许访问,否则拒绝访问。
2.Referer 验证
Referer
指的是页面请求来源,意思是,只接受本站的请求
,服务器才做响应;如果不是,就拦截。
3.使用验证码
对于重要请求,要求用户输入验证码
,强制用户必须与应用进行交互
,才能完成最终请求。
3.点击劫持
点击劫持就是将一个危险网站设置透明,然后在其上方设置一个按钮,当你点击这个按钮的时候,就会触发底部恶意网站的某些事件。
解决方案
1.设置http响应头 X-Frame-Options
X-Frame-Options HTTP 响应头是用来给浏览器指示允许一个页面可否在
<frame>
,<iframe>
或者<object>
中展现的标记。网站可以使用此功能,来确保自己网站的内容没有被嵌到别人的网站中去。2.使用CSP(Content Security Policy)内容安全策略
4.不安全的第三方依赖
现如今进行应用开发,无论是后端服务器应用还是前端应用开发,绝大多数时候我们都是在借助开发框架和各种类库进行快速开发。然而,一些第三方的依赖或者插件存在很多安全性问题,也会存在这样那样的漏洞,所以使用起来得谨慎。
解决方案
1.尽量减少第三方依赖,选用相对成熟的依赖包
。2.使用
自动化工具检查这些第三方代码有没有安全问题
,比如NSP(Node Security Platform),Snyk
等等。
5.本地存储数据泄露
很多开发者为了方便,把一些个人信息不经加密直接存到本地或者cookie,这样是非常不安全的,黑客们可以很容易就拿到用户的信息。
解决方案
1.不在本地存储重要数据
敏感、机密信息不要存储在本地。
2.加密
所有在放到cookie中的信息或者localStorage里的信息要进行加密
,加密可以自己定义一些加密方法或者网上寻找一些加密的插件
,或者用base64进行多次加密然后再多次解码
。
☆☆☆☆ajax和axios、fetch的区别
1. JQuery ajax
$.ajax({
type: 'POST',
url: url,
data: data,
dataType: dataType,
success: function () {},
error: function () {}
});
传统 Ajax 指的是 XMLHttpRequest(XHR), 最早出现的发送后端请求技术,隶属于原始js中,核心使用XMLHttpRequest对象,多个请求之间如果有先后关系的话,就会出现回调地狱。
JQuery ajax 是对原生XHR的封装
,除此以外还增添了对JSONP的支持。经过多年的更新维护,真的已经是非常的方便了,优点无需多言;如果是硬要举出几个缺点,那可能只有:
1.本身是针对MVC的编程
,不符合现在前端MVVM的浪潮
2.基于
原生的XHR
开发,XHR本身的架构不清晰。
3.JQuery整个项目太大
,单纯使用ajax却要引入整个JQuery非常的不合理
(采取个性化打包的方案又不能享受CDN服务)
4.不符合关注分离
(Separation of Concerns)的原则
5.配置和调用方式非常混乱
,而且基于事件的异步模型不友好。
PS:MVVM(Model-View-ViewModel), 源自于经典的 Model–View–Controller(MVC)模式。MVVM 的出现促进了 GUI 前端开发与后端业务逻辑的分离,极大地提高了前端开发效率。
MVVM 的核心是 ViewModel 层,它就像是一个中转站(value converter),负责转换 Model 中的数据对象来让数据变得更容易管理和使用,该层向上与视图层进行双向数据绑定,向下与 Model 层通过接口请求进行数据交互,起呈上启下作用。
View 层展现的不是 Model 层的数据,而是 ViewModel 的数据,由 ViewModel 负责与 Model 层交互,这就完全解耦了 View 层和 Model 层,这个解耦是至关重要的,它是前后端分离方案实施的最重要一环。
如下图所示:
2.axios
axios({
method: 'post',
url: '/user/12345',
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
Vue2.0之后,尤雨溪推荐大家用axios替换JQuery ajax,想必让axios进入了很多人的目光中。axios 是一个基于Promise 用于浏览器和 nodejs 的 HTTP 客户端,
本质上也是对原生XHR的封装
,只不过它是Promise的实现版本
,符合最新的ES规范
,它本身具有以下特征:
1.从浏览器中创建 XMLHttpRequest
2.支持 Promise API
3.客户端支持防止CSRF
4.提供了一些并发请求的接口
(重要,方便了很多的操作)
5.从node.js创建 http 请求
6.拦截请求和响应
7.转换请求和响应数据
8.取消请求
9.自动转换JSON数据
PS:防止CSRF:就是让你的每个请求
都带一个从cookie中拿到的key
, 根据浏览器同源策略,假冒的网站是拿不到你cookie中得key的
,这样,后台就可以轻松辨别出这个请求是否是用户在假冒网站上的误导输入,从而采取正确的策略。
3.fetch
try {
let response = await fetch(url);
let data = response.json();
console.log(data);
} catch(e) {
console.log("Oops, error", e);
}
fetch号称是AJAX的替代品,是在ES6出现
的,使用了ES6中的promise对象。Fetch是基于promise设计的
。Fetch的代码结构比起ajax简单多了
,参数有点像jQuery ajax。但是,一定记住fetch不是ajax的进一步封装,而是原生js,没有使用XMLHttpRequest对象。
fetch的优点:
1.符合关注分离
,没有将输入、输出和用事件来跟踪的状态混杂在一个对象里
2.更好更方便的写法
坦白说,上面的理由对我来说完全没有什么说服力,因为不管是Jquery还是Axios都已经帮我们把xhr封装的足够好,使用起来也足够方便,为什么我们还要花费大力气去学习fetch?
我认为fetch的优势主要优势就是:
语法简洁,更加语义化
基于标准 Promise 实现,支持 async/await
同构方便,使用
- 更加底层,提供的API丰富(request, response)
脱离了XHR
,是ES规范里新的实现方式
最近在使用fetch的时候,也遇到了不少的问题:
fetch是一个低层次的API
,你可以把它考虑成原生的XHR,所以使用起来并不是那么舒服,需要进行封装。
例如:
1)fetch只对网络请求报错
,对400,500都当做成功的请求,服务器返回 400,500 错误码时并不会 reject,只有网络错误这些导致请求不能完成时,fetch 才会被 reject。
2)fetch默认不会带cookie
,需要添加配置项: fetch(url, {credentials: 'include'})
3)fetch不支持abort
,不支持超时控制
,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费
4)fetch没有办法原生监测请求的进度
,而XHR可以
总结:axios既提供了并发的封装,也没有fetch的各种问题,而且体积也较小,当之无愧现在最应该选用的请求的方式。
JSON.stringify深拷贝的缺点
浅拷贝和深拷贝:
- 针对对象
深拷贝和浅拷贝只针对像Object和Array这样的复杂对象
的,String,Number等简单类型不存在深拷贝
。
浅拷贝:
因为浅拷贝只会将对象的各个属性进行依次复制,并不会进行递归复制。在JavaScript中,对于Object和Array这类引用类型值,当从一个变量向另一个变量复制引用类型值时
,这个值的副本其实是一个指针,两个变量指向同一个堆对象
,改变其中一个变量,另一个也会受到影响
。所以浅拷贝会导致 obj.arr 和 shallowObj.arr 指向同一块内存地址
,当修改obj.arr的值时,shallowObj.arr的值同样会被修改,
var a={a:1,b:2,c:3}
var b=a
b.a=2
console.log(a,b)
深拷贝
而深拷贝则不同,它不仅将原对象的各个属性逐个复制出去
,而且将原对象各个属性所包含的对象
也依次采用深拷贝的方法递归复制到新对象
上。这就不会存在上面 obj 和 shallowObj 的 arr 属性指向同一个对象的问题
。当修改obj.arr的值时,shallowObj.arr的值不会被修改,仍然为原值
eg:
function deepClone(obj){
let obj_ = JSON.stringify(obj),
loneObj = JSON.parse(obj_);
return loneObj
}
let a=[1,2,3,4],
b=deepClone(a);
b[0] = 2;
console.log(a,b);
弊端:
1.如果obj里面有时间对象,则JSON.stringify后再JSON.parse的结果,时间将只是字符串的形式,而不是对象的形式
eg:
var test = {
name: 'a',
date: [new Date(1536627600000), new Date(1540047600000)],
};
let b;
b = JSON.parse(JSON.stringify(test));
console.log(b);
2.如果obj里有RegExp(正则表达式的缩写)、Error对象,则序列化的结果将只得到空对象;
eg:
const test = {
name: 'a',
date: new RegExp('\\w+'),
};
// debugger
const copyed = JSON.parse(JSON.stringify(test));
test.name = 'test'
//console.error('ddd', test, copyed);
console.log(copyed); //date: {空} name: "a"
3、如果obj里有函数,undefined,则序列化的结果会把函数或 undefined丢失;
eg:
const test = {
name: 'a',
date: function hehe() {
console.log('fff')
},
};
// debugger
const copyed = JSON.parse(JSON.stringify(test));
test.name = 'test'
console.error('ddd', test, copyed);
4、如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null
5、JSON.stringify()
只能序列化对象的可枚举的自有属性
,例如 如果obj中的对象是有构造函数生成
的, 则使用JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的constructor
;
eg:
function Person(name) {
this.name = name;
console.log(name)
}
const liai = new Person('liai');
const test = {
name: 'a',
date: liai,
};
// debugger
const copyed = JSON.parse(JSON.stringify(test));
test.name = 'test'
console.error('ddd', test, copyed)
6、如果对象中存在循环引用
的情况也无法正确实现深拷贝
;总结:
用法简单,然而使用这种方法会有一些隐藏的坑:因为在序列化JavaScript对象时,所有函数和原型成员会被有意忽略。
通俗点说,JSON.parse(JSON.stringfy(X)),其中X只能是Number, String, Boolean, Array, 扁平对象,即那些能够被 JSON 直接表示的数据结构。
JSON.stringify()的几种妙用:
判断
数组是否包含某对象
,或者判断对象是否相等
。
let data = [
{name:'echo'},
{name:'听风是风'},
{name:'天子笑'},
],
val = {name:'天子笑'};
console.log(JSON.stringify(data).indexOf(JSON.stringify(val)) !== -1); //true
console.log(data.indexOf(val) !== -1) //false
//因为数组和对象是引用值不能比较,所以需要序列化一下转成字符串
判断两数组/对象是否相等
,但是不准确,那些在经过转化的时候消失的都会出问题,例如undefined
var arr = [1,2,3]
var arr1 = [1,2,3]
console.log(JSON.stringify(arr)===JSON.stringify(arr))
```
让localStorage/sessionStorage可以存储对象。
- localStorage/sessionStorage默认只能存储字符串,而实际开发中,我们往往需要存储的数据多为对象类型,那么这里我们就可以在存储时利用json.stringify()将对象转为字符串,而在取缓存时,只需配合json.parse()转回对象即可。
//存
function setLocalStorage(key,val){
window.localStorage.setItem(key,JSON.stringify(val));
};
//取
function getLocalStorage(key){
let val = JSON.parse(window.localStorage.getItem(key));
return val;
};
//测试
setLocalStorage('demo',[1,2,3]);
let a = getLocalStorage('demo');//[1,2,3]
JSON.stringify()与toString()这两者虽然都可以将目标值转为字符串,但本质上还是有区别的
,比如
let arr = [1,2,3];
console.log(JSON.stringify(arr));//'[1,2,3]'
console.log(arr.toString());//1,2,3
☆☆☆☆深入理解浏览器的缓存机制
一、前言
缓存可以说是性能优化中简单高 效的一种优化方式了。一个优秀的缓存策略可以缩短网页请求资源的距离,减少延迟,并且由于缓存文件可以重复利用,还可以减少带宽,降低网络负荷。
对于一个数据请求
来说,可以分为发起网络请求、后端处理、浏览器响应三个步
骤。浏览器缓存可以帮助我们在第一和第三步骤中优化性能。比如说直接使用缓存而不发起请求,或者发起了请求但后端存储的数据和前端一致,那么就没有必要再将数据回传回来,这样就减少了响应数据。
接下来的内容中我们将通过缓存位置、缓存策略以及实际场景应用缓存策略来探讨浏览器缓存机制。
如需获取思维导图或想阅读更多优质文章请猛戳GitHub 博客
二、缓存位置
从缓存位置上来说分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络。
- Service Worker
- Memory Cache
- Disk Cache
- Push Cache
1.Service Worker
Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。
使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。
Service Worker 实现缓存功能一般分为三个步骤:首先需要先注册 Service Worker
,然后监听到 install 事件以后就可以缓存需要的文件
,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。
当 Service Worker 没有命中缓存的时候,我们需要去调用 fetch 函数获取数据。也就是说,如果我们没有在 Service Worker 命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示我们是从 Service Worker 中获取的内容。
2.Memory Cache
Memory Cache 也就是内存中的缓存
,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。
一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。
那么既然内存缓存这么高效,我们是不是能让数据都存放在内存中呢?
这是不可能的。计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。
当我们访问过页面以后,再次刷新页面,可以发现很多数据都来自于内存缓存
内存缓存中有一块重要的缓存资源是preloader相关指令(例如<link rel="prefetch">
)下载的资源。总所周知preloader的相关指令已经是页面优化的常见手段之一,它可以一边解析js/css文件,一边网络请求下一个资源。
需要注意的事情是,内存缓存在缓存资源时并不关心返回资源的HTTP缓存头Cache-Control是什么值,同时资源的匹配也并非仅仅是对URL做匹配,还可能会对Content-Type,CORS等其他特征做校验。
3.Disk Cache
Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。
在所有浏览器缓存中,Disk Cache 覆盖面基本是最大的。它会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。绝大部分的缓存都来自 Disk Cache,关于 HTTP 的协议头中的缓存字段,我们会在下文进行详细介绍。
浏览器会把哪些文件丢进内存中?哪些丢进硬盘中?
关于这点,网上说法不一,不过以下观点比较靠得住:
- 对于大文件来说,大概率是不存储在内存中的,反之优先
- 当前系统内存使用率高的话,文件优先存储进硬盘
4.Push Cache
Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在Chrome浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令。
Push Cache 在国内能够查到的资料很少,也是因为 HTTP/2 在国内不够普及。这里推荐阅读Jake Archibald
的 HTTP/2 push is tougher than I thought 这篇文章,文章中的几个结论:
- 所有的资源都能被推送,并且能够被缓存,但是 Edge 和 Safari 浏览器支持相对比较差
- 可以推送 no-cache 和 no-store 的资源
- 一旦连接被关闭,Push Cache 就被释放
- 多个页面可以使用同一个HTTP/2的连接,也就可以使用同一个Push Cache。这主要还是依赖浏览器的实现而定,出于对性能的考虑,有的浏览器会对相同域名但不同的tab标签使用同一个HTTP连接。
- Push Cache 中的缓存只能被使用一次
- 浏览器可以拒绝接受已经存在的资源推送
- 你可以给其他域名推送资源
如果以上四种缓存都没有命中的话,那么只能发起请求来获取资源了。
那么为了性能上的考虑,大部分的接口都应该选择好缓存策略,通常浏览器缓存策略分为两种:强缓存和协商缓存,并且缓存策略都是通过设置 HTTP Header 来实现的。
三、缓存过程分析
浏览器与服务器通信的方式为应答模式,即是:浏览器发起HTTP请求 – 服务器响应该请求,那么浏览器怎么确定一个资源该不该缓存,如何去缓存呢?浏览器第一次向服务器发起该请求后拿到请求结果后,将请求结果和缓存标识存入浏览器缓存,浏览器对于缓存的处理是根据第一次请求资源时返回的响应头来确定的。具体过程如下图:
第一次发起HTTP请求
由上图我们可以知道:
- 浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识
- 浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中
以上两点结论就是浏览器缓存机制的关键,它确保了每个请求的缓存存入与读取,只要我们再理解浏览器缓存的使用规则,那么所有的问题就迎刃而解了,本文也将围绕着这点进行详细分析。为了方便大家理解,这里我们根据是否需要向服务器重新发起HTTP请求将缓存过程分为两个部分,分别是强缓存和协商缓存。
四、强缓存
强缓存:不会向服务器发送请求,直接从缓存中读取资源,在chrome控制台的Network选项中可以看到该请求返回200的状态码,并且Size显示from disk cache或from memory cache。强缓存可以通过设置两种 HTTP Header 实现:Expires 和 Cache-Control。
1.Expires
缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点。也就是说,Expires=max-age + 请求时间,需要和Last-modified结合使用。Expires是Web服务器响应消息头字段,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。
Expires 是 HTTP/1 的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失效。Expires: Wed, 22 Oct 2018 08:41:00 GMT
表示资源会在 Wed, 22 Oct 2018 08:41:00 GMT 后过期,需要再次请求。
2.Cache-Control
在HTTP/1.1中,Cache-Control是最重要的规则,主要用于控制网页缓存。比如当Cache-Control:max-age=300
时,则代表在这个请求正确返回时间(浏览器也会记录下来)的5分钟内再次加载资源,就会命中强缓存。
Cache-Control 可以在请求头或者响应头中设置,并且可以组合使用多种指令:
image
public:所有内容都将被缓存(客户端和代理服务器都可缓存)。具体来说响应可被任何中间节点缓存,如 Browser <-- proxy1 <-- proxy2 <-- Server,中间的proxy可以缓存资源,比如下次再请求同一资源proxy1直接把自己缓存的东西给 Browser 而不再向proxy2要。
private:所有内容只有客户端可以缓存,Cache-Control的默认取值。具体来说,表示中间节点不允许缓存,对于Browser <-- proxy1 <-- proxy2 <-- Server,proxy 会老老实实把Server 返回的数据发送给proxy1,自己不缓存任何数据。当下次Browser再次请求时proxy会做好请求转发而不是自作主张给自己缓存的数据。
no-cache:客户端缓存内容,是否使用缓存则需要经过协商缓存来验证决定。表示不使用 Cache-Control的缓存控制方式做前置验证,而是使用 Etag 或者Last-Modified字段来控制缓存。需要注意的是,no-cache这个名字有一点误导。设置了no-cache之后,并不是说浏览器就不再缓存数据,只是浏览器在使用缓存数据时,需要先确认一下数据是否还跟服务器保持一致。
no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存
max-age:max-age=xxx (xxx is numeric)表示缓存内容将在xxx秒后失效
s-maxage(单位为s):同max-age作用一样,只在代理服务器中生效(比如CDN缓存)。比如当s-maxage=60时,在这60秒中,即使更新了CDN的内容,浏览器也不会进行请求。max-age用于普通缓存,而s-maxage用于代理缓存。s-maxage的优先级高于max-age。如果存在s-maxage,则会覆盖掉max-age和Expires header。
max-stale:能容忍的最大过期时间。max-stale指令标示了客户端愿意接收一个已经过期了的响应。如果指定了max-stale的值,则最大容忍时间为对应的秒数。如果没有指定,那么说明浏览器愿意接收任何age的响应(age表示响应由源站生成或确认的时间与当前时间的差值)。
min-fresh:能够容忍的最小新鲜度。min-fresh标示了客户端不愿意接受新鲜度不多于当前的age加上min-fresh设定的时间之和的响应。
cache-control
从图中我们可以看到,我们可以将多个指令配合起来一起使用,达到多个目的。比如说我们希望资源能被缓存下来,并且是客户端和代理服务器都能缓存,还能设置缓存失效时间等等。
3.Expires和Cache-Control两者对比
其实这两者差别不大,区别就在于 Expires 是http1.0的产物,Cache-Control是http1.1的产物,两者同时存在的话,Cache-Control优先级高于Expires;在某些不支持HTTP1.1的环境下,Expires就会发挥用处。所以Expires其实是过时的产物,现阶段它的存在只是一种兼容性的写法。
强缓存判断是否缓存的依据来自于是否超出某个时间或者某个时间段,而不关心服务器端文件是否已经更新,这可能会导致加载文件不是服务器端最新的内容,那我们如何获知服务器端内容是否已经发生了更新呢?此时我们需要用到协商缓存策略。
五、协商缓存
协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,主要有以下两种情况:
- 协商缓存生效,返回304和Not Modified
协商缓存生效
- 协商缓存失效,返回200和请求结果
协商缓存失效
协商缓存可以通过设置两种 HTTP Header 实现:Last-Modified 和 ETag 。
1.Last-Modified和If-Modified-Since
浏览器在第一次访问资源时,服务器返回资源的同时,在response header中添加 Last-Modified的header,值是这个资源在服务器上的最后修改时间,浏览器接收后缓存文件和header;
Last-Modified: Fri, 22 Jul 2016 01:47:00 GMT
浏览器下一次请求这个资源,浏览器检测到有 Last-Modified这个header,于是添加If-Modified-Since这个header,值就是Last-Modified中的值;服务器再次收到这个资源请求,会根据 If-Modified-Since 中的值与服务器中这个资源的最后修改时间对比,如果没有变化,返回304和空的响应体,直接从缓存读取,如果If-Modified-Since的时间小于服务器中这个资源的最后修改时间,说明文件有更新,于是返回新的资源文件和200
image
但是 Last-Modified 存在一些弊端:
- 如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务端不能命中缓存导致发送相同的资源
- 因为 Last-Modified 只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源
既然根据文件修改时间来决定是否缓存尚有不足,能否可以直接根据文件内容是否修改来决定缓存策略?所以在 HTTP / 1.1 出现了 ETag
和If-None-Match
2.ETag和If-None-Match
Etag是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),只要资源有变化,Etag就会重新生成。浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的Etag值放到request header里的If-None-Match里,服务器只需要比较客户端传来的If-None-Match跟自己服务器上该资源的ETag是否一致,就能很好地判断资源相对客户端而言是否被修改过了。如果服务器发现ETag匹配不上,那么直接以常规GET 200回包形式将新的资源(当然也包括了新的ETag)发给客户端;如果ETag是一致的,则直接返回304知会客户端直接使用本地缓存即可。
ETag和If-None-Match
3.两者之间对比:
- 首先在精确度上,Etag要优于Last-Modified。
Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的Last-Modified也有可能不一致。
- 第二在性能上,Etag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值。
- 第三在优先级上,服务器校验优先考虑Etag
六、缓存机制
强制缓存优先于协商缓存进行,若强制缓存(Expires和Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since和Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,返回200,重新返回资源和缓存标识,再存入浏览器缓存中;生效则返回304,继续使用缓存。具体流程图如下:
缓存的机制
看到这里,不知道你是否存在这样一个疑问:如果什么缓存策略都没设置,那么浏览器会怎么处理?
对于这种情况,浏览器会采用一个启发式的算法,通常会取响应头中的 Date 减去 Last-Modified 值的 10% 作为缓存时间。
七、实际场景应用缓存策略
1.频繁变动的资源
Cache-Control: no-cache
对于频繁变动的资源,首先需要使用Cache-Control: no-cache
使浏览器每次都请求服务器,然后配合 ETag 或者 Last-Modified 来验证资源是否有效。这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小。
2.不常变化的资源
Cache-Control: max-age=31536000
通常在处理这类资源时,给它们的 Cache-Control 配置一个很大的 max-age=31536000
(一年),这样浏览器之后请求相同的 URL 会命中强制缓存。而为了解决更新的问题,就需要在文件名(或者路径)中添加 hash, 版本号等动态字符,之后更改动态字符,从而达到更改引用 URL 的目的,让之前的强制缓存失效 (其实并未立即失效,只是不再使用了而已)。
在线提供的类库 (如 jquery-3.3.1.min.js
, lodash.min.js
等) 均采用这个模式。
八、用户行为对浏览器缓存的影响
所谓用户行为对浏览器缓存的影响,指的就是用户在浏览器如何操作时,会触发怎样的缓存策略。主要有 3 种:
- 打开网页,地址栏输入地址: 查找 disk cache 中是否有匹配。如有则使用;如没有则发送网络请求。
- 普通刷新 (F5):因为 TAB 并没有关闭,因此 memory cache 是可用的,会被优先使用(如果匹配的话)。其次才是 disk cache。
- 强制刷新 (Ctrl + F5):浏览器不使用缓存,因此发送的请求头部均带有
Cache-control: no-cache
(为了兼容,还带了Pragma: no-cache
),服务器直接返回 200 和最新内容。
HTTP与TCP的区别和联系
HTTP与TCP的区别和联系
TCP对应于传输层
,HTTP对应于应用层
,从本质上来说,二者没有可比性。
Http协议是建立在TCP协议基础之上的
,当浏览器需要从服务器获取网页数据
的时候,会发出一次Http
请求。Http
会通过TCP建立起一个到服务器的连接通道
,当本次请求需要的数据完毕
后,Http会立即将TCP连接断开
,这个过程是很短
的。所以Http连接是一种短连接,是一种无状态的连接。
TCP是
底层协议
,定义的是数据传输和连接方式的规范
。HTTP是
应用层协议
,定义的是传输数据的内容的规范。
HTTP
协议中的数据是利用TCP协议传输
的,所以支持HTTP就一定支持TCP。
☆☆☆???webpack构建之loader与plugin的区别
在webpack中,loader和plugin是俩个关键的部分,常常被面试官拿来比较他们,那么他们到底有什么区别呢?
loader
loader是文件加载器,能够加载资源文件,并对这些文件进行统一处理,诸如编译、压缩等,最终一起打包到指定的文件中。
处理一个文件可以使用多个loader,loader的执行顺序和配置中的顺序正好相反,也就是说最后一个loader最先执行,第一个loader最后执行。第一个执行的loader的返回值接收源文件内容作为参数,其他loader接收前一个执行的loader的返回值作为参数,最后执行的loader会返回此模块的JavaScript的源码。
编写自己的loader的时候需要引用官方提供的loader-utils,调用loaderUtils.getOptions(this)来拿到webpack的配置参数,然后进行自己的处理。loader本身只是一个函数。接收模块代码的内容然后发返回代码内容转化后的结果。并且一个文件还可以链式的经过多个loader的转化,比如:less-loader-->css-loader-->style-loader.一个loader的职责是单一的。只需要完成一种转化,如果一个源文件需要多步转化才能正常使用,就通过多个loader转化,在调用多个loader去转化一个文件的时候,每个loader会链式的去顺序执行。每一个loader将会拿到需要处理的原内容。上一个laoder处理的结果将会传给下一个loader接着处理,最后的loader将处理的最终结果返回给webpack。
简单的loader例子:
module.exports = function(source) {
// do something
return source
}
常用的loader有哪些?
file-loader: 文件加载;url-loader:文件加载,在文件大小(单位 byte)低于指定的限制时,可以返回一个 DataURL;
image-loader:加载并压缩图片
json-loader:处理json,webpack已经默认包含
babel-loader:ES6+ 转化成ES5
ts-loader:将ts转化成js
postcss-loader:扩展css语法。使用postcss各种插件autoprefixer,cssnext,cssnano
eslint-loader:检查代码
vue-loader:加载vue单文件组件
i18n-loader:国际化
cache-loader:性能开销大的loader前添加,将结果缓存到磁盘
expose-loader:暴露对象为全局对象
import-loader,exports-loader等可以向模块诸如变量或者提供导出模块功能
plugin
plugin的功能更加强大,loader不能做的,plugin都能做。plugin的功能要更加丰富,从打包 优化和压缩,到从新定义环境变量。功能强大到可以用来处理各种各样的任务。
plugin让webpack的机制更加灵活,他的编译过程中留下的一系列生命周期钩子,通过调用这些钩子来实现在不同编译结果时对源模块进行处理。
他的编译是基于事件流来编译的,主要通过taptable来实现插件的绑定和执行的,taptable主要就是基于发布订阅的插件架构,是用来创建生命周期钩子的库,通过complier.hooks.run.tap开始注册创建complilation,基于创建chunks,通过parser解析chunks,使用模块和依赖管理模块之间的依赖关系。最后使用template基于compilation数据生成结果代码。
plugin的实现可以是一个类,使用的时候传入相关配置来创建一个实例,然后放到配置的plugins字段中,而plugin实例中最重要的方法就是apply,该方法在webpack complier安装插件的时候会被调用一次。apply接收webpack complier对象实例的引用。你可以在complier对象实例上注册各种事件钩子函数。来影响webpack的所有构建流程。以便于完成更多的其他构建任务。
简单的plugin例子:
class BasicPlugin {
constructor (options) {}
apply(complier) {
complier.plugin('compilation', function(compilation) {
// do something
})
}
}
module.exports = BasicPlugin
常用的plugin有哪些?
ignore-plugin:忽略文件
uglifyjs-webpack-plugin:不支持es6压缩
terser-webpack-plugin:支持es6压缩
webpack-parallel-uglify-plugin:多进程执行代码压缩,提升构建速度
serviceworker-webpack-plugin:为网页应用增加离线缓存功能
commonsChunkPlugin:提高打包效率,将第三方库和业务代码分开打包
html-webpack-plugin:可以根据模版自动生成html代码,并自动引用css和js文件
HotModuleRelpacementPlugin:热更新
less sass与css区别
1.变量,
Less-作用域如
@color:#00c;
\#header{
@color:#c00;/*red*/
border:1px solid @color;/*红色边框*/
}
\#footer{
border:1px solid @color;/*蓝色边框*/
}
就相当于js里面设置全局变量和局部变量的作用域
2.嵌套,
.box{
width:20px;
height:20px;
.inner{
width:10px;
height:10px
}
}
3.运算符,
可以直接在css预处理中进行样式计算
body{
margin:(14px/2);
top:50px+100px;
right:100px-50px;
left:10*10;
}
4.混入(Minxin),
创建一个minxin来处理不同浏览器的css写法是很简单的,节省了大量的重复工作和痛苦的代码编辑。
Sass
@mixin border-radius($values){
-webkit-border-radius:$values;
-moz-border-radius:$values;
border-radius:$values;
}
5.继承,如:sass写法
.block{
margin:10px 5px;
padding:2px;
}
p{
@extend .block;/*继承上面声明的.block*/
border:1px solid #fee;
}
6.颜色处理,
css预处理一般都会内置一些颜色处理函数用来对颜色值进行处理,如:加亮,变暗,颜色梯度等。
如sass的部分颜色处理函数:
lighten($color,10%);
darken($color,10%);
$color:#89234c
h1{
background:$color;
border:3px solid darken($color,50%)
}
7.函数等
jsonp的原理,应用场景,优缺点
在开发测试中,难免会在不同域下进行跨域操作,出于安全性考虑,浏览器中的同源策略阻止从一个域上加载的脚本获取或者操作另一个域下的文档属性,这时需要进行跨域的方式进行解决,如:使用jsonp ,iframe等
1.jsonp的原理
jsonp,即json+padding,动态创建script标签
,利用script标签的src属性可以获取任何域下的js脚本
,通过这个特性(也可以说漏洞),服务器端不在返回json格式,而是返回一段调用某个函数的js代码
,在src中进行了调用,这样实现了跨域.
2.应用场景
在网上经常看到别人的blog中在用jsonp模仿360和百度进行跨域拿数据,这两者就是典型的跨域请求案例.又比如在近期开发中前端部分用的是vue.js进行开发,所以跟后台进行交互的时候就可以通过跨域进行通信
,正好用的jsonp(折腾 一番之后,最终没有用这种方式,后面会说到),另外,qq空间大部分用的都是jsonp.
3.优缺点
jsonp优点:
完美解决在测试或者开发中获取不同域下的数据,用户传递一个callback参数给服务端
,然后服务端返回数据
时会将这个callback参数作为函数名来包裹住JSON数据,这样客户端就可以随意定制自己的函数来自动处理返回数据
了。简单来说数据的格式没有发生很大变化
jsonp缺点:
这里主要讲jsonp的缺点,也就是我上面说的没有用这个的原因
1.jsonp只支持get请求而不支持post请求
,也即是说如果想传给后台一个json格式的数据,此时问题就来了,浏览器会报一个http状态码415错误,告诉你请求格式不正确,这让我很蛋疼(在登录注册中需要给后台传一大串数据),如果都用参数的形式拼接在url后面的话不太现实,后台取值也会显得繁琐,2.在登录模块中
需要用到session来判断当前用户的登录状态
,这时候由于是跨域的原因
,前后台的取到的session是不一样的
,那么就不能就行session来判断.3.由于
jsonp存在安全性问题
(不知qq空间的跨域是怎么解决的,还是另有高招?)后来考虑到上面的一系列问题,采用的是后台进行设置允许跨域请求(但还是存在缺陷的,实质上还是跨域,如上面说的session问题).Header set Access-Control-Allow-Origin * 为了防止XSS攻击我们的服务器, 我们可以限制域,比如Access-Control-Allow-Origin
安全防范:
1.防止callback参数意外截断js代码,特殊字符单引号双引号,换行符存在风险.
2.防止callback参数
恶意添加script标签,造成xss漏洞
3.防止跨域
请求滥用,阻止非法站点恶意调用
详细请看:http://blog.csdn.net/jian_xi/article/details/66474717
JS变量提升
什么是变量提升
首先我们要知道js执行前有一个“预编译”过程,预编译主要有两个任务:
声明所有var变量
(初始为undefined)。解析定义式函数语句。
也就是说变量的提升实在js的预编译阶段完成
的。
变量提升的概念:函数和变量的声明会被js的解释器放到最上面。
在ES6之前,JavaScript没有块级作用域(一对花括号{}即为一个块级作用域),只有全局作用域和函数作用域。
变量提升
- 例子1
function fn1() {
a = 1;
console.log(a); //1
console.log(window.a); //undefined
var a = 5;
console.log(a); //5
}
fn1();
输出结果依次为:1 undefined 5,因为代码在解析的时候相当于
var a;//函数的声明会被解释器放到头部,预先声明,但没有赋值,所以在此刻a是undefined
a = 1;
console.log(a);
console.log(window.a); //此处打印的是全局变量的a,但是并没有声明全局变量a
a = 5;
console.log(a);
- 栗子2
function fn1() {
console.log(a); // a is not defined
a = 5;
console.log(a); // 如果不考虑报错,输出5
}
fn1();
变量提升一定是要有变量声明的过程,如var xx=5。像上面栗子中如果不声明直接对a赋值,那么a将变成一个全局对象。不存在变量提升。所以第一个console会报错。
- 栗子3
//全局作用域下
a = 5;
//全局作用域下
var a = 5;
都在全局作用域下执行两端代码有何区别?
a=1相当于window.a=5。为全局window对象添加了一个属性a值为5
var a=5相当于在全局作用域中声明了变量a,在整个作用域中都有效
后者比前者多了一个声明的行为
前者没有变量提升过程,提前访问会报错,后者有变量提升
- 栗子4
function fn2(){
console.log(a);//undefined
if(true){
var a = 10;
}
console.log(a);//10
}
这个栗子中,a变量的声明同样被提升了。所以说if是没有作用域的。只有函数有作用域
。(for循环等也是一样的)
函数的提升
js中我们定义函数有两种方式:
- 函数表达式 var fn = function fn(){}
- 函数声明方式,也叫定义式 function fn(){}
当函数是通过函数声明的方式定义时,就会有函数提升,特别注意的是:
- 变量提升中,变量赋值并没有被提升,只是声明被提升了。
但是,函数提升有点不一样,函数体也会一同被提升。
test();
function test(){
console.log(1); //1
}
所以会出现上面的情况,先执行test函数,然后声明,但是实际上函数确实被执行了,这就是与变量提升不同的点,函数提升不止声明提升,函数体也会一同被提升。
- 特别注意通过函数表达式创建的函数只会将声明提升,函数体不会提升
fn3(); //fn3 is not a function
var fn3 = function(){}
再看两个示例:1:
function bar() {
console.log('bar1')
}
var bar = function () {
console.log('bar2')
}
bar() //bar2
2:
var bar = function () {
console.log('bar2')
}
function bar() {
console.log('bar1')
}
bar() //bar2
结果全部输出 bar2,在预编译阶段,变量 bar 进行声明,但不赋值。函数bar进行创建并提升。运行阶段bar被赋值。
补充——es6中的变量提升
es6 新增块级作用域,let 和 const 声明的变量会产生块级作用域。并且let 和 const 声明的变量不会做变量提升
。所以就会出现暂时性死区。
foo(10) //Uncaught ReferenceError: Cannot access 'foo' before initialization
function foo(num) {
// 暂时性死区
console.log(foo)
// 暂时性死区
let foo = num
}
此时,浏览器报错foo is not defined
。这就是因为 let 和 const 声明的变量不会做变量提升
,所以变量的声明和赋值都是在console.log之后,所以访问一个不存在,没有声明的变量时,浏览器自然会报错。
ES6新增操作数组的方法
findIndex方法:传入一个回调函数,找到数组中符合当前搜索规则的第一个元素,返回它的下标,终止搜索。
idx = members.findIndex(item => item.id == callbackSelectedAccountMemberId)
//满足条件返回数组下标,不满足条件返回-1。
filter方法
let member = members.filter(item => item.id != e.target.dataset.id)
//遍历本数组中所有元素,将本数组中不满足条件的元素过滤掉,返回值是数组。
find方法:传入一个回调函数,找到数组中符合当前搜索规则的第一个元素,返回它,并且终止搜索。
let obj = datas.find(o => o.skuOpt == sku[0] && o.skuOptValue == sku[1]);
//遍历本数组中所有元素,满足条件返回元素本身。
reverse方法:使数组元素倒排
let arr = [1,4,2,5,9,1.5]
console.log(arr.reverse()) // [1.5, 9, 5, 2, 4, 1]
sort方法:数组元素正序,倒序排列
var array=[1,80,4,33,21,55];
array.sort(function (x,y) {
return y-x; // y-x倒序;x-y正序
});
console.log(array) //[80, 55, 33, 21, 4, 1]
indexOf方法:查询数组(对象)中的元素,匹配到返回下标,没有匹配到返回-1
let arr = [1,2,4.3.2]
console.log(arr.indexOf(5)) // -1
includes() 方法:用来判断一个数组是否包含一个指定的值,如果是返回 true,否则false。
[1, 2, 3].includes(2); // true
[1, 2, 3].includes(4); // false
http1.0 http1.1 http2.0特性及区别
#http1.0特性
- 无状态:服务器不跟踪不记录请求过的状态
- 无连接:浏览器每次请求都需要建立tcp连接
#无状态
对于无状态的特性可以借助cookie/session机制来做身份认证和状态记录
#无连接
无连接导致的性能缺陷有两种:
1. 无法复用连接
每次发送请求,都需要进行一次tcp连接(即3次握手4次挥手),使得网络的利用率非常低
2. 队头阻塞
http1.0规定在前一个请求响应到达之后下一个请求才能发送,如果前一个阻塞,后面的请求也给阻塞的
#http1.1特性
为了解决http1.0的性能缺陷,http1.1出现了
http1.1特性:
- 长连接:新增Connection字段,可以设置keep-alive值保持连接不断开
- 管道化:基于上面长连接的基础,管道化可以不等第一个请求响应继续发送后面的请求,但响应的顺序还是按照请求的顺序返回
- 缓存处理:新增字段cache-control
- 断点传输
#长连接
http1.1默认保持长连接,数据传输完成保持tcp连接不断开,继续用这个通道传输数据
#管道化
基于长连接的基础,我们先看没有管道化请求响应:
tcp没有断开,用的同一个通道
请求1 > 响应1 --> 请求2 > 响应2 --> 请求3 > 响应3
1
管道化的请求响应:
请求1 --> 请求2 --> 请求3 > 响应1 --> 响应2 --> 响应3
1
即使服务器先准备好响应2,也是按照请求顺序先返回响应1
虽然管道化,可以一次发送多个请求,但是响应仍是顺序返回,仍然无法解决队头阻塞的问题
#缓存处理
当浏览器请求资源时,先看是否有缓存的资源,如果有缓存,直接取,不会再发请求,如果没有缓存,则发送请求
通过设置字段cache-control来控制
#断点传输
在上传/下载资源时,如果资源过大,将其分割为多个部分,分别上传/下载,如果遇到网络故障,可以从已经上传/下载好的地方继续请求,不用从头开始,提高效率
在 Header 里两个参数实现的,客户端发请求时对应的是 Range 服务器端响应时对应的是 Content-Range
#http2.0特性
- 二进制分帧
- 多路复用: 在共享TCP链接的基础上同时发送请求和响应
- 头部压缩
- 服务器推送:服务器可以额外的向客户端推送资源,而无需客户端明确的请求
#二进制分帧
将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码
#多路复用
基于二进制分帧,在同一域名下所有访问都是从同一个tcp连接中走,http消息被分解为独立的帧,乱序发送,服务端根据标识符和首部将消息重新组装起来
#区别
- http1.0 到http1.1的主要区别,就是从无连接到长连接
- http2.0对比1.X版本主要区别就是多路复用
饿了么首页实现左右两栏联动。
实现目的两个:
1 手指点击左边菜单栏,右边食物栏会联动到菜单栏下面的内容。
2 手指滑动右边食物栏,左边菜单栏会随着右侧的滚动而相应出现active样式。
我自己用原生写了好几个,问题很多,有的卡顿,有的每次都从头开始走,很烦人。知道我引用了插件才算顺畅。
插件是better-scroll npm一下,import一下开始操作。
先实现目的1;
目的1 很简单
(1)在左侧目标li绑定click事件。触发函数move
(2)初始化两个better-scorll对象,一个food(右边的),一个menu(左边的),别忘记给他们设置click:true。
在move函数里执行 food.scrollToElement(le,time) 这个方法简直逆天:能food里的目标元素el在time毫秒内滚动到最顶部。el可以通过move(index)来获取;
此时点击右侧,左侧就可以联动了。
实现目的2 比实现目的1较复杂:
(1)定义一个数组,记录每个food中list的高度
(如果没有border情况下,clientHeight或者offsetHeight都可以,有border请用offsetHeight,但是style.height不可以,因为只有行间样式style才能点到)
真实的宽度在data里肯定获取不到,因为涉及到dom创建,所以在created钩子里的nextTick()回调函数里获取;(大家都知道created钩子执行时DOM 其实并未进行任何渲染,获取不到 offsetHeight)
mounted回调里是无法直接通过this.$refs获取到用ref命名的子组件的,只有nextTick才能访问到,我也尝试过在mounted里获取offsetHeight,没有获取到。()
scroll事件实时监听滚动位置,并且将位置赋给scrollY,scrollY发生变化引起了函数再次执行并返回index。
接下来就简单了,新index的变化引起绑定class属性的变化添加active类名的变化。
ES5 的继承和 ES6 的继承有什么区别 ?
ES5 的继承和 ES6 的继承有什么区别 ?
ES5 的继承时通过 prototype 或构造函数机制来实现。
- ES5 的继承实质上是
先创建子类的实例对象
,然后再将父类的方法添加到 this 上
(Parent.apply(this))。- ES6 的继承机制完全不同,实质上是
先创建父类的实例对象 this
(所以必须先调用父类的 super()方法),再用子类的构造函数修改
this具体的:ES6 通过 class 关键字定义类,里面有构造方法,
类之间通过 extends 关键字实现继承。
子类必须在constructor 方法中调用 super 方法
,否则新建实例报错。因为子类没有自己的 this 对象
,而是继承了父类的 this 对象
,然后对其进行加工。如果不调用 super 方法,子类得不到 this 对象。
ps:super 关键字指代父类的实例,即父类的 this 对象。在子类构造函数中,调用 super 后,才可使用 this 关键字
,否则报错。
正确判断空对象和空数组的方法
判断空对象
let obj={};
if(obj){
//会进来这里。因为这样判断永远为真哦 即使obj为空对象这样判断是不行的。
}
1.JSON.stringify() :将对象转化为json字符串,再判断该字符串是否为"{}"
var obj1 = {};
var obj2 = {
name:'xxx',
age:20
};
function test(obj) {
var flag = JSON.stringify(obj);
if(flag === '{}'){
return true;
}else {
return false;
}
}
console.log('obj1',test(obj1)); // obj1 true
console.log('obj2',test(obj2)); // obj2 false
2.for 循环判断
var obj = {};
var b = function(obj) {
for(var key in obj) {
return false;
}
return true;
}
console.log(b(obj));//true
3.jquery的isEmptyObject方法
此方法是jquery将2方法(for in)进行封装,使用时需要依赖jquery
var data = {};
var b = $.isEmptyObject(data);
console.log(b);//true
4.Object.getOwnPropertyNames()方法
此方法是使用Object对象的getOwnPropertyNames方法,获取到对象中的属性名
,存到一个数组中,返回数组对象
,我们可以通过判断数组的length来判断此对象是否为空
注意:此方法不兼容ie8,其余浏览器没有测试
var data = {};
var arr = Object.getOwnPropertyNames(data); //[]
alert(arr.length == 0);//true
5.使用ES6的Object.keys()方法
var data = {};
var arr = Object.keys(data);
alert(arr.length == 0);//true
判断空数组
1、利用数组的length属性来判断
if(arrayName.length > 0){
//数组不为空
}else{
//数组为空
}
2、利用先判断类型,再判断长度的方法来实现。这样增加了代码的安全性,因为不是Array类型的话是没有length属性的。
if(A && A.constructor==Array && A.length==0)
ES6中的Promise.all和Promise.race
一、Promise.all
这个方法返回一个新的promise对象
,该promise对象在所有的promise对象都成功的时候才会触发成功
,
一旦有任何一个
里面的promise对象失败则立即触发该promise对象的失败
。这个新的promise对象在触
发成功状态
以后,会把一个包含所有promise返回值的数组作为成功回调的返回值,顺序跟参数的顺序保持
一致;
如果这个新的promise对象触发了失败状态
,它会把第一个触发失败的promise对象的错误信息作
为它的失败错误信息
。Promise.all方法常被用于处理多个promise对象的状态集合
let p1 = new Promise((resolve, reject) => {
resolve('成功了1')
})
let p2 = new Promise((resolve, reject) => {
resolve('成功了2')
})
let p3 = Promse.reject('失败')
Promise.all([p1, p2])
.then((result) => {
console.log(result) //['成功了1', '成功了2']
})
.catch((error) => {
console.log(error)
})
Promise.all([p1,p3,p2])
.then((result) => {
console.log(result)
})
.catch((error) => {
console.log(error) // '失败'
})
Promise.race的使用
race和all用法类似。Promse.race方法顾名思义就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面
哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('1')
},1000)
})
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('2')
}, 500)
})
Promise.race([p1, p2])
.then((result) => {
console.log(result)
}).catch((error) => {
console.log(error) // 打印出2 因为他快
})
js判断空数组,空对象
判断对象是否为空对象
1.将json对象转化为json字符串,再判断该字符串是否为"{}"
var data = {};
var b = (JSON.stringify(data) == "{}");
console.log(b); //true
2.for in 循环判断
如果是空对象返回true, 否则返回 false 。
var obj = {};
var b = function() {
for(var key in obj) {
return false; //不为空
}
return true;
}
console.log(b()); //true
3.使用ES6的Object.keys()方法
var data = {};
var arr = Object.keys(data);
console.log(arr.length == 0); //true
判断数组是否为空数组
1.判断数组长度length是否为0
let arr = [];
if (arr.length == 0){
console.log("数组为空")
}else {
console.log("数组不为空")
}
2.通过data == false 来判断数组为空
在数组直接与布尔类型比较的时候,默认都是将数组和布尔类型都转化为Number类型比较,空数组转化为Number类型的时候为0
let data = [];
if(data == false){
//data为空数组时,要执行的代码
console.log('数组为空');
}
3.将数组转化为json字符串,再判断该字符串是否为"[]"
let obj = [];
console.log(JSON.stringify(obj) === '[]') //true
4.数组是Object类型,Object类型用等号比较的是内存地址,可以先判断是否为数组,再判断是否为空
var a = []
if(Object.prototype.toString.call(a) === '[object Array]' && a.length === 0){
console.log(13)
}
async/await的原理以及和promise的区别
一、async/await的基础使用
async/await是一套关于异步的解决方案
。下面是它的基本使用。
注意!!
但是使用async/await的时候,无法捕获错误,需要通过try/catch来捕获。
async返回promise对象,只有当async函数内部的异步操作执行完,才会执行then方法的回调函数。
//定义一个异步函数
getJSON(){
return 'JSON'
};
//在需要使用异步函数的函数前面加上async声明,声明这是一个异步函数
async testAsync(){
//在异步函数前面加上await,代表执行完await声明的函数,在往下执行
await getJSON();
.....剩下的代码
}
二、async/await的原理
1.generator函数
generator函数:generator(生成器)是ES6标准新引入的数据类型。一个generator看起来像一个函数,但是可以执行多次。
- 通过
next()执行,如果没有则不执行
。- 也可以
通过next修改
属性。yield只能在generator函数里
。- 遇到
return直接返回最后的值
,只能返回一个值。
yield表示暂停执行,next方法表示恢复执行。
function* Generator() {
//这几个函数是可以分为多次执行的 每次执行一个 返回一个值 从上往下以此执行
yield '11111111';
yield '22222222'
return '3333333';
}
let aaa = Generator();
let a = aaa.next() //{value: "11111111", done: false}
let b = aaa.next() // {value: "22222222", done: false}
let c = aaa.next() //{value: "3333333", done: true}
let d = aaa.next() //{value: undefined, done: true}
console.log(a,b,c,d)
2.async/await和Generator的关系
async函数就是generator的语法糖
,只不过generator需要自己写执行器
。async/await可以看成带自动启
动器的generator函数的语法糖
。
async/await只支持promise对象和原始值,不支持thunk函数。
- 将*号替换成async,将yield替换成await
- co库约定,
yield命令后面只能是thunck函数或promise对象,但是async函数的await命令只能是promise对象。
async函数可以看成多个异步操作
,包装成一个promise对象
,而await命令就是内部的then命令。
3.async/await和Promise的关系
async/await其实是基于Promise对象的。async函数其实是把Promise包装了一下,看起来更简洁了。
demo1:
function getJson(){
return new Promise((reslove,reject) => {
setTimeout(function(){
console.log(2)
reslove(2)
},2000)
})
}
async function testAsync() {
await getJson()
console.log(3)
}
testAsync() //2-->3
上面的代码真正解析:
function getJson(){
return new Promise((reslove,reject) => {
setTimeout(function(){
console.log(2)
reslove()
},2000)
})
}
//async返回的是promise对象,await也是,只不过把await后面的代码放在await返回的promise的.then后面。
function testAsync() {
return new Promise((reslove,reject) => {
getJson().then(function (res) {
console.log(3)
})
})
}
testAsync()
async返回的是promise对象,await也是,只不过把await后面的代码放在await返回的promise的.then后面。
三、async/await与promise的区别
- 函数前使用关键字async,await只能用在async标记的函数内。
- 比promise更简洁
- 处理结果上:
promise需要使用.then()来处理promise返回的结果
,async/await则直接在代码上顺序处理结果
。- 错误处理:promise如果在
then里出现异常
,只能用promise的catch函数来捕获
,外面的try/catch捕获不到。async/await可以同时捕获异步和同步代码抛出的异常。
- 中间值上
const makeRequest = () => {
return promise1()
.then(value1 => {
// do something
return promise2(value1)
.then(value2 => {
// do something
return promise3(value1, value2)
})
})
}
async/await获取value1:
const makeRequest = async () => {
const value1 = await promise1()
const value2 = await promise2(value1)
return promise3(value1, value2)
}
https://www.cnblogs.com/zheng...
http://www.javanx.cn/20180904...
★★★★web前端安全性
跨站脚本攻击(XSS攻击)
XSS(Cross Site Scripting),跨站脚本攻击。XSS是常见的Web攻击技术之一.所谓的跨站脚本攻击指得是:恶意攻击者往Web页面里注入恶意Script代码,用户浏览这些网页时,就会执行其中的恶意代码,可对用户进行盗取cookie信息、会话劫持等各种攻击.
解决方案:
- 输入过滤。永远不要相信用户的输入,对用户输入的数据做一定的过滤。如输入的数据是否符合预期的格式,比如日期格式,Email格式,电话号码格式等等。这样可以初步对XSS漏洞进行防御。上面的措施只在web端做了限制,攻击者通抓包工具如Fiddler还是可以绕过前端输入的限制,修改请求注入攻击脚本。因此,后台服务器需要在接收到用户输入的数据后,对特殊危险字符进行过滤或者转义处理,然后再存储到数据库中。
- 输出编码。服务器端输出到浏览器的数据,可以使用系统的安全函数来进行编码或转义来防范XSS攻击。在PHP中,有htmlentities()和htmlspecialchars()两个函数可以满足安全要求。相应的JavaScript的编码方式可以使用JavascriptEncode。
- 安全编码。开发需尽量避免Web客户端文档重写、重定向或其他敏感操作,同时要避免使用客户端数据,这些操作需尽量在服务器端使用动态页面来实现。
- HttpOnly Cookie。预防XSS攻击窃取用户cookie最有效的防御手段。Web应用程序在设置cookie时,将其属性设为HttpOnly,就可以避免该网页的cookie被客户端恶意JavaScript窃取,保护用户cookie信息。
- WAF(Web Application Firewall),Web应用防火墙,主要的功能是防范诸如网页木马、XSS以及CSRF等常见的Web漏洞攻击。由第三方公司开发,在企业环境中深受欢迎。
跨站请求伪造(CSRF攻击)
CSRF(Cross Site Request Forgery),即跨站请求伪造,是一种常见的Web攻击,但很多开发者对它很陌生。CSRF也是Web安全中最容易被忽略的一种 网站攻击。CSRF攻击的原理:CSRF攻击过程的受害者用户登录网站A,输入个人信息,在本地保存服务器生成的cookie。然后在A网站点击由攻击者构建一条恶意链接跳转到B网站,然后B网站携带着的用户cookie信息去访问B网站.让A网站造成是用户自己访问的假相,从而来进行一些列的操作,常见的就是转账.
解决方案:
- 验证码。应用程序和用户进行交互过程中,特别是账户交易这种核心步骤,强制用户输入验证码,才能完成最终请求。在通常情况下,验证码够很好地遏制CSRF攻击。但增加验证码降低了用户的体验,网站不能给所有的操作都加上验证码。所以只能将验证码作为一种辅助手段,在关键业务点设置验证码。
- Referer Check。HTTP Referer是header的一部分,当浏览器向web服务器发送请求时,一般会带上Referer信息告诉服务器是从哪个页面链接过来的,服务器籍此可以获得一些信息用于处理。可以通过检查请求的来源来防御CSRF攻击。正常请求的referer具有一定规律,如在提交表单的referer必定是在该页面发起的请求。所以通过检查http包头referer的值是不是这个页面,来判断是不是CSRF攻击。但在某些情况下如从https跳转到http,浏览器处于安全考虑,不会发送referer,服务器就无法进行check了。若与该网站同域的其他网站有XSS漏洞,那么攻击者可以在其他网站注入恶意脚本,受害者进入了此类同域的网址,也会遭受攻击。出于以上原因,无法完全依赖Referer Check作为防御CSRF的主要手段。但是可以通过Referer Check来监控CSRF攻击的发生。
- Anti CSRF Token。目前比较完善的解决方案是加入Anti-CSRF-Token,即发送请求时在HTTP 请求中以参数的形式加入一个随机产生的token,并在服务器建立一个拦截器来验证这个token。服务器读取浏览器当前域cookie中这个token值,会进行校验该请求当中的token和cookie当中的token值是否都存在且相等,才认为这是合法的请求。否则认为这次请求是违法的,拒绝该次服务。这种方法相比Referer检查要安全很多,token可以在用户登陆后产生并放于session或cookie中,然后在每次请求时服务器把token从session或cookie中拿出,与本次请求中的token 进行比对。由于token的存在,攻击者无法再构造出一个完整的URL实施CSRF攻击。但在处理多个页面共存问题时,当某个页面消耗掉token后,其他页面的表单保存的还是被消耗掉的那个token,其他页面的表单提交时会出现token错误。
SQL注入攻击
SQL注入(SQL Injection),应用程序在向后台数据库传递SQL(Structured Query Language,结构化查询语言)时,攻击者将SQL命令插入到Web表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令.
解决方案:
- 防止系统敏感信息泄露。设置php.ini选项display_errors=off,防止php脚本出错之后,在web页面输出敏感信息错误,让攻击者有机可乘。
- 数据转义。设置php.ini选项magic_quotes_gpc=on,它会将提交的变量中所有的’(单引号),”(双引号),\(反斜杠),空白字符等都在前面自动加上\。或者采用mysql_real_escape()函数或addslashes()函数进行输入参数的转义。
- 增加黑名单或者白名单验证。白名单验证一般指,检查用户输入是否是符合预期的类型、长度、数值范围或者其他格式标准。黑名单验证是指,若在用户输入中,包含明显的恶意内容则拒绝该条用户请求。在使用白名单验证时,一般会配合黑名单验证。
文件上传漏洞
上传漏洞在DVBBS6.0时代被黑客们利用的最为猖獗,利用上传漏洞可以直接得到WEBSHELL,危害等级超级高,现在的入侵中上传漏洞也是常见的漏洞。该漏洞允许用户上传任意文件可能会让攻击者注入危险内容或恶意代码,并在服务器上运行。 文件上传漏洞的原理:由于文件上传功能实现代码没有严格限制用户上传的文件后缀以及文件类型,导致允许攻击者向某个可通过 Web 访问的目录上传任意PHP文件,并能够将这些文件传递给 PHP 解释器,就可以在远程服务器上执行任意PHP脚本。
解决方案:
- 检查服务器是否判断了上传文件类型及后缀。
- 定义上传文件类型白名单,即只允许白名单里面类型的文件上传。
- 文件上传目录禁止执行脚本解析,避免攻击者进行二次攻击。 Info漏洞 Info漏洞就是CGI把输入的参数原样输出到页面,攻击者通过修改输入参数而达到欺骗用户的目的。
转载于:https://www.cnblogs.com/lmjZo...
什么是BFC?看这一篇就够了
BFC 定义
BFC(Block formatting context)直译为"块级格式化上下文"。它是一个独立的渲染区域,只有Block-level box参与, 它规定了内部的Block-level Box如何布局,并且与这个区域外部毫不相干。
在解释什么是BFC之前,我们需要先知道Box、Formatting Context的概念。
Box:css布局的基本单位
Box 是 CSS 布局的对象和基本单位, 直观点来说,就是一个页面是由很多个 Box 组成的。元素的类型和 display 属性,决定了这个 Box 的类型。 不同类型的 Box, 会参与不同的 Formatting Context(一个决定如何渲染文档的容器),因此Box内的元素会以不同的方式渲染。让我们看看有哪些盒子:
- block-level box:display 属性为 block, list-item, table 的元素,会生成 block-level box。并且参与 block fomatting context;
- inline-level box:display 属性为 inline, inline-block, inline-table 的元素,会生成 inline-level box。并且参与 inline formatting context;
- run-in box: css3 中才有, 这儿先不讲了。
Formatting Context
Formatting context 是 W3C CSS2.1 规范中的一个概念。它是页面中的一块渲染区域,并且有一套渲染规则,它决定了其子元素将如何定位,以及和其他元素的关系和相互作用。最常见的 Formatting context 有 Block fomatting context (简称BFC)和 Inline formatting context (简称IFC)。
BFC是一个独立的布局环境
,其中的元素布局是不受外界的影响,
并且在一个BFC中,块盒与行盒(行盒由一行中所有的内联元素所组成)
都会垂直的沿着其父元素的边框排列。
BFC的布局规则
内部的Box会在垂直方向,一个接一个地放置。
- Box
垂直方向的距离由margin决定
。属于同一个BFC的两个相邻Box的margin会发生重叠。
- 每个盒子(块盒与行盒)的
margin box的左边
,与包含块border box的左边相接触
(对于从左往右的格式化,否则相反)。即使存在浮动也是如此。- BFC的区域
不会与float box重叠。
- BFC就是页面上的一个隔离的独立容器,容器里面的
子元素不会影响到外面的元素
。反之也如此。- 计
算BFC的高度时,浮动元素也参与计算。
如何创建BFC
- 1、float的值不是none。
- 2、position的值不是static或者relative。
- 3、display的值是inline-block、table-cell、flex、table-caption或者inline-flex
- 4、overflow的值不是visible
BFC的作用
1.利用BFC避免margin重叠。
一起来看一个例子:
<!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>防止margin重叠</title>
</head>
<style>
*{
margin: 0;
padding: 0;
}
p {
color: #f55;
background: yellow;
width: 200px;
line-height: 100px;
text-align:center;
margin: 30px;
}
</style>
<body>
<p>看看我的 margin是多少</p>
<p>看看我的 margin是多少</p>
</body>
</html>
页面生成的效果就是这样的:
根据第二条,属于同一个BFC的两个相邻的Box会发生margin重叠,所以我们可以设置,两个不同的BFC,也就是我们可以让把第二个p用div包起来,然后激活它使其成为一个BFC
<!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>防止margin重叠</title>
</head>
<style>
*{
margin: 0;
padding: 0;
}
p {
color: #f55;
background: yellow;
width: 200px;
line-height: 100px;
text-align:center;
margin: 30px;
}
div{
overflow: hidden;
}
</style>
<body>
<p>看看我的 margin是多少</p>
<div>
<p>看看我的 margin是多少</p>
</div>
</body>
</html>
2.自适应两栏布局
根据:每个盒子的margin box的左边,与包含块border box的左边相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此。
<!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>Document</title>
</head>
<style>
*{
margin: 0;
padding: 0;
}
body {
width: 100%;
position: relative;
}
.left {
width: 100px;
height: 150px;
float: left;
background: rgb(139, 214, 78);
text-align: center;
line-height: 150px;
font-size: 20px;
}
.right {
height: 300px;
background: rgb(170, 54, 236);
text-align: center;
line-height: 300px;
font-size: 40px;
}
</style>
<body>
<div class="left">LEFT</div>
<div class="right">RIGHT</div>
</body>
</html>
页面:
又因为:BFC的区域不会与float box重叠。
所以我们让right单独成为一个BFC
<!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>Document</title>
</head>
<style>
*{
margin: 0;
padding: 0;
}
body {
width: 100%;
position: relative;
}
.left {
width: 100px;
height: 150px;
float: left;
background: rgb(139, 214, 78);
text-align: center;
line-height: 150px;
font-size: 20px;
}
.right {
overflow: hidden;
height: 300px;
background: rgb(170, 54, 236);
text-align: center;
line-height: 300px;
font-size: 40px;
}
</style>
<body>
<div class="left">LEFT</div>
<div class="right">RIGHT</div>
</body>
</html>
页面:
right会自动的适应宽度,这时候就形成了一个两栏自适应的布局。
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>清除浮动</title>
</head>
<style>
.par {
border: 5px solid rgb(91, 243, 30);
width: 300px;
}
.child {
border: 5px solid rgb(233, 250, 84);
width:100px;
height: 100px;
float: left;
}
</style>
<body>
<div class="par">
<div class="child"></div>
<div class="child"></div>
</div>
</body>
</html>
页面:
这个时候我们根据最后一条:计算BFC的高度时,浮动元素也参与计算。
给父节点激活BFC
<!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>
<style>
.par {
border: 5px solid rgb(91, 243, 30);
width: 300px;
overflow: hidden;
}
.child {
border: 5px solid rgb(233, 250, 84);
width:100px;
height: 100px;
float: left;
}
</style>
<body>
<div class="par">
<div class="child"></div>
<div class="child"></div>
</div>
</body>
</html>
页面:
总结
以上例子都体现了:
BFC就是页面上的一个隔离的独立容器
,容器里面的子元素不会影响到外面的元素
。反之也如此。
因为BFC内部的元素和外部的元素绝对不会互相影响
,因此, 当BFC外部存在浮动时
,它不应该影响BFC
内部Box的布局
,BFC会通过变窄,而不与浮动有重叠。同样的,当BFC内部有浮动
时,为了不影响外部元
素的布局,BFC计算高度时会包括浮动的高度
。避免margin重叠也是这样的一个道理。
JS 箭头函数与普通函数的区别
区别
- 箭头函数体内的
this对象
,就是定义时所在的对象
,而不是使用时所在的对象
。- 箭头函数
没有arguments,
如果要用,可以用 rest 参数代替 (注意在node环境下是有arguments的)- 箭头函数
不能作为构造函数
,不能使用new- 箭头函数
没有原型,不能继承
- 箭头函数
不能当做Generator函数
,不能使用yield关键字
验证示例
- 浏览器侧验证
<script>
const test1=(...numbers)=>{
console.log(this)
console.log(numbers)
console.log(arguments)
};
const test2=function(){
console.log(this)
console.log(arguments)
}
test1(123);//分别输出 window [123] 报错
test2(123);//分别输出 window Arguments
</script>
- node侧验证
const test1=(...numbers)=>{
console.log(this)
console.log(numbers)
console.log(arguments)
};
const test2=function(){
console.log(this)
console.log(arguments)
}
test1(123);//分别输出 global [123] Arguments
test2(123);//分别输出 global Arguments
深入理解css3中的flex-grow、flex-shrink、flex-basis
flex-grow 扩张
所以这里flex-grow的意思已经很明显了,就是索取父容器的剩余空间,默认值是0,
就是三个子容器都
不索取
剩余空间。但是当B1设置为1的时候
,剩余空间就会被分成一份,然后都给了B1。
如果此时
B2设置了flex-grow:2
,那么说明B2也参与到瓜分
剩余空间中来,并且他是占据了剩余空间中的2
份
,那么此时父容器就会把剩余空间分为3份,然后1份给到B1,2份给到B2
,如下面这样子。
flex-basis 和width功能似
flex-basis (default:auto) 初次见flex-basis这个属性,还挺疑惑的,不知道它是用来干嘛的。 后来研究发发现,这个属性值的作用也就是width的替代品。 如果子容器设置了flex-basis或者width
,那么在分配空间之前
,他们会先跟父容器预约这么多的空间,然后剩下的才是归入到剩余空间
,然后父容器再把剩余空间分配给设置了flex-grow
的容器。 如果同时设置flex-basis和width,那么width属性会被覆盖
,也就是说flex-basis的优先级比width高
。有一点需要注意,如果flex-basis和width其中有一个是auto,那么另外一个非auto的属性优先级会更高。
tips:flex-basis和width为auto值,那最后的空间就是根据内容多少来定的,内容多占据的水平空间就多。
flex-shrink 压缩
flex-shrink (default:1) 好了,上面讲了这么多,你们应该都明白了把。 有人会想,不就这样嘛,很容易啊,不就是剩余空间的分配吗?是的,,上面讲的都是剩余空间的分配。但是,你有没有想过还有没有其他的情况呢?可以发现,上面讲的例子B1 B2 B3的宽度总和都是没有超过父容器的宽度的。 那如果三个子容器的宽度和超过父容器的宽度呢?那还有剩余空间可以分配吗,此时浏览器又是怎么处理呢?请看下面:
tips:flex环境默认是不换行的,即使父容器宽度不够也不会,除非设置flex-wrap来换行
此时我们会发现,B1设置的flex-grow没有作用,不但没有获取到剩余空间,他的空间甚至是比他定义的300px还要小,而且我们发现B2和B3的空间也相应的被压缩了。 那么这里的问题就是:1、为什么flex-grow没有作用,反而被压缩呢?2、三个容器的压缩比例是这样的呢?
同样的,三个容器处于flex环境中,所以布局之前,父容器还是会计算剩余空间。 这一次计算的结果是这样的:剩余空间=500px - 300px - 160px - 120px = -80px,剩余空间是一个负数所以很容易理解第一个问题,即使是设置了flex-grow,但是由于没有剩余空间,所以B1分配到的空间是0。
由于flex环境的父容器的宽度500px是不会变,所以为了是子容器的宽度和最多为父容器的宽度,那就只有两个办法:第一个是使子容器换行,第二个是压缩子容器使之刚好撑满父容器的宽度。 因为flex子容器是默认不换行的,所以这里不做讨论。而第二种压缩,实际上就是上面例子表现出来的样式。现在就遇到了上面第二个问题,这三个的压缩比例是多少呢,各自需要压缩的空间是多少呢?
这个时候就需要谈谈flex-shrink,这个属性其实就是定义一个子容器的压缩比例。他的默认值是1
,所以上面那个例子,就是三个子容器压缩的比例是一样的 1:1:1。 如果此时我们设置B1的压缩比例是2,那会怎样呢?
我们可以发现,B1被压缩的更多了。而B2和B3得到了跟多的空间。那我们怎么得出他们各自的压缩率呢?我们假设B2 B3的压缩率是X1,那么B1的压缩率就是X2了,那就有了如下方程:
X2 = 2 * X1; ---->(500px-580px)/500px/4--->-80px/500px/4--->4%
500 = 300 * X2 + 160 * X1 + 120 * X1;
通过上面我们就可以解出X1和X2等于多少了,这样就可以计算出压缩率和被压缩了多少空间了。
总结 通过上面的分析,我们就可以得出这样几个结论:
1、剩余空间=父容器空间-子容器1.flex-basis/width - 子容器2.flex-basis/width - …
2、如果父容器空间不够,就走压缩flex-shrink
,否则走扩张flex-grow;
3、如果你不希望某个容器在任何时候被压缩
,那设置flex-shrink:0;
4、如果子容器的的flex-basis设置为0(width也可以,不过flex-basis更符合语义),那么计算剩余空间的时候将不会为子容器预留空间。
5、如果子容器的的flex-basis设置为auto(width也可以,不过flex-basis更符合语义),那么计算剩余空间的时候将会根据子容器内容的多少来预留空间。
★★★★★ES6 类(Class)基本用法和静态属性+方法详解
原文地址:http://blog.csdn.net/pcaxb/ar...
ES6 类(Class)基本用法和静态属性+方法详解JavaScript语言的传统方法是通过构造函数,定义并生成新对象,prototype 属性使您有能力向对象添加属性和方法。下面是通过传统的方式创建和使用对象的案例:
function Person(x,y){
this.x = x;
this.y = y;
}
Person.prototype.toString = function (){
return (this.x + "的年龄是" +this.y+"岁");
}
export {Person};
//index.js
import {Person} from './Person';
let person = new Person('张三',12);
1.Class的基本用法
ES6引入了Class(类)这个概念,作为对象的模板,通过class关键字,可以定义类。基本上,ES6的class可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。上面的代码用ES6的“类”改写,就是下面这样。
class Person{
// 构造
constructor(x,y){
this.x = x;
this.y = y;
}
toString(){
return (this.x + "的年龄是" +this.y+"岁");
}
}
export {Person};
//index.js
import {Person} from './Person';
let person = new Person('张三',12);
console.log(person.toString());
上面代码定义了一个“类”,可以看到里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。也就是说,ES5的构造函数Person,对应ES6的Person类的构造方法。
Person类除了构造方法,还定义了一个toString方法。注意,定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。
ES6的类,完全可以看作构造函数的另一种写法。
console.log(typeof Person);//class类的的类型也是function
console.log(Person === Person.prototype.constructor);//true
上面代码表明,类的数据类型就是函数,类本身就指向构造函数。
console.log(Person.prototype);//输出的是一个对象
构造函数的prototype属性,在ES6的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面
,通过以下方式可是覆盖类中的方法,当然定义类的时候也可以通过这个方式添加方法。
Person.prototype = {
getName(){
return '张三';
},
getAge(){
return '12';
}
};
在类的实例上面调用方法,其实就是调用原型上的方法
console.log(person.constructor === Person.prototype.constructor);//true
Object.assign方法可以给对象Person动态的增加方法
,而Person.prototype = {}是覆盖对象的方法,或者在初始化的时候添加方法。
Object.assign(Person.prototype,{
getWidth(){
console.log('12');
},
getHeight(){
console.log('24');
}
});
console.log(Person.prototype);
toString方法是Person类内部定义的方法,ES6中它是不可枚举的,这一点与ES5的行为不一致,ES5是可以枚举的
。
//ES5
console.log(Object.keys(Person.prototype));//["toString", "getWidth", "getHeight"]
console.log(Object.getOwnPropertyNames(Person.prototype));//["constructor", "toString", "getWidth", "getHeight"]
//ES6
console.log(Object.keys(Person.prototype));//["getWidth", "getHeight"]
console.log(Object.getOwnPropertyNames(Person.prototype));//["constructor", "toString", "getWidth", "getHeight"]
Object.keys(obj),返回一个数组,数组里是该obj可被枚举的所有属性
。Object.getOwnPropertyNames(obj),返回一个数组,数组里是该obj上所有的实例属性。
在ES6中,类的属性名可以使用表达式
,具体实现方式如下
let methodName = "getTitle";
export default class Article{
[methodName](){
console.log('输出文章的标题1');
}
}
//index.js
import Article from './Article';
//console.log(Article.prototype);
let article = new Article();
article.getTitle()
constructor方法是类的构造函数是默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个默认的constructor方法会被添加。所以即使你没有添加构造函数,也是有默认的构造函数的。一般constructor方法默认返回实例对象this
,但是也可以指定constructor方法返回一个全新的对象,让返回的实例对象不是该类的实例。
import Article from './Article';
export default class ConstructorStu{
// 构造
constructor() {
console.log('constructor');
return new Article();
}
}
//index.js
import ConstructorStu from './ConstructorStu';
console.log('==111==');
console.log(new ConstructorStu() instanceof ConstructorStu);//false
console.log('==222==');
let cons = new ConstructorStu();
console.log('==333==');
cons.constructor();
console.log('==444==');
运行结果
==111==
constructor
false
==222==
construct=or
==333==
==444==
说明:类的构造函数,不使用new是没法调用的,即使你使用实例对象去调用也是不行的,这是它跟普通构造函数的一个主要区别。
实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。hasOwnProperty( )函数用于指示一个对象自身(不包括原型链)是否具有指定名称的属性。如果有,返回true,否则返回false。
<span style="font-size:18px;">Person.js</span>
class Person{
// 构造
constructor(x,y){
this.x = x;
this.y = y;
}
toString(){
return (this.x + "的年龄是" +this.y+"岁");
}
}
let person = new Person('lis',8);
console.log(person.toString());
console.log(person.hasOwnProperty('x'));//true
console.log(person.hasOwnProperty('y'));//true
console.log(person.hasOwnProperty('toString'));//false
console.log(person.__proto__.hasOwnProperty('toString'));//true
说明:上面结果说明对象上有x,y属性,但是没有toString属性。也就是说x,y是定义在this对象上,toString定义在类上。
<span style="font-size:18px;">let person1 = new Person('张三',12);
let person2 = new Person('李四',13);
console.log(person1.__proto__ === person2.__proto__);//true</span>
类的所有实例共享一个原型对象,person1和person2都是Person的实例,它们的原型都是Person.prototype,所以__proto__属性是相等的。这也意味着,可以通过实例的__proto__属性为Class添加方法。
<span style="font-size:18px;">let person1 = new Person('张三',12);
let person2 = new Person('李四',13);
person1.__proto__.getH = function (){
return "Height";
};
console.log(person1.getH());
console.log(person2.getH());</span>
上面代码在person1的原型上添加了一个getH方法,由于person1的原型就是person2的原型,因此person2也可以调用这个方法。而且,此后新建的实例person3也可以调用这个方法。这意味着,使用实例的__proto__属性改写原型,必须相当谨慎,不推荐使用,因为这会改变Class的原始定义,影响到所有实例。
__proto__参考资料:点击打开链接
class不存在变量提升,需要先定义再使用,因为ES6不会把类的声明提升到代码头部,但是ES5就不一样,ES5存在变量提升,可以先使用,然后再定义。
<span style="font-size:18px;">//正确
new A();
function A(){
}//ES5可以先使用再定义,存在变量提升
//错误
new B();
class B{
}//B is not a constructor
//ES6不能先使用再定义,不存在变量提升</span>
这个类的名字是Expression而不是Expre,Expre只在Class的内部代码可用,指代当前类。
<span style="font-size:18px;">//Expression.js
const Expression = class Expre{
static getAge(){
return '12';
}
getClassName(){
return " ClassName1= " +Expre.name + " ClassName2= " +Expression.name;
}
};
let exp = new Expression();
//let exp = new Expre();错误
//bundle.js:7935 Uncaught ReferenceError: Expre is not defined
console.log(exp.getClassName());//ClassName1= Expre ClassName2= Expre
//console.log(Expre.getAge());错误
//bundle.js:7935 Uncaught ReferenceError: Expre is not defined
console.log(Expression.getAge());</span>
说明:Expre.name和Expression.name返回的都是Expre,返回的都是当前。
如果类的内部没用到的话,可以省略Expre,也就是可以写成下面的形式
<span style="font-size:18px;">//Expression.js
const MyExpre = class{
getClassName(){
return MyExpre.name;
}
};
let myExpre = new MyExpre();
console.log(myExpre.getClassName());//MyExpre</span>
说明:如果省略了class后面的那个名字Expre,MyExpre.name返回的就是MyExpre,如果没有省略MyExpre.name返回就是class后面的那个名字Expre。
采用Class表达式,可以写出立即执行的Class
<span style="font-size:18px;">//Expression.js
let person = new class{
// 构造
constructor(props) {
this.props = props;
}
getProps(){
return this.props;
}
}('构造函数的参数');
console.log(person.getProps());//构造函数的参数</span>
私有方法是常见需求,但ES6不提供,只能通过变通方法模拟实现。一种做法是在命名上加以区别,在方法前面加上_(下划线),表示这是一个只限于内部使用的私有方法。但是,这种命名是不保险的,在类的外部,还是可以调用到这个方法。另一种方法就是索性将私有方法移出模块,因为模块内部的所有方法都是对外可见的。
<span style="font-size:18px;">//PrivateMethod.js
export default class PrivateMethod{
// 构造
constructor() {
}
getName(){
priName();
}
}
function priName(){
console.log('私有方法测试');
}
//index.js
import PriMe from './PrivateMethod';
let prime = new PriMe();
prime.getName();</span>
说明:通过这种方式还可以定义私有属性,同理。还有一种方法是利用Symbol值的唯一性,将私有方法的名字命名为一个Symbol值,不过这种方式稍微麻烦点。
类的方法内部如果含有this,它默认指向类的实例。getName方法中的this,默认指向ThisStu类的实例。但是,如果将这个方法提取出来单独使用,this会指向该方法运行时所在的环境,因为找不到name方法而导致报错。
<span style="font-size:18px;">//ThisStu.js
class ThisStu{
getName(){
return this.name();
}
name(){
return '王五';
}
}
export {ThisStu};
//index.js
import {ThisStu} from './ThisStu';
let thisStu = new ThisStu();
console.log(thisStu.getName());
const {getName} = thisStu;
getName();
//Cannot read property 'name' of undefined</span>
一个比较简单的解决方法是,在构造方法中绑定this,这样就不会找不到name方法了。修改ThisStu.js文件如下:
<span style="font-size:18px;">//ThisStu.js
class ThisStu{
// 构造
constructor() {
this.getName = this.getName.bind(this);
}
getName(){
return this.name();
}
name(){
return '王五';
}
}
export {ThisStu};</span>
使用构造函数,直接给当前实例的getName赋值,修改修改ThisStu.js文件的构造函数如下:
<span style="font-size:18px;">// 构造
constructor() {
//this.getName = this.getName.bind(this);
this.getName = ()=>{
return this.name();
}
}
</span>
2.Class的静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
<span style="font-size:18px;">//StaticMethod.js
//定义静态方法
static getAge(){
return '获取Age的静态方法';
}
//通过类名直接调用
console.log(StaticMethod.getAge());</span>
如果StaticMethod继承StaticMethodParent,StaticMethodParent的静态方法,可以被StaticMethod继承。
<span style="font-size:18px;">//StaticMethodParent.js
export default class StaticMethodParent{
static getCommon(){
return '父类的静态方法';
}
}
//通过子类直接调用
console.log(StaticMethod.getCommon());
如果StaticMethod继承StaticMethodParent,StaticMethodParent的静态方法,可以在StaticMethod中用super调用。
//定义静态方法
static getAge(){
//子类可以调用父类的静态方法
console.log(super.getCommon());
return '获取Age的静态方法';
}</span>
说明:静态方法只能在静态方法中调用,不能再实例方法中调用。
3.Class静态属性和实例属性
静态属性指的是Class本身的属性,即Class.propname,而不是定义在实例对象(this)上的属性。ES6使用静态属性和实例属性:
<span style="font-size:18px;">//StaticMethod.js
//定义静态属性
StaticMethod.firstName = 'pca';
console.log(StaticMethod.firstName);
//定义实例属性
//ES6实例属性只能在constructor构造函数中定义
constructor() {
super();
this.width = '40cm';
}
getWidth(){
return this.width;//使用的时候需要加上this
}
//为了可读性的目的,对于那些在constructor里面已经定义的实例属性,新写法允许直接列出。
width;</span>
说明:目前ES6,只有这种写法可行,因为ES6明确规定,Class内部只有静态方法,没有静态属性。
**
**ES7有一个静态属性的提案,目前Babel转码器支持。安装babel-preset-stage-0 包含了0-3的stage,可根据需要添加,不同的stage封装了不同的插件,官方推荐是使用stage-1
安装命令(根据自己的需求调整):
<span style="font-size:18px;">npm install --save babel-preset-stage-0</span>
ES7使用静态属性和实例属性:
<span style="font-size:18px;">//StaticMethod.js
//ES7提案 定义静态属性
static lastName = 'pcaca';
//ES7定义实例属性
height = '150cm';</span>
说明:ES7和ES6的静态属性和实例属性只是定义不一样,调用的方式是一样的
Class的静态方法/Class静态属性和实例属性的整个案例:
<span style="font-size:18px;">//StaticMethodParent.js
export default class StaticMethodParent{
static getCommon(){
return '父类的静态方法';
}
}
//StaticMethod.js
import StaticMethodParent from './StaticMethodParent'
//定义静态属性和静态方法
class StaticMethod extends StaticMethodParent{
//因为ES6明确规定,Class内部只有静态方法,没有静态属性,所以ES6在类中定义静态属性都是错误的。
//static lastName = 'pcaca';ES6错误
//ES7提案 定义静态属性
//安装babel-preset-stage-0 包含了0-3的stage,可根据需要添加,
//不同的stage封装了不同的插件,官方推荐是使用stage-1
static lastName = 'pcaca';
//ES7定义实例属性
height = '150cm';
getHeight(){
return this.height;//ES7的使用也要加上this
}
//ES6实例属性只能在constructor构造函数中定义
constructor() {
super();
this.width = '40cm';
}
//为了可读性的目的,对于那些在constructor里面已经定义的实例属性,新写法允许直接列出。
width;
getWidth(){
return this.width;//使用的时候需要加上this
}
//定义静态方法
static getAge(){
//子类可以调用父类的静态方法
console.log(super.getCommon());
return '获取Age的静态方法';
}
};
//定义静态属性
StaticMethod.firstName = 'pca';
export {StaticMethod};
//index.js
import {StaticMethod} from './StaticMethod';
console.log(StaticMethod.getAge());
console.log(StaticMethod.getCommon());
console.log(StaticMethod.firstName);
console.log(StaticMethod.lastName);
let staticMethod = new StaticMethod();
console.log(staticMethod.height);
console.log(staticMethod.getHeight());
console.log(staticMethod.width);
console.log(staticMethod.getWidth());
//staticMethod.getAge();//bundle.js:7906 Uncaught TypeError: staticMethod.getAge is not a function</span>
Vue传值——bus总线机制
众所周知,vue提供了很多套组件间传值的方法,父子组件直接用props和$emit就好,大型项目则用vuex,但有一种更适合在小项目中使用的非父子组件传值方法,即bus总线机制。它的用法的实现原理是前端面试中常考的。在第一次换工作的时候,几乎所有面试官都问了有关vue组件传值的几种方法,所以掌握这个知识点是很重要的。
这种方法的原理是通过一个空的Vue实例作为中央事件总线,通过它来触发事件和监听事件,可以实现几乎任何组件间的通信,这在一些比较小的项目中是非常好用的,当然如果是简单的小项目,你也可以使用$root(但我坚决不支持这样使用,可以在面试时炫技)。接下来我们来看看在项目中的实战:
在utils文件夹下面定义一个bus.js,这样一个中央事件总线就定好了,里面内容就这两行:
import Vue from 'vue'
export const Event = new Vue() // 定义一个事件总线
传递方:
import { Event } from '../../utils/bus' // 引入这个事件总线
export default {
data() {
userId: ''
}
watch: {
userId() { // 监听userId改变时触发事件,当然你可以在所有其他事件去触发$emit
Event.$emit('changeUser', userId) // $emit接收参数,一个是触发的事件名,一个是携带的参数
}
}
接收方:
import { Event } from '../../utils/bus' // 依然是把bus总线引进来
export default {
created() {
Event.$on('changeSonRoute',showIndex => { // $on接收发送方传给我们的数据
console.log(showIndex)
})
}
}
这个方法听起来挺唬人的,但实际操作就是这么简单。
js中var、let、const的区别详解
不知道是不是有小白跟我一样习惯的所有定义变量的时候都用var… 知道真相的我眼泪掉下来系列。接下来我就将这三个详细通过代码来描述一下。
1、var 定义的变量挂载在window上
通过var定义的变量是挂在windos上的,而let与const并不会。
var a = 1;
let b = 2;
const c = 3;
console.log(a,window.a); // 1 1
console.log(b,window.b); // 2 undefined
console.log(c,window.c); // 3 undefined
打印出来:
2、var a = 1 与 a = 1 的区别
首先他们俩都是挂载在window上的但是不同的是:
var a = 1;是挂在window上的变量,而 a = 1; 是挂载在window上的属性
因此导致 a = 1 可以用delete删除,而var a = 1 不能用delete删除。
var a = 1;
b = 2;
console.log("deleteBeforeA",a,window.a); // deleteBeforeA 1 1
console.log("deleteBeforeB",b,window.b); // deleteBeforeB 2 2
console.log("deleteA",delete a); // deleteA false
console.log("deleteB",delete b); // deleteB true
console.log("deleteBefore",a,window.a); // deleteBefore 1 1
console.log("deleteBefore",b,window.b); // Uncaught ReferenceError: b is not defined
3、var 定义的变量存在变量提升
通过var定义的变量存在变量提升,而let与const并不会。
console.log(a); // undefined
console.log(b); // 报错 Cannot access 'b' before initialization
console.log(c);
var a = 1;
let b = 2;
const c = 3;
1234567
4、let和const声明形成块作用域
if(1){
var a = 1;
let b = 2; // b 的作用域范围处于{}里
}
console.log(a); // 1
console.log(b); // 报错 b is not defined
123456
if(1){
var a = 1;
const b = 2; // b 的作用域范围处于{}里
}
console.log(a); // 1
console.log(b); // 报错 b is not defined
5、var 可重复声明,let与const不可以
var a = 1;
var a = 2;
console.log(a); // 2
let b = 1;
let b = 2;
console.log(b); // 报错 Identifier 'b' has already been declared
const c = 1;
const c = 2;
console.log(c); // 报错 Identifier 'c' has already been declared
6、var、let 可以重复赋值,const不可以
var a = 1;
a = 2;
console.log(a); // 2
let b = 1;
b = 2;
console.log(b); // 2
const c = 1;
c = 2;
console.log(c); // 报错 Uncaught TypeError: Assignment to constant variable
1234567891011
需要注意的是,如果const定义的是引用类型的话,是可以更改值的。
const a = {
name:"张三",
age:19
}
a.name = "李四";
a.age = 20;
console.log(a);
打印出来:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。