JavaScript - 总结
# JavaScript的编译,上下文context
# 过程
- 预编译期
- 浏览器的js引擎解析js代码
- 建立arguments对象(隐藏对象,不可见),函数,参数,变量
- 建立作用域链
- 确定this指向
- 执行期
- 按从上到下的顺序执行代码
- 函数体的预解析发生在函数被调用之时,被调用时先进行函数体的预编译,然后按顺序进行执行
# 上下文context
- 指的是一种运行环境
- context指的是,函数被调用时,看this指向哪个object,这个object就是当前的上下文。
# 执行上下文
概念
- 用来区分不同的运行环境,需要引出一个概念:执行上下文(Execution Context)
- 他是一个对象,由js的执行引擎创建
- 有三个属性:变量对象,作用域链,this指针
上下文栈
- js执行过程中有一个上下文栈,存放的是不同的上下文对象(不同的js运行环境)。
- 当前执行代码的context对象总是在栈顶。
变量对象
- 变量对象的创建过程如下:(变量提升被提出的原因)
- 创建arguments对象,其中保存多个属性,属性的key是0,1,2... value即传入参数的实际值。
- 找到这个作用域内的所有var和function,作为属性存储在变量对象中,如果是func,则属性名是 函数名,属性值是函数引用。 如果是var,属性名是变量名,属性值是undefined
- 变量对象的创建过程如下:(变量提升被提出的原因)
# js里的引用值,原始值和堆栈的区别
# 引用值
- 以对象形式存储在堆(heap)中,也就是说,存储在变量中的是一个指向内存的指针。由于引用值的大小 会改变,所以不能存储在栈中,否则会降低变量查询的速度。存放在栈空间的变量的值是该对象存储在堆中的地址,地 址的大小是固定的,所以把他存储在栈中对变量性能无任何负面影响。
# 原始值
- 存储在栈(stack)中得简单数据段,也就是说它们的值直接存储在变量访问的位置;基本数据类型按值访问 ,因此直接操作保存在变量中的实际值。
# 栈
- 由操作系统自动分配内存空间,自动释放,存储的是基础变量以及一些对象的引用变量,占 据固定大小的空间。
- 基础变量的值是存储在栈中,而引用变量存储在栈中的是指向堆中的数组或者对象的地址,这就 是为何修改引用类型总会影响到其他指向这个地址的引用变量。
- 优点:相比于堆来说存取速度会快,并且栈内存中的数据是可以共享的,例如同时声明了var a = 1 和var b = 1,会先处理a,然后在栈中查找有没有值为1的地址,如果没有就开辟一个值为1的地址,然后a指向这个地址 ,当处理b时,因为值为1的地址已经开辟好了,所以b也会同样指向同一个地址。
- 缺点:相比于堆来说的缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。
# 堆
- 由操作系统动态分配的内存,大小不定也不会自动释放,一般由程序员分配释放,也可由垃圾回收机 制回收。
- 堆内存中的对象不会随方法的结束而销毁,就算方法结束了,这个对象也可能会被其他引用变量所引用(参数传递)。 创建对象是为了反复利用(因为对象的创建成本通常较大),这个对象将被保存到运行时数据区(也就是堆内存)。 只有当一个对象没有任何引用变量引用它时,系统的垃圾回收机制才会在核实的时候回收它。
# ajax 与 axios
# ajax
- 原理:Ajax的原理简单来说,实际上就是通过XmlHttpRequest对象来向服务器发异步请求,从服务器获得数据,然后用javascript来操作DOM而更新页面
# axios
- 介绍:Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。
- 原理:本质上也是对原生xhr的封装,只不过它是promise的实现版本,符合最新的ES规范
# 垃圾回收机制
# 标记清除
- 就是在变量进入执行环境时标记成“进入环境”,离开时标记成“离开环境”
# 引用计数
- 就是跟踪一个值的引用次数,当声明一个变量并将一个引用类型赋值给该变量时该值引用次数加1, 当这个变量指向其他一个时该值的引用次数便减1。当该值引用次数为0时,则说明没有办法再访问这个值了, 被视为准备回收的对象。
# 数据类型
# 基本数据类型
- undefined, number string null boolean ES6新增Symbol, bigint
# 复杂(引用)数据类型
- object, array,function
# this指向(new,隐式绑定,显式绑定)
- new一个对象的原理
- 创建空对象 var obj = newObject()
- 让Person中的this指向obj,并执行Person这个构造函数。var result = Person.call(obj);
- 设置原型链,将obj的__proto__指向Person函数对象的prototype成员对象。obj.proto = Person.prototype;
- 判断result的返回值类型,如果是值类型,返回obj。如果是引用类型,就返回这个引用类型的对象。(因此调用call方法的 时候可能返回了this,也可能没有返回)
function Person(){ // let this = { // __proto__: Person.prototype // } // 创建一个新对象,赋值this,这一步是隐性的 // 给this指向的对象赋予构造属性 this.name = name this.age = age // 如果没有手动返回对象,则默认返回this指向的这个对象,也是隐性 // return this } const person = new Person()
1
2
3
4
5
6
7
8
9
10
11
12- 实现一个new方法
// 构造器函数 let Parent = function(name, age) { this.name = name this.age = age } Parent.prototype.sayName = function(){ console.log(this.name) } // 自己定义的new方法 let newFoo = function(Parent, ...rest) { // 以构造器的prototype属性为原型,创建新对象 let child = Object.create(Parent.prototype) // 将this和调用参数传给构造器执行 let result = Parent.apply(child, rest) // 如果构造器没有手动返回对象,则返回第一步的对象 return typeof result === 'object'? result:child; } // 创建实例,将构造函数Parent与形参作为参数传入 const child = newFoo(Parent, 'jack', 26) child.sayName() //'jack' // 检验,与使用new效果相同 child.instanceof Parent //t child.hasOwnProperty('name') //t child.hasOwnProperty('age') //t child.hasOwnProperty('sayName') //f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 关于this指向的问题
- 要记住几句话:
- this 永远指向最后调用它的那个对象
- 匿名函数的 this 永远指向 window
- 没有挂载在任何对象上的函数,非严格模式下 this 指向 window
- 有关例子,请移步这里 (opens new window)
# 箭头函数
- 箭头函数中没有this,若想打印它的话只能沿着作用域链去找。(也就是,箭头函数的this是指向上下文的)
- 箭头函数不能作为构造函数实例化,也不能被继承。
- 箭头函数没有arguments
# 深拷贝 浅拷贝
- 浅拷贝(拷贝指针)
- for in
- Object.assign() =>es5新方法
- 深拷贝(拷贝对象)
- 有三种方法,第一种JSON.parse 和 JSON.stringify; 第二种,JQ的extends; 第三种,自定义函数
- 第一种
var obj = {name:'123'}; var obj2 = JSON.parse(JSON.stringify(obj)) // 先对象转字符串,然后字符串转对象
1
2- 第二种
var a = [0,1,[2,3],4]; b = $.extend(true,[],a); a[0] = 1; a[2][0] = 7; console.log(a); // [1,1,[7,3],4]; console.log(b); // [0,1,[2,3],4]; //$.extend参数: //第一个参数是布尔值,是否深复制 //第二个参数是目标对象,其他对象的成员属性将被附加到该对象上 //第三个及以后的参数是被合并的对象
1
2
3
4
5
6
7
8
9
10- 第三种
// 简单实现版 function deepCopy(obj){ //判断是否是简单数据类型, if(typeof obj == "object"){ //复杂数据类型 var result = obj.constructor == Array ? [] : {}; for(let i in obj){ result[i] = typeof obj[i] == "object" ? deepCopy(obj[i]) : obj[i]; } }else { //简单数据类型 直接 == 赋值 var result = obj; } return result; } // 考虑循环引用(这个用得多点) function deepCopy(target, map=new Map()){ if(typeof target === 'object'){ const cloneTarget = Array.isArray(target)? []:{}; // 这里就是解决循环复用问题的地方,已经存在就直接返回 if(map.get(target)) return map.get(target); map.set(target, cloneTarget); for(const key in target){ cloneTarget[key] = deepCopy(target[key], map) } } else { return target; } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# Function的call,apply,bind方法
- 作用:这三个方法的作用其实都是改变函数内this的指向。实质上,call和apply只是使用了Function的属性,并且调用方法而已。
- 实际用途:扩充变量的作用域
- 函数的一些基本知识:
- 函数本身都是对象
- 每个函数都有自带两个属性:length和prototype。
- 函数的arguments对象包含了要传递的参数
# call
使用:call(作用域,枚举参数)
作用:在特定的作用域里(一般是this)执行函数。
call与apply不同的是,call必须明确传入每一个参数(枚举)
调用的例子
function sum(num1, num2){ return num1 + num2 } function callSum1(num1, num2) { return sum.call(this, num1, num2) } alert(callSum1(10, 10)) // 20
1
2
3
4
5
6
7
8手写call
// 思路 替换原函数中的this为目标对象,将参数传给原函数,兼容null作目标对象,自动转换为window对象
// 将要改变this指向的方法挂到目标this上执行并返回
Function.prototype.myCall = function (context) {
if (typeof this !== 'function') {
throw new TypeError('not funciton')
}
context = context || window //兼容传入null的情况
context.fn = this //将原函数添加到目标对象属性上
// es6写法的处理函数的参数,将具有length属性的对象 转成数组
let arg = [...arguments].slice(1)
let res = context.fn(...arg)
delete context.fn // 去除属性
return res
}
// test
const me = { name: 'Jack' }
function say() {
console.log(`My name is ${this.name || 'default'}`);
}
say.myCall(me)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# apply
使用:apply(作用域,参数)
作用:在特定的作用域里(一般是this)调用函数
调用的例子
// 思路 在context上调用方法,触发this绑定为context, 使用Symbol防止原有属性覆盖 function sum(num1, num2){ return num1 + num2 } function callSum1(num1, num2) { return sum.apply(this, arguments) } function callSum2(num1, num2) { return sum.apply(this, [num1, num2]) } alert(callSum1(10, 10)) // 20 alert(callSum2(10, 10)) // 20
1
2
3
4
5
6
7
8
9
10
11
12手写apply
// 将要改变this指向的方法挂到目标this上执行并返回
Function.prototype.myApply = function (context = globalThis) {
const key = Symbol('key')
context[key] = this
let res
if(arguments[1]) {
res = context[k](...arguments[1])
} else {
res = context[key]()
}
delete context[key]
return res
}
// test
const me = { name:'jack' }
function say() {
console.log('my name is ${this.name || 'default'}')
}
say.myApply(me)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# bind
使用:bind(绑定对象)
作用:把函数的this绑定到绑定对象上
调用的例子
window.color = 'red' var o = { color: 'blue' } function sayColor(){ alert(this.color) } var objectSayColor = sayColor().bind(o) objectSayColor(); // blue
1
2
3
4
5
6
7手写bind
Function.prototype.myBind = function (context = globalThis) {
const fn = this
const args = Array.from(arguments).slice(1)
const newFunc = function () {
const newArgs = args.concat(...arguments)
if (this instanceof newFunc) {
// 通过new调用,绑定this为实例对象
fn.apply(this, newArgs)
} else {
// 通过普通函数形式调用,绑定context
fn.apply(context, newArgs)
}
}
// 支持new调用方式
newFunc.prototype = object.create(fn.prototype)
return newFunc
}
// test
const me = { name:'jack' }
const other = { name:'jackson' }
function say(){
console.log('my name is ${this.name || 'default'}')
}
const mySay = say.bind(me)
mySay()
const otherSay = say.bind(other)
otherSay()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 综合用例
第一道
var name = 'name1'; var obj = { name: 'name2', sayName: function(){ // 返回一个默认全局的函数 return function(){ console.log(this.name); }; }, changeName: function(){ // 返回一个默认全局的函数 setTimeout(function(){ this.name = 'name3'; // 然后将该函数绑定给this(当前obj对象) }.bind(this),0); } }; // obj.sayName()这个函数,让obj来调用 obj.sayName().call(obj); // 让this(也就是全局对象)来调用 obj.sayName().apply(this); obj.changeName(); setTimeout(function(){ // 输出更改之后,全局name的值 console.log(name); // 输出更改之后,obj对象中 name的值 console.log(obj.name); },0);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31- 结果: name2 name1 name1 name3
第二道 Function.call.call.call
function f1() {
console.log(1)
}
function f2() {
console.log(2)
}
f1.call(f2) // 1
f1.call.call(f2) // 2
f1.call.call.call(f2) // window
2
3
4
5
6
7
8
9
# 闭包(概念 用途 手写)
# 概念
- 闭包是包含了那个局部变量的容器,它被内部函数对象引用。
- 特点是2个函数相互嵌套,内部函数引用了外部函数的局部变量,执行外部函数。
- 闭包的本质还是函数
# 作用
- 延长局部变量的生命周期
- 使函数外部可以多次间接操作到函数内部的数据。
# 应用
- 循环遍历监听
- IIFE定义模块
- jQuery内部
# 优缺点
- 优点:延长了变量的生命周期
- 缺点:无法被回收机制及时回收造成内存泄露,只能手动释放(手动=null)
# 例子
var F = function () {
var sum = 10;
return function () {
console.log(sum) //注意这里一定不能加this,不然就是全局查找变量,输出undefined
}
}
var f = new F()
f() //10
2
3
4
5
6
7
8
# 高阶函数
###定义
- 高阶函数是对其它函数进行操作的函数,它接收函数作为参数或者将函数作为返回值输出
- 说人话就是将一个函数作为返回值输出。
- 个人感觉,用来封装用的。面向AOP变成(切面,动态织入)
# 条件
- 函数可以作为参数被传递
- 函数可以作为返回值输出
# 实例
// 判断数据类型
var isType = function (obj) {
return function (obj) {
return Object.prototype.toString.call(obj) === '[object' + type + ']';
}
}
var isStr = isType('String') // 封装判断String方法
var Array = isType('Array') // 封装判断数组方法
console.log(isArray([1,2,3])) //调用判断数组方法,返回true
2
3
4
5
6
7
8
9
# 同步与异步
# 同步
- 同步:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。如果在函数A返回 的时候,调用者就能够得到预期的结果(即拿到了预期的返回值或者看到了预期的效果),那么这个函数 就是同步的。
# 异步
- 不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步 任务可以执行了,该任务才会进入主线程执行。如果在函数A返回的时候,调用者还不能马上得到预期 的结果,而是需要在将来通过一定的手段得到,那么这个函数就是异步的 。
- 发起函数和回调函数
- 异步的执行机制:
- 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)
- 主线程之外,还存在一个"任务队列"(task queue),只要异步任务有了运行结果,就在"任务队列"之中放置一个事件
- 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件,那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行
- 主线程不断重复上面的第三步
# ES5
# var 变量提升 函数提升
- var相当于全局变量,他的值可以被内部修改。用var声明的变量/函数在预编译时是有提升的。
- 变量提升 具体看这一则代码:
// 源码
function test(){
var n = 1
if(1){
var n = 2
}
console.log(n) // 输出2
}
test()
if(1){
var a = 100
}
console.log(a) // 输出100
// 预编译
var n // 此时n是undefined
var a // 此时a是undefined
function test(){
n = 1 // n被赋值1
if(1){
n = 2 // n被赋值1
}
console.log(n) //输出2
}
if(1){
a = 100 // a被赋值100
}
console.log(a) //输出100
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- 函数提升。在js里函数的优先级是最高的,如果有同名的函数和变量,函数将优先被提升至作用域顶端。
var foo = 3
function test(){
console.log(foo); //function foo(){}
foo=5;
console.log(foo); // 5
function foo(){}
}
test();
console.log(foo) // 3
2
3
4
5
6
7
8
9
- 提升的意义
- 提升的意义是人为的,为了解决函数的相互递归问题,也就是两个函数的执行都要调用到对方。
##Object对象及常见方法
# Object.defineProperty
- Object.defineProperty(obj, prop, desc),作用:直接在一个对象上定义新属性,或者修改一个已经存在的属性。
# ES6
# 块级作用域 let const
# 继承
- 建立父类
function Person(name) {
this.name = name;
this.sum = function () {
alert(this.name);
}
}
Person.prototype.age = 10;
2
3
4
5
6
7
- 原型链继承
function Per() {
this.name = "ker";
}
Per.prototype = new Person();
var per1 = new Per();
console.log(per1.age);
// 判断元素是否在另一个元素的原型链上,per1继承了Person的属性
console.log(per1 instanceof Person); // true
2
3
4
5
6
7
8
让新实例的原型等于父类的实例。
缺点:
- 新实例无法向父类构造函数传参
- 继承单一
- 所有新实例共享父类实例属性。(原型上的属性是共享的,一个属性被修改,另一个也会被修改)
构造函数继承
function Con() {
Person.call(this, "jer") //利用call、apply将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行)
this.age = 12;
}
let con1 = new Con();
console.log(con1.name);
console.log(con1.age);
console.log(con1 instanceof Person);
2
3
4
5
6
7
8
特点:只继承了父类构造函数的属性,原型没有继承。可以继承多个构造函数属性(多个call)。 在子类中可向父类传参。
缺点:
- 只能继承父类构造函数的属性
- 无法实现构造函数复用(每次都要重新调用)
- 每个新实例都会有父类构造函数的副本,冗余。
组合继承(常用)(原型+构造)
function SubType(name) {
Person.call(this, name);
}
SubType.prototype = new Person();
let sub = new SubType("gar");
console.log(sub.name);
console.log(sub.age);
2
3
4
5
6
7
重点:结合了传参和复用
特点:1. 可以继承父类原型上的属性,可传参,可复用。 2. 每个新实例引入的构造函数属性私有。
缺点:调用了两次父类构造函数(耗内存),子类构造函数会代替原型上的父类构造函数。
寄生组合继承
// 寄生,content就是F实例的另一种表示法
function content() {
function F() {};
F.prototype = obj;
return new F();
}
let con = content(Person.prototype); // con实例(F实例)的原型继承了父类函数的原型,上述更像是原型链继承,只不过是继承了原型属性。
// 组合
function Sub() {
Person.call(this); // 这个继承了父类构造函数的属性,解决了组合式两次条用构造函数属性的缺点
}
// 重点
Sub.prototype = con; //继承con实例
con.constructor = Sub; // 修复实例
var sub1 = new Sub();
console.log(sub1.age) //10
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
重点:解决了组合继承的问题。
ES6继承
class Animal {
constructor(props) {
this.name = props.name || 'null'
}
eat(){
console.log(this.name + 'Animal name')
}
}
class Cat extends Animal{
constructor(props, myAttr) {
super(props); //重点 这里一定要有props
this.type = props.type || 'null'
this.attr = myAttr //私有属性
}
fly(){
console.log(this.name + 'cat name')
}
myattr(){
console.log(this.type + '---' + this.attr)
}
}
let myCat = new Cat({
name: 'yellow cat',
type: 'cat'
}, 'cat class')
myCat.eat() //yellow catAnimal name
myCat.fly() //yellow catcat name
myCat.myattr() //cat---cat class
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# Proxy(代理,守卫,拦截)
- 作用:让函数和其变量私有化
- 语法
let p = new Proxy(target, handler);
- target:需要用Proxy包装的目标对象
- handler:一个对象,其属性是当执行一个操作时代理的行为函数。
- 用法
- 比如这里有一个例子:
const obj = {
val: 10;
}
obj.val2 = 20;
// 这里用proxy 对obj对象设置get方法和set方法的拦截
const handler = {
get: function (obj, prop) {
if(prop == "id"){
return 6;
}
console.log("get");
return obj[prop];
},
set: function(obj, prop, value) {
if(typeof value !== "string") {
throw new Error("Only String values can be stored in this object!");
} else {
obj[prop] = value;
}
}
}
const initialobj = {
id: 1,
name: "Foo bar"
}
const proxiedObject = new Proxy(initialobj, handler);
console.log(proxiedObject.id);
proxiedObject.name = 8; // 这里会报错,触发拦截
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# Set WeakSet Map WeakMap
set和weakSet
- 一样都是不重复的值的集合。
- 区别:WeakSet的成员只能是对象,而不能是其它的值。WeakSet中的对象都是弱引用,即垃圾回收机制不考虑WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该 对象还存在于 WeakSet 之中。
map和weakMap
- 一样都是键值对的集合(hash)。
- 区别:weakMap只接受对象和null作为键名。weakMap的键名指向的对象不计入垃圾回收机制。也就是说会随着其引用的对象被回收而释放。
# Promise async await
# Promise
解释:是异步编程的一种解决方案,比传统的异步解决方案更合理和强大。
作用:解决回调地狱问题
状态与关系:
- Promise中约定,当对象创建之后同⼀个Promise对象只能从pending状态变更为fulfilled或rejected中的其中⼀ 种,并且状态⼀旦变更就不会再改变,此时Promise对象的流程执⾏完成并且finally函数执⾏
常用方法
- all: 只有全部promise成功,才return成功。
- race: 比赛,看谁最快完成,返回最快完成的状态。
- allSelected: 接收一个promise数组,如果有非promise项则当做成功,把每个promise结果,集合成数组返回
- any: 和all相反,只要有一个成功,就返回成功的那个
执行流程
- new Promise会调用构造器创建一个Promise对象,系统调用匿名回调函数,并且将创建好的resolved和rejected函数,传给函数, 成功后在Promise对象中调用resolve()会把resolve内的参数递给.then(funcion(data))
创建方式
- new Promise(fn);
- Promise.resolve(fn);
promise与事件循环
- promise在初始化时,传入的函数是同步执行的,然后注册then回调。注册完之后,继续往下执行同步代码。在这之前,then中回调不会执行。 同步代码块执行完毕后,才会在时间循环中检测是否有可用的promise回调,如果有,执行,没有,下一个事件循环。
promise与es6
- es6中提供了一种generator和promise的语法糖,就是async和await。
(async () => {
let 蔬菜 = await 买菜();
let 饭菜 = await 做饭(蔬菜);
let 送饭结果 = await 送饭(饭菜);
let 通知结果 = await 通知我(送饭结果);
})
2
3
4
5
6
- 手写promise
# 宏任务 微任务 EventLoop
- 任务队列分两种类型,宏任务先执行,微任务后执行。宏任务会阻塞浏览器的渲染进程,微任务会在通任务结束后立即执行,在渲染之前。
# 为什么JavaScript是单线程?
- 作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。
# 宏任务(macro Task)
- IO,setTimeout; setInterval; setImmediate; requestAnimationFrame
# 微任务(micro Task)
- Promise,prototype,then catch finally; (node环境)process.nextTick; MutationObserver,
# EventLoop 事件循环
为了解决JS异步编程的一种方案。
进程与线程
- 进程是CPU资源分配的最小单位。
- 线程是CPU调度的最小单位。
- 它俩的区别就是进程携带资源,线程不带。
- 它俩的关系,一个进程可以有多个线程。
浏览器中的EventLoop执行顺序
- eventLoop会不断循环的去获取任务队列中最老的一个任务(宏任务)推入栈执行,并在当次 循环里依次执行并清空微任务队列里的任务。
- 执行完微任务队列里的任务,有可能会渲染更新。(浏览器积攒变动以最高60HZ的频率更新视图)
- 图示
练习的例子:点这里 (opens new window)
# 事件冒泡与事件委托
# 事件冒泡
- 概念:js所谓的事件冒泡就是子级元素的某个事件被触发,它的上级元素的该事件也被递归执行
- 例子:假设这里有列表嵌套
<ul><li></li></ul>
, 我点击li标签,ul标签也会被触发点击事件,这就叫冒泡。 - 阻止:e.stopPropagation():
$("ul[data-type='cityPick']").on('click',function(){
alert("父元素ul被点击");
});
$("ul[data-type='cityPick']").on('click','li',function(e){
e.stopPropagation();//阻止冒泡
alert("子元素li被点击");
});
2
3
4
5
6
7
# 事件委托
- 概念:事件委托,即利用冒泡的原理,从点击的元素开始,以递归的方式向父元素传播事件。事件目标并不处理事件,而是把事件委托给父元素/祖先元素。
- 好处:
- 对于大量要处理的元素,不用为每个元素都绑定事件,只需要在父元素绑定一次即可,提高性能。
- 可以处理动态插入dom中的元素。
- 实例:(给5个按钮绑定事件)
<div class="button-group">
<bottoun type="button" class="btn">提交</bottoun>
<bottoun type="button" class="btn">提交</bottoun>
<bottoun type="button" class="btn">提交</bottoun>
<bottoun type="button" class="btn">提交</bottoun>
<bottoun type="button" class="btn">提交</bottoun>
</div>
<script>
$(".button-group").on('click','.btn',function(){
alert($(this).html());
});
</script>
2
3
4
5
6
7
8
9
10
11
12
# 原型与原型链
- 原型。所有引用类都有一个__proto__隐式原型对象,属性值是一个普通的对象
- 所有函数都有一个prototype原型属性,值是一个普通对象
- 所有引用类型的__proto__属性指向它构造函数的prototype
- 蓝色的线就是原型链。当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有 找到,则会去它的__proto__隐式原型上查找,即它的构造函数的prototype,如果还没有找到 就会再在构造函数的prototype的__proto__中查找,这样一层一层向上查找就 会形成一个链式 结构,我们称为原型链。
# 函数柯里化(Currying)
# 概念
- 柯里化是指把有多个参数的函数转成只有一个参数的函数,并返回接受余下参数且返回结果的新函数的技术。
- 直接看个例子吧:
function sum(x, y) { return x+y } // 柯里化 function curryingSum(x) { return function (y) { return x+y } } sum(1, 2) curryingSum(1)(2)
1
2
3
4
5
6
7
8
9
10
11
# 好处
- 说白了就是在函数里面再封装一层,但是这么做的意义是什么呢?(再提一嘴,bind实现的机制就是currying)
- 参数复用
- 提前确认
- 延迟执行
# currying封装
- 对于每次使用currying都要对底层函数进行修改,所以做了以下封装:
- 大概思想就是,通过闭包把初步参数保存下来,然后通过获取剩下的arguments对象进行拼接,最后
执行需要currying的函数。
function progressCurrying(fn, args) { var _this = this var len = fn.length var args = args || [] return function () { var _args = Array.prototype.slice.call(arguments) Array.prototype.push.apply(args, _args) // 如果参数个数小于最初的fn.length,则递归调用,继续收集参数 if(_args.length < len) { return progressCurrying.call(_this, fn, _args) } // 参数收集完毕,执行fn return fn.apply(this, _args) } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 通用写法(定长)
function currying(fn, ...args) {
return fn.length>args.length? (...newArgs) => currying(fn, ...args, ...newArgs) : fn(...args)
}
function sum(a,b) {
return a+b
}
sum = currying(sum)
sum(1)(2)
sum(1,2)
2
3
4
5
6
7
8
9
# 经典面试题,常见!
// 实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;
// 参数多少不定
function add(...args) {
return args.reduce((a,b) => a+b);
}
function currying(fn) {
let args = [];
return function _c(...newArgs){
if(newArgs.length){
// 数据合并
args = [
...args,
...newArgs
]
return _c
} else {
return fn.apply(this, args);
}
}
}
let addCurry = currying(add);
console.log(addCurry(1)(2)(3)(4,5)()); // 注意这里多了个括号
// 定长
function add1(a,b,c,d) {
return [...arguments].reduce((a,b) => a+b);
}
function currying1(fn) {
let len = fn.length;
let args = [];
return function _c(...newArg){
args = [
...args,
...newArg
];
if(args.length < len) return _c; // 返回函数
else return fn.apply(this, args.slice(0, len)) // 返回结果
}
}
let useCurry = currying1(add1);
let total = useCurry(1)(2)(3,4);
console.log(total)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 防抖和节流
# 防抖debounce
作用:防抖函数的作用就是控制函数在一定时间内的执行次数。防抖意味着n秒内函数只会被执行一次,如果n秒内 再次被触发,则重新计算延迟时间。
应用场景:
- 搜索框输入查询
- 表单验证
- 按钮提交事件
- 浏览器窗口缩放
防抖函数的实现
- 思路:时间第一次触发,timer是null,调用later(),若immediate是t,那么立即调用 func.apply(this, params); 若immediate是f,那么过wait之后,调用func.apply(this, params)
- 事件第二次触发,如果timer重置为null,那么流程和第一次触发一样,如果timer不为null,则清空定时器, 重新计时。
- immediate为true时,表示函数在每个等待时延的开始被调用;为false,表示在时延结束被调用
// 首次运行时把定时器赋值给一个变量,第二次执行时,如果间隔没超过定时器设定的时间则会清除掉定时器,重新设定定时器,依次反复,当我们停止下 // 来时,没有执行清除定时器,超过一定时间后触发回调函数。 function debounce(fn, wait) { let timeout = null return function () { let context = this let args = arguments if(timeout) clearTimeout(timeout) timeout = setTimeout(() => { fn.apply(context, args) }, wait) } } // test const task = () => {console.log('run task')} const debounceTask = debounce(task, 1000) window.addEventListener('scroll', debounceTask)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 函数节流(throttle)
- 作用:当持续触发事件时,保证一定时间段内只调用一次事件处理函数。
- 应用场景:
- scroll
- touchmove
- 节流实现
function throttle(fn, ms=1000) {
let canRun = true
return function (...args) {
canRun = false
setTimeout(() => {
fn.apply(this, args)
canRun = true
}, ms)
}
}
// test
const task = () => {console.log('run task')}
const throttleTask = throttle(task, 1000)
window.addEventListener('scroll', throttleTask)
2
3
4
5
6
7
8
9
10
11
12
13
14
# 防抖与节流的区别
- 节流是不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而 防抖是只在最后一次事件之后才触发一次函数。比如在页面的无限加载场景下,我们需要用户在滚动页面时 每隔一段时间发一次ajax请求,而不是在用户停止滚动时才请求数据,那么就是节流。
# 重绘与回流/重排
# html加载
- 页面加载时,浏览器把获取到的HTML代码解析成一个DOM树,DOM树里包含了所有HTML标签,包括display:none 隐藏,还有js动态添加的元素等。
- 浏览器把所有的样式解析成样式结构体。DOMTree和样式结构体组合后构件Render Tree。
# 重绘(repaint)
- 重绘就是Render Tree中一些元素需要更新(只影响元素的外观,风格,不影响布局),就称为重绘。
- 重绘:不涉及任何DOM元素的排版问题的变动为重绘。
- 触发重绘:color修改,text-align修改,a:hover修改,:hover引起的颜色等。
# 回流(重排)(reflow)
Render Tree中的一部分因为元素的规模尺寸,布局,隐藏等改变而需要重新构建,就叫回流。
每个页面至少需要一次回流,就是页面第一次加载的时候。
完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程成为重绘。
触发重排:
- width/height/margin/border/padding修改
- 动画,:hover等伪类引起的元素表现变动,display=none等造成页面回流。
- appendChild等DOM元素操作
- font类style修改。
- background修改
- scroll页面,不可避免
- resize页面(桌面版本的进行浏览器大小缩放
- 读取元素属性:offsetLeft\top\height\width, getComputedStyle()等
# 区别与优化
区别
- 回流必然引起重绘,但重绘不一定会引起回流。
- 当页面布局和几何属性改变的时候就需要回流。(比如浏览器大小缩放)
- 回流的代价比重绘大。
优化
- DOM的增删行为。如果要多加个子元素,最好是用documentfragment
- 几何属性变化(比如border,front-size)。如果要改变多个几何属性,最好将这些属性定义在一个class中,直接修改 class名,这样只引起一次回流。
- 元素位置(margin,padding)的变化。做元素位移的动画,不要更改margin之类的属性,使用定位脱离文档流后位置会更好,
- 获取元素的偏移量属性。比如scrollTop,offsetTop之类,浏览器为了保证值的正确也会回流得到最新 的值。如果要多次操作,最好取完做个缓存。
- 浏览器窗口尺寸改变。 resize也会引起回流。
# 跨域的几种方式
# 什么是跨域
- 首先,一个域名地址的组成:协议+子域名+主域名+端口号+请求地址
- js出于安全考虑,不允许跨域调用其他页面对象。跨域就是js同源策略限制。当协议、主域名、子域名、端口号中任意一个不相同,都算作不同的域。
- 同源策略限制的存在是为了保护用户隐私信息,防止身份伪造等(读取cookie)
# 跨域解决
JSONP
- 原理:利用
<script>
元素的开放策略,网页可以得到其他源动态产生的JSON数据。 - 优缺点。优:兼容性好。 缺:仅支持get请求
- 实现
<script type="text/javascript"> function fn(data) { alert(data.msg); } </script> <script type="text/javascript" src="http://crrossdomain.com/jsonServerResponse?jsonp=fn"></script> // ajax $ajax({ url:"http://crrossdomain.com/jsonServerResponse?jsonp=fn", dataType: "jsonp", type: "get", jsonCallback:"fn", // 自定义传递给服务器的函数名,可省略 jsonp:"jsonp", // 把传递函数名的形参callback变成jsonp,可省略 success: function(data) { console.log(data) } })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18- 原理:利用
CROS
- 原理:实现CROS通信的关键是服务器。只要服务器实现了CROS接口,就可以跨源通信。
- 优缺点。 优:是跨域的根本方法,由浏览器自动完成,支持更强大的HTTP Method。 缺:兼容性IE10以上。
- 实现:
// 前端设置:
axios.defaults.withCredentials = true //axios
Vue.http.options.credentials = true // vue-resource(插件来的,主要应该还是上面的axios配置,这个不装就不用)
// 服务器设置:
header("Access-Control-Allow-Origin:*");
header("Access-Control-Allow-Methods:POST,GET");
// 这里以node为例子
const express = require("express")
const app = express();
app.all("*", function (req, res, next) {
res.header("Access-Control-Allow-Origin","*");
res.header("Access-Control-Allow-Headers","content-type");
res.header("Access-Control-Allow-Methods","DELETE,PUT,POST,GET,OPTIONS");
if (req.method.toLowerCase() == 'options')
res.send(200); //让options尝试请求快速结束
else
next();
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 网络安全
# XSS攻击(cross site scripting 跨域脚本攻击)
- 原理: 将一些隐私数据像 cookie、session 发送给攻击者,将受害者重定向到一个由攻击者控制的网站,在受害者的机器上进行一些恶意操作。
- 攻击方式:向网站 A 注入 JS代码,然后执行 JS 里的代码,篡改网站A的内容。
- 防范
- HTTPOnly 防止劫取cookie
- 输入检查:输入检查一般是检查用户输入的数据中是否包含 <,> 等特殊字符,如果存在,则对特殊字符进行过滤或编码,这种方式也称为 XSS Filter。
// vuejs 中的 decodingMap // 在 vuejs 中,如果输入带 script 标签的内容,会直接过滤掉 const decodingMap = { '<': '<', '>': '>', '"': '"', '&': '&', ' ': '\n' }
1
2
3
4
5
6
7
8
9
10
# CSRF/XSRF攻击(Cross Site Request Forgery,跨站请求伪造)
- 原理:劫持受信任用户向服务器发送非预期请求的攻击方式。
- 攻击方式:利用受害者浏览器中的cookie发起验证请求,因为是用户浏览器中自带的,所以不会被服务端发现。
- 防范
- 验证码。验证码会强制用户必须与应用进行交互,才能完成最终请求。因为通常情况下,验证码能够很好地遏制 CSRF 攻击。
- Referer Check (http请求头中有个referer字段,它记录了该 HTTP 请求的来源地址)。以下是服务端代码
if (req.headers.referer !== 'http://www.c.com:8002/') { res.write('csrf 攻击'); return; }
1
2
3
4- token。可以在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token,如果请求 中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。
# 浏览器的缓存
# 强制缓存
- 强缓存只有在资源过期之后,才会向服务器请求资源。
- 强制缓存判断HTTP首部字段:cache-control,Expires。
- Expires是服务器时间(绝对时间)。 浏览器检查当前时间,如果还没到失效时间就直接使用缓存文件。缺:服务器时间与客户端时间可能不一致,现今少用,
- cache-control中的max-age保存相对时间。如果同时存在cache-control 和 Expires,优先cache。
# 协商缓存(对比缓存)
- 协商缓存是先从缓存中获取对应的数据表示,向服务器发起请求,确认数据是否更新,如果更新,则返回新数据和缓存,反之返回304,告知客户端未更新,可继续使用。
- 协商缓存通过HTTP的last-modified, Etag字段判断。
- last-modified:第一次请求资源时服务器返回的字段,表示最后更新的时间。下一次浏览器请求资源时就发送if-modified-since字段。服务器用本地Last-modified时 间与if-modified-since时间比较,如果不一致则认为缓存已过期并返回新资源给浏览器;如果时间一致则发送304状态码,让浏览器继续使用缓存。
- Etag:资源实体标识(哈希串),当资源内容更新时,Etag会改变。服务器会判断Etag是否发生变化,如果变化则返回新资源,否则304。(有点像webpack实现热更新的原理..)
# 从输入URL到页面加载完成,期间发生了什么?
# 完整的过程
- 浏览器的地址栏输入URL并按下回车。
- 浏览器查找当前URL的DNS缓存记录。
- DNS解析URL对应的IP地址。
- 根据IP地址建立TCP连接(三次握手)。
- 浏览器向服务器发送HTTP请求。
- 服务器处理请求,浏览器接收HTTP响应。
- 渲染页面,构建DOM树。
- 关闭TCP连接(四次挥手)。
# 过程中的几个关键点
TCP(传输层协议,面向连接,可靠)
- DNS解析后,获取到了服务器IP地址,在获取到IP地址后,TCP会开始建立一次链接,就是所谓的“三次握手”:
- 第一次握手: 建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;
- 第二次握手: 服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
- 第三次握手: 客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。
- 最后由tcp关闭连接,即四次挥手。(FIN ACK, ACK, FIN ACK, ACK)
- 第一次挥手是浏览器发完数据后,发送FIN请求断开连接。
- 第二次挥手是服务器发送ACK表示同意,如果在这一次服务器也发送FIN请求断开连接似乎也没有不妥,但考虑到服务器可能 还有数据要发送,所以服务器发送FIN应该放在第三次挥手中。
- 这样浏览器需要返回ACK表示同意,也就是第四次挥手。
浏览器与服务器的交互
- 浏览器发送http请求
- 完整的HTTP请求: 起始行、请求头、请求主体
- 浏览器接受响应
- 服务器在收到浏览器发送的HTTP请求之后,会将收到的HTTP报文封装成HTTP的Request对象,并通过不同的Web服务器进行处理,处理完 的结果以HTTP的Response对象返回,主要包括状态码,响应头,响应报文三个部分。
- 响应头主要由Cache-Control、 Connection、Date、Pragma等组成。响应体为服务器返回给浏览器的信息,主要由HTML,css,js,图片文件组成。
- 状态码如下
- 1xx:指示信息–表示请求已接收,继续处理。
- 2xx:成功–表示请求已被成功接收、理解、接受。
- 3xx:重定向–要完成请求必须进行更进一步的操作。
- 4xx:客户端错误–请求有语法错误或请求无法实现。
- 5xx:服务器端错误–服务器未能实现合法的请求。
- 浏览器发送http请求
页面渲染
- 如果说响应的内容是HTML文档的话,就需要浏览器进行解析渲染呈现给用户。整个过程涉及两个方面:解析和渲染。在渲染页面之前,需要构建DOM树和CSSOM树。
- 构件HTML解析DOM树,并行请求css/img/js
- css下载完成,开始构件CSSOM
- CSSOM构建结束后,和DOM一起生成Render Tree(渲染树)
- 布局(layout),计算出每个节点在屏幕中的位置
- 显示(painting):把元素绘制在屏幕上
- 如果说响应的内容是HTML文档的话,就需要浏览器进行解析渲染呈现给用户。整个过程涉及两个方面:解析和渲染。在渲染页面之前,需要构建DOM树和CSSOM树。