我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情
闭包
闭包的两个特点:
- 是一个函数
- 这个函数在其他函数内部定义,能够读取其他函数内部的变量
function f1() {
let num = 1
function f2() {
debugger
// 访问外层函数的内部变量
console.log(num)
}
f2() // 形成闭包
debugger
}
f1()
高阶函数: 一个函数中return另一个函数,此时也会形成闭包。
// 高阶函数
function f3() {
let num = 1
return function() {
debugger
console.log(num)
}
}
let res = f3() // 闭包
res() // 1
闭包会使函数中的变量都被保存在内存中,大量使用闭包会造成性能问题。应该注意在合适的时候将闭包销毁。
销毁闭包: 代码执行完毕后,将闭包赋值为null,垃圾回收机制就会把内存中闭包引用的变量回收掉。
function f3() {
let num = 0
return function() {
num++
console.log(num)
debugger
}
}
let res = f3() // 闭包
res() // 1
res() // 2
res() // 3
res = null // 销毁闭包
res = f3()
res() // 1 闭包被销毁后,又重新开始计数
res() // 2
防抖和节流
节流和防抖都是用来控制某些函数的调用频率。例如,在 onmousemove 监听鼠标移动事件:
let count = 1;
const app = document.getElementById("app");
function change(e) {
app.innerHTML = count;
count++;
// console.log(e);
}
// 不限制函数执行,直接监听
app.onmousemove = change;
按照这种写法,会使函数非常频繁的执行,影响性能。应该使用节流或防抖来限制函数的执行。
节流
函数在一段时间内的多次调用,仅第一次有效。(频繁调用,指定时间内只执行一次。)
function throttle(func, delay) {
var timer = null
return function () {
if (!timer) {
func.apply(this, arguments)
timer = setTimeout(() => {
timer = null
}, delay)
} else {
console.log('上一个定时器尚未完成')
}
}
}
// 节流版本
app.onmousemove = throttle(change, 500)
防抖
防抖和节流不同的地方在于,函数在一段时间内的多次调用,仅使得最后一次调用有效。(函数被频繁调用后不会立即执行,指定时间内不再被调用,函数才会执行。)
function debounce(func, delay) {
var timeout
return function () {
clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(this, arguments)
}, delay)
}
}
// 防抖版本
app.onmousemove = debounce(change, 500);
原型与原型链
- 当读取实例的属性时,如果找不到,就会去查找原型的属性,还找不到就去找原型的原型,查到最顶层为止。
- 原型也是一个对象。原型对象就是通过 Object 构造函数生成的。
const obj = new Object();
console.log(obj.__proto__);
- 原型链:由相互关联的原型组成的链状结构就是原型链(下图蓝色虚线)。
执行上下文
三种可执行代码:全局代码、函数代码、eval代码。
当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。
JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文。
每个执行上下文都包含下面三种属性:
- 变量对象(Variable object,VO)
- 作用域链(Scope chain)
- this
变量对象
- 变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。
- 全局上下文中的变量对象就是
Window全局对象。 - 函数上下文中,用活动对象(activation object, AO)来表示变量对象。函数上下文的变量对象初始化只包括 Arguments 对象。
执行过程
执行上下文执行过程包括两个阶段:
- 进入执行上下文
- 代码执行
进入执行上下文
进入执行上下文时,还没有执行代码,变量对象会包括:
- 函数的所有形参
- 函数声明
- 变量声明
代码执行阶段
执行代码,修改变量的值。
作用域链
作用域
- JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。函数的作用域在定义的时候就决定了。
- 词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。
作用域链
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
函数创建
函数的作用域在定义的时候就决定了。
函数有一个内部属性 [[scoped]],当函数创建的时候,就会保存所有父变量对象到其中。注意:[[scoped]]并不代表完整的作用域链。
函数激活
当函数激活时,进入函数上下文,创建VO/AO后,就会将活动对象添加到作用域链的前端。
这时候执行上下文的作用域链命名为 Scope:
Scope = [AO].concat([[Scope]]);
作用域链创建完毕。
this
this 指向
- 全局环境: 浏览器的全局输出this,指向当前window对象;严格模式下,this是undefined; (node)单独的js文件全局输出this, this===module.exports this指向导出的对象。
- 事件调用环境(事件里面的this): 谁触发这个事件(函数),事件(函数)里面的this指向的就是谁(一般是接收事件的元素)。
- dom元素给js事件传入this参数,这个this指向dom元素本身。
- 函数内部:
-
this最终指向的是调用它的对象(与函数定义或声明的时候无关)
-
函数被多层对象所包含,如果函数最外层对象调用,this指向的也只是它的上一级的对象;
-
构造函数:构造函数中this指向的是实例对象。
function Person(name){ this.age = 18; this.name = name; console.log(this); } const tom = new Person("tom");// this -> Person: { age: 18, name: tom }new 作用:(1)new Object()创建一个对象(2)设置原型链(3)把创建出来的对象和this进行绑定(4)如果构造函数没有返回值,隐式返回this;如果构造函数有return返回值,返回值为对象(数组、函数)时,this指向返回的对象,否则将保持原本的规则。[特殊:return null 时,this指向保持原来规则]
-
- 箭头函数中的 this :箭头函数本身没有 this 和 arguments , 在箭头函数中引用this实际上调用的是上一层作用域的this,这里强调一下是上一层作用域,js对象不能形成独立作用域。 箭头函数在被声明时,this指向同时声明。
修改this指向:
示例:
const fruit = {
name: "apple",
getName: function(price, number){
console.log(`名称:${this.name} 价格:${price}元 数量:${number}个 `);
return this.name;
}
}
const good = {
name: "book"
}
// 原来的
fruit.getName(1,1); // 名称:apple 价格:1元 数量:1个
// 修改this后的
fruit.getName.call(good, 100, 2); // 名称:book 价格:100元 数量:2个
fruit.getName.apply(good, [50, 5]); // 名称:book 价格:50元 数量:5个
fruit.getName.bind(good, 30, 10)(); // 名称:book 价格:30元 数量:10个
- 方法(1)
fn.call(要修改的this指向,参数1,参数2, ...) - 方法(2)
fn.apply(要修改的this指向,[参数1,参数2, ...]) - 方法(3)
fn.bind(要修改的this指向,参数1,参数2, ...)bind()参数和call()参数一样,但是bind()会返回一个函数。apply()需要将参数放在数组中传递。
重排(reflow)和重绘(repaint)
-
重排:也叫回流。
- 页面初始渲染是最大的一次重排
- 添加/删除dom
- 改变元素位置、尺寸
- resize等事件浏览器窗口变化时
-
重绘:外观发生改变,但没有改变布局。
- color
- background
- ... 等属性
重排重绘的优化
重排开销很大,破坏用户体验,UI渲染慢,从以下几个方面优化:
- 减少重排范围,以局部布局(把一个dom的宽高等几何信息定死,然后在dom内部触发重排,就只会重新渲染该dom内部的元素,而不会影响到外界)形式组织html代码,不要通过父元素样式去控制子元素。
- 减少重排次数,js读写元素最好不要混在一起写,两个读之间不插入写的操作。这样的话浏览器的渲染队列机制会把写操作放在一起一次执行,减少了重排的次数。
- absolute和fixed脱离文档流,对其他节点的影响小。
- 优化动画:
- 可以把动画效果应用到浮动或定位元素上。
- 一些的属性可以让浏览器使用GPU渲染,提高性能。比如 Canvas2D,布局合成, CSS3转换(transitions),CSS3 3D变换(transforms),WebGL和视频(video)。
- 重排重绘都是基于cpu的,使用GPU渲染可以提高性能。
浏览器输入url到页面显示发生了什么
- 浏览器的地址栏输入URL并按下回车。
- 浏览器查找当前URL是否存在缓存
- 域名解析(DNS): 利用DNS解析,找到域名对应的IP地址。
- TCP连接: 三次握手
- 发送 HTTP 请求
- 服务器处理请求并返回 HTTP 报文
- 浏览器解析渲染页面
- 断开连接:TCP 四次挥手
安全
XSS(跨站脚本攻击)
XSS 全称 Cross Site Scripting,翻译过来是跨站脚本。XSS 攻击是指攻击者往 HTML 文件中或者 DOM 中注入恶意脚本,从而在用户浏览页面时利用注入的恶意脚本对用户实施攻击的一种手段。
攻击方式
- 恶意代码被攻击者利用漏洞提交到目标网站的数据库中存储起来
- 用户打开网站后,网站服务器从数据库中返回拼接在 HTML 中的恶意代码,被用户执行
- 可能导致用户将自身账号密码等敏感信息发送给攻击者,或者在当前浏览器调用目标接口完成攻击者指定的操作,再或者能够获取到窃取到Cookie信息等
防范
- 输入内容长度控制,增加 XSS 攻击难度
- 服务器对输入脚本进行过滤或转码
- 对第三方域进行限制:禁止提交数据、禁止加载资源等
- 在 http 响应头部设置 HttpOnly,防止 cookie 盗用
CSRF(跨站请求伪造)
Cross-site request forgery(跨站请求伪造)简称 CSRF,攻击者先获取到Cookie冒充用户在自动登录的网站上,利用登录状态可以做出和用户一样的操作,例如请求接口等。
XSS 利用的是用户对网站的信任,CSRF 利用的是网站对用户的信任。
防范
本质上还是因为 攻击者获取到了 cookie,所以要防止攻击者拿到cookie,具体方式有:
- 检查 Referrer 字段,配置白名单,Referrer 在白名单内才放过
- 添加校验 token
Vue专题
优化相关
import("./Foo.vue")路由懒加载,有效拆分App的大小,访问时才加载keep-alive缓存组件,避免重复创建组件,且能保存组件状态v-show和v-if的使用场景v-once和v-memo,v-once渲染后数据不再改变使用;v-memo传入一个条件,符合条件的才会被更新- 长列表性能优化,虚拟滚动,只渲染可视区域的内容。开源库
vue-virtual-scrollervue-virtual-scroll-grid - 事件的销毁:Vue组件销毁时,会自动解绑它全部的事件和指令,但仅限于组件本身的事件。一些用户自定义的事件,需要自己解绑/删除。
export default {
created(){
this.timer = setInterval(()=>{},1000)
},
beforeUnmount(){
clearInterval(this.timer)
}
}
- 图片懒加载: 用户看到的时候才加载图片。 开源库:
vue-lazyload - 一些三方库(element-ui)按需加载
- 子组件分割策略:重状态的组件适合拆分。无状态不更新的组件不要拆分。组件实例消耗远大于dom节点
SSR / SSG服务端渲染 / 静态网站生成