一、JS核心重点基础
1. JS数据的存储
- 数据
- 存储在内存中代表特定信息的东西,本质上就是01010...的二进制信息
- 数据的特点:可传递、可运算
- 内存
- 内存条通电之后产生的可存储数据的空间(临时的)
- 内存产生和死亡:内存条(电路板) => 通电 => 产生内存空间 => 存储数据 => 处理数据 => 断电 => 内存空间和数据都消失
- 内存中的两种数据
- 内部存储的数据本身
- 存储对应的地址值
- 内存的分类
- 栈内存: 存储基本的数据类型值 和 对象的引用地址值
- 堆内存: 存储对象数据
- 变量
- 可变化的量,由变量名和变量值组成
- 每个变量都对应一小块内存,变量名用来查找对应的值,变量值就是内存中保存的数据
- 内存、数据和变量三者之间的关系:
- 内存用来存储数据的空间
- 变量名是栈内存空间的地址值标识名,堆内存的标识就是对应的地址值
- JS引擎如何管理内存
- 内存生命周期
- 分配小内存空间,得到它的使用权
- 存储数据,可以反复进行操作
- 释放小内存空间
- 释放内存
- 局部变量:函数执行完自动释放
- 对象:成为垃圾对象 => 垃圾回收器回收
- 内存生命周期
- JS语句是否加分号?
- js的每一条语句后面可以加也可以不加分号
- 是否加分号是编码风格问题,没有应改不应该,只有喜欢不喜欢
- 以下2种情况下不加分号会有问题
- 小括号开头的前一条语句:会被当成调调用函数
IIFE - 中括号开头的前一条语句:会被当前读取对象的属性
var a = 4 [1,2].forEach() - 处理办法:在这2种情况的首行加分号
// js调用函数时传递变量参数时,是值传递还是引用传递 ? // 理解一:都是值传递,基本值 和 引用类型的地址值 // 理解二:可能是值传递,也可能是引用传递(引用数据的地址值) var a = {age: 13} function fn2 (obj) { obj = {age: 15} } fn2(a) console.log(a) // {age: 13} - 小括号开头的前一条语句:会被当成调调用函数
2. JS的数据类型及特点说明
基本数据类型(值类型):
- Number
- 包含:正数、负数、0、正负小数
- 还包含两个特别的值:
NaN和InfinityNaN表示不是一个有效的数字- 通过
isNaN方法判断一个值是不是NaN NaN和任何值都不相等,和自己本身都不相等,即NaN == NaN为falseNaN和任何数值进行计算,都等于NaN
- parstInt parseFloat Number 数值转换:
- parstInt() 从参数的第一个字符开始找,找到不是数字的字符时就停止查找(第一个字符可以是负号),并返回已查找的数值,如果都没有找到,就返回NaN
- parseFloat() 从参数的第一个字符开始找,找到不能组合成有效数字时就停止查找,并返回已查找的数值,如果都没有找到,就返回NaN
- Number() 参数必须都是有效的数字型字符串,如果不是则返回NaN。记一下:
true => 1false => 0null => 0"" => 0[]=>0undefined => NaN{}=>NaN。
只有truefalsenull""[]这几个能被Number函数转换为数值,其他非字符串/数字的值都转换为NaN
- String
- Boolean
- undefined
- null
- Symbol
- BigInt
引用数据类型(对象类型):
- Object
- Object 对象,根据JS默认的构造函数类型的细分
- 普通的数据对象
{} - 数组类型
[](一种特别的对象,数值下标,有length属性,内部数据有序) - 正则类型
/[0-9]{2,4}/ - Math数学计算类型
Math - Data日期类型
Date() - ...
- 普通的数据对象
- Object 对象,根据JS默认的构造函数类型的细分
- Function
-
将Function这个单独作为一类应用类型,
typeof function(){} => "function"
其他的引用类型typeof 都是"object"
- undefined 和 null 的区别?
- undefined代表未赋值,有三类为undefined的情况:
- 已定义未赋值
- 函数形参为传入实参
- 对象是未定义的属性为undefined
- null表示定义并赋值了,只是值为null,对象的原型链末端最终指向null
- undefined代表未赋值,有三类为undefined的情况:
- 什么时候给变量设置为null?
- 通常定义一个引用类型的变量,但是定义的时候还不确定对象值为多少,初始赋值为null,表明将要赋值为对象
- 一个引用类型变量使用完毕之后,设置为null,让指向的对象成为垃圾对象,进而让垃圾回收器回收
- 严格区别变量类型和数据类型
- 数据类型: 基本类型 和 对象类型
- 变量类型(变量内存值的类型):
- 基本类型:保存的是基本的数据类型
- 引用类型:保存的是地址值
-
3. JS数据类型检测
typeof检测数据类型的逻辑运算符:typeof [value]—— 返回当前value值的数据类型,返回结果都是字符串,"数据类型",如:"number" | "string" | "boolean" | "undefined" | "symbol" | "bigint" | "object" | "function" (没有"null")- 局限性:
- 不能检测基本类型
null:typeof null => "object" typeof不能细分对象类型,除了函数类型返回为"function"之外,其它应用类型都返回"Object"
- 不能检测基本类型
typeof typeof typeof [1,2]返回的值为? 结果为:"string"typeof [1,2]=> "object"typeof "object"=> "string"typeof "string"=> "string"
instanceof检测数据是否为某个类的实例,主要用来判断对象的具体类型- 表达式:
A instanceof B - 如果B函数的显式原型对象
prototype在A对象的原型链上,返回true,否则返回false - JS中申明的函数对象都是通过
new Funtion产生的 实例对象
- 表达式:
- 使用
===判断undefined和null constructor检测构造函数- 测试一个对象/函数的隐式原型__proto__ 和 构造函数的显式原型的prototype 的构造函数constructor属相是否等于对应要测试的构造函数
function Person (name) { this.name = name } var a = {}; var b = new Person('test'); a.__proto__.constructor === Person // false b.__proto__.constructor === Person // true var fn = function () {}; fn.__proto__.constructor === Person // false fn.__proto__.constructor === Function // true
- 测试一个对象/函数的隐式原型__proto__ 和 构造函数的显式原型的prototype 的构造函数constructor属相是否等于对应要测试的构造函数
Object.prototype.toString.call()检测数据类型返回的字符串function checkType (data: any, judgeType: string | undefined): boolean | string { var typeStr = Object.prototype.toString.call(data); var type = typeStr.match(/\[object (\S*)\]/)[1].toLowerCase() if (judgeType) { // 判断数据data是不是这个judgeType这个数据类型 return judgeType.toLowerCase() === type } else { // 返回data的数据类型 return type } }- 还有一些专门检测某种类型的检测Api:
Array.isArray()
检测方法总结:🚩
- 基本类型(除了null) 和 函数类型 使用
typeof判断,不能判断null和object undefined和null可以使用===判断- 对象类型使用
instanceof判断具体的类型,数组可以使用类型Array.isArray()判断 - 使用
Object.prototype.toString.call()封装函数判断所有数据的具体类型
4. JS数据类型转换
- 强转换(基于底层机制转换)
Number([value])- 一些隐式转换都是基于
Number完成的- isNaN("12px") 先将其他类型的值使用
Number()转换为数字再检测 - 数学运算
"12.3" + 6=>18.3"13.5px" - 6=>NaN - 字符串==数字 两个等于号比较很多时候也是要将其他类型值转换为数字进行比较
- Number()转换结果:
true => 1false => 0null => 0"" => 0undefined => NaN[]=>0{}=>NaN - 字符串必须保证完全是有效的数值型字符串,才能被正确转换为数字,额否则都是
NaN
- isNaN("12px") 先将其他类型的值使用
- 其他隐式转换
0|""|undefined|null|NaN转换为boolean都是false,其他数据转换都为true[] == true=> 都转换为数字进行比较 结果为false❓🤔️ 🚩Number([])=>Number('')=>Number(0)=>0Number(true)=>1
{} == true=> 都转换为数字进行比较 结果为false🚩Number({})=>Number('[object Object]')=>Number(NaN)=>0Number(true)=>1
- 一些隐式转换都是基于
- 弱转换(基于一些额外的方法转换)
parseInt([value])parseFloat([value])Number和parseInt/parseFloat转换的区别: parseInt和parseFloat处理的值是字符串,从字符串的左侧开始查找有效数字字符(遇到非有效数字字符则停止查找),最后返回如果不是一个有效的数字,就返回NaNNumber直接调用浏览器最底层多数据类型检测机制完成隐式转换parseInt('') // NaN parseInt(null) // parseInt('null') => NaN parseInt('12px') // 12 parseInt('abc') // NaN Number('') // 0 Number(null) // Number(0) => 0 Number('12px') // NaN Number('abc') // NaN isNaN('') // false isNaN(null) // false isNaN('12px') // true isNaN('abc') // true parseFloat('12.6px') + parseInt('1.2px') + typeof parseInt(null) // "13.6number" isNaN(Number(!!Number(parseInt("0.8")))) // false typeof !parseInt(null) + !isNaN(null) // 'booleantrue' const result = 10 + false + undefined + [] + 'Tencent' + null + true + {} console.log(result) // 结果为:'NaNTencentnulltrue[object Object]' // 10 + false => 10 + 0 => 10 // 10 + undefined => 10 + Number(undefined) => 10 + Number('undefined') => 10 + NaN => NaN // NaN + [] => NaN + String([]) => NaN + '' => 'NaN' // 'NaN' + 'Tencent' => 'NaNTencent' // 'NaNTencent' + null => 'NaNTencent' + String(null) => 'NaNTencent' + 'null' => 'NaNTencentnull' // 'NaNTencentnull' + true => 'NaNTencentnull' + String(true) => 'NaNTencentnull' + 'true' => 'NaNTencentnulltrue' // 'NaNTencentnulltrue' + {} => 'NaNTencentnulltrue' + String({}) => 'NaNTencentnulltrue' + '[object Object]' => 'NaNTencentnulltrue[object Object]'
两个等号比较的规律:🚩
- 对象==字符串 => 对象转换为字符串再比较
null == undefined为true,自此之外null和undefined其他的值都不相等- 其他两边类型不同的值比较,都是转换为数字
5. JS中的函数
- 什么是函数?
- 实现特定功能的多条语句的封装 - 只有函数是可以执行的,其他类型的数据都不能执行
- 为什么要用函数?
- 提高代码复用
- 方便阅读交流
- 如何定义函数?
- 函数声明
function test () { ... } - 函数表达式
var test = funtion () { ... }
- 函数声明
- 如何调用(执行)函数?
- 直接调用: test()
- 通过对象调用:obj.test()
- new调用:new test()
- 临时调用:test.call(obj) 通过制定的对象obj调用test函数。可以让一个函数成为指定任意对象的方法进行调用
- 回调函数
- 什么是回调函数
- 你定义的
- 你没有调用
- 但是它最终被执行了
- 常见的回调函数
- dom事件回调函数 onclikc = fn
- 定时器回调函数 setTimeout(fn,times) setInterval(fn,times)
- ajax请求回调函数
- 生命周期回到函数
- 什么是回调函数
-
立即执行函数(IIFE)
- 一个函数不用名称,也不要赋值给一个变量,直接使用()执行的函数
- 作用
- 隐藏实现
- 不会污染外面(全局)的命名空间
- 用来编写js模块
-
函数的this
- this是什么?
- 任何函数本质上都是通过某个对象调用的,如果没有直接制定就是window
- 所有函数内部都有一个变量this
- 它的值是调用这个当前函数的对象
- 如何确定this的值? 🚩
- 直接调用 test() : window
- 指定对象调用 p.test() : p
- new test(): 新创建的对象
- 指定this调用 p.call(obj) : obj
- this是什么?
-
箭头函数和普通函数的区别?
- 箭头函数没有prototype(原型),所以箭头函数本身没有this
- 箭头函数的this在定义的时候继承自外层第一个普通函数的this。
- 如果定义箭头函数的外层没有普通函数,严格模式和非严格模式下它的this都会指向window(全局对象)
- 箭头函数本身的this指向不能改变,但可以修改它要继承的对象的this。
- 箭头函数的this指向全局,使用arguments会报未声明的错误,可以用rest参数
...解决。 - 箭头函数的this指向普通函数时,它的argumens继承于该普通函数
- 箭头函数是匿名函数,没有原型
prototype, 也就没有constructor构造函数,不能使用new - 箭头函数不支持new.target
- 箭头函数不支持重命名函数参数,普通函数的函数参数支持重命名
- 箭头函数相对于普通函数语法更简洁优雅
-
函数中的prototype属性
- 每个函数都有一个prototype属性,默认指向一个空Object对象(即称为:原型对象)
(这里的空对象值我们没有自己定义的属性) - 原型对象中有一个属性constructor,他指向函数对象
- 可以给原型对象添加属性(一般都是方法)
- 作用:函数的所有实例对象都自动拥有原型对象中的属性(方法)
var c = 1; function c(c){ console.log(c) var c = 3 } // 相当于c的赋值语句 c = 1 在这一行执行,从就不是一个函数了 c(2) // 报错了: c is not a function
- 每个函数都有一个prototype属性,默认指向一个空Object对象(即称为:原型对象)
6. JS中的对象
对象的理解
- 什么是对象?
- 多个数据的封装体,是用来保存多个数据的容器
- 一个对象代表现实中的一个事物,每个属性代表这个事物的属性
- 为什么要用变量?
- 统一管理多个数据
- 对象的组成
- 属性:由属性名(字符串)和属性值(任意值)组成
- 方法:是一种特别的属性(属性值是函数的属性),可以执行
- 如何访问对象内部数据?
.属性名:编码简单,只能访问一般的字符串属性[属性名]:编码麻烦,适用更多的情况
- 什么时候必须使用
[属性名]的方式访问对象内部的数据?- 属性名中带有特殊字符:-、空格
- 属性名不确定,属性名是一个变量的时候
对象创建模
- Object构造函数模式
- 用法:先创建空的Object对象,在动态添加属性/方法
- 适用场景:起初不确定对象内部的数据
- 问题:语句太多,繁琐
var person = new Object(); person.name = '张三'; person.age = 18 person.setName = function(name){ this.name = name } - 对象字面量模式
- 用法:使用{}创建对象,同时指定属性和方法
- 适用场景:起初对象内部的数据是确定的
- 问题:如果创建多个对象,代码重复
var person = { name: '张三', age: 18, setName: function(name){ this.name = name } } - 工厂函数模式
- 用法:通过工厂函数动态创建对象并返回
- 适用场景:需要创建多个对象
- 问题:创建的对象没有具体的类型,都是Object类型
function createPerson(name, age) { // 返回一个对象的函数就是工厂函数 var obj = { name: name, age: age, setName: function(name){ this.name = name } } return obj; } var p1 = createPerson('张三', 18) var p2 = createPerson('李四', 20) - 自定义构造函数模式
- 用法:自定义构造函数,通过new创建对象
- 适用场景:需要创建多个类型确定的对象
- 问题:每个对象都有相同的数据,浪费空间
function Person (name, age) { this.name = name; this.age =age; this.setName = function(name){ this.name = name } //每个对象都有一个相同的setName函数对象 } var p1 = new Person('张三', 18) var p2 = new Person('李四', 20) - 构造函数 + 原型的组合模式
- 用法:自定义构造函数,不同对象的属性在函数中初始化,相同的方法添加在原型上
- 适用场景:需要创建多个类型确定的对象
function Person (name, age) { // 在构造函数中初始化一般的数据 this.name = name; this.age =age; } Person.prototype.setName = function(name){ this.name = name } var p1 = Person('张三', 18) var p2 = Person('李四', 20)
对象继承模式
- 原型链继承: 继承父类型原型的属性和方法
- 用法:
- 定义父类型的构造函数
- 给父类型的原型对象上添加方法
- 定义子类型的构造函数
- 创建父类型的实例对象并赋值给子类型的原型对象
- 将子类型的原型对象的构造函数属性设置为子类型
- 给子类型的原型对象上添加方法
- 创建的子类型的实例对象就可以调用父类型的原型上的方法
- 关键点:
- 子类型的原型为父类型的一个实例对象
function Super () { this.name = 'superName'; } Super.prototype.getSuperName = funtion () { return this.name; } function Sub () { this.name = 'subName'; } // 让Sub的原型指向Super的一个实例对象,这个实例对象的__proto__指向Super的原型对象prototype Sub.prototype = new Supper() // 实现原型链的继承 // 只有上面的继承,Sub的实例对象的构造函数都是指向Super,这样不太好,修正一下 Sub.prototype.constructor = Sub // 修正继承者的构造函数 Super.prototype.getSubName = funtion () { return this.name; } var sub = new Sub() sub.getSubName() // 'subName' 这个肯定是可以找到并执行的 sub.getSuperName() // 'superName' 这个需要让Sub的原型继承Super才可以 - 用法:
- 借助构造函数继承: 复用父类型的代码设置实例的属性
- 用法:
- 定义父类型的构造函数
- 定义子类型的构造函数
- 在子类型的构造函数中调用父类型的构造函数
- 关键点:
- 在子类型的构造函数中通过call()绑定当前子类型的实例对象this来调用父类型的构造函数
function Person (name, age) { this.name = name; this.age = age; } Person.prototype.getName = funtion () { return this.name; } function Student (name, age, classNo) { // 通过call绑定Student的实例为this,从而复用父类型的属性。实际上不存在继承的关系 Person.call(this, name, age) this.classNo = classNo; } Super.prototype.getClassNo = funtion () { return this.classNo; } var sub = new Sub() sub.getSubName() // 'subName' 这个肯定是可以找到并执行的 sub.getSuperName() // 'superName' 这个需要让Sub的原型继承Super才可以 - 用法:
- 原型链+借用构造函数的组合继承:
- 利用原型链继承实现对父类型的原型属性和方法的继承
- 利用call()借用父类型的构造函数初始化相同的实例属性和方法
function Person (name, age) { this.name = name; this.age = age; } Person.prototype.setName = funtion (name) { this.name = name; } function Student (name, age, classNo) { // 通过call绑定Student的实例为this,从而复用父类型的属性。实际上不存在继承的关系 Person.call(this, name, age) // 得到同父类型相同的属性 this.classNo = classNo; } Student.prototype = new Person() // 继承父类型原型的属性方法 Student.prototype.constructor = Student Student.prototype.getClassNo = funtion () { return this.classNo; } var sub = new Sub() sub.getSubName() // 'subName' 这个肯定是可以找到并执行的 sub.getSuperName() // 'superName' 这个需要让Sub的原型继承Super才可以 - ES6的class语法继承:
- 利用class语法的extends实现类的继承
class Person { constructor (name, age) { this.name = name; this.age = age; } setName (name) { this.name = name; } } class Student extends Person { constructor (name, age, classNo) { super(name, age) this.classNo = classNo } getClass () { return this.classNo; } }
7. 原型和原型链
-
概念:
- 原型是函数和对象都拥有的一个属性,在函数对象上,有一个prototype属性的原型对象,被称为显式原型,普通对象都是Object构造对象的实例,每个对象上都有一个__proto__属性的原型对象,被称为隐式原型
- 原型链是在对象实例上的__proto__属性形成的一个链结构,在原型链的最末位是Object函数的原型对象,Object的原型对象的__proto__属性指向null
-
显式原型和隐式原型
- 每一个函数function都有一个prototype属性,即显式原型(属性)
- 每一实例对象有一个__proto__属性,可称为隐式原型(属性)
- 实例对象的隐式原型的值为其对应构造函数的显式原型的值
- 总结:
- 函数的prototype属性:在定义函数时自动添加的,默认是一个Object对象
- 对象的__proto__属性:创建对象时自动添加的,默认值为构造函数的prototype属性值
- 程序员能直接操作显示原型,但不能直接操作隐式原型(es6之前)
-
原型链:
- 访问一个对象的属性时,先在自身属性中查找,找到就返回;
- 如果没有,再沿着隐式原型__proto__这条链向上查找,找到就返回
- 如果最终没有找到,就返回undefined
-
构造函数 / 原型 / 实例对象关系图
-
function Foo(){}的原型图分析- 等价于:
var Foo = new Funtion()
- 等价于:
-
var b = {}的原型图分析- 等价于:
var a = new Object()
- 等价于:
-
JS默认的系统构造函数中的特殊解读:
-
- 函数的显式原型对象
prototype默认指向的是Object构造函数的实例对象(Object构造函数自身除外)
console.log(fn.prototype instanceof Object)=>trueconsole.log(Object.prototype instanceof Object)=>falseconsole.log(Function.prototype instanceof Object)=>true
- 函数的显式原型对象
-
- 所有函数都是
Function构造函数的实例(包括Function构造函数本身)
console.log(Funtion.__proto__ === Function.prototype)=>trueconsole.log(Object.__proto__ === Function.prototype)=>true
- 所有函数都是
-
Object构造函数的原型对象是原型链的尽头
console.log(Object.prototype.__proto__)=>null
-
-
对象属性的读取和设置:
- 读取对象的属性值时:对象自身找不到的时候会自动到原型链中查找
- 设置对象的属性值时:不会查找原型链,如果当前对象上没有此属性,直接在当前对象上添加此属性,不影响对象原型的值
- 方法一般定义在原型中,所有的对象实例公用,属性一般通过构造函数定义在对象本身
8. 作用域和作用域链
-
作用域
- 什么是作用域?
- 作用域在内存中就是一块小的内存空间,是一个代码段所在的区域
- 作用域是静态的(相对于上下文),在编写代码的时候就确定了
- 作用域的分类
- 全局作用域
- 函数作用域
- 块级作用域(ES6之后才有这个概念)
- 作用域的作用
- 隔离变量,不同作用域下同名变量不会有冲突而发生覆盖
- 作用域和执行上下文的区别:
- 区别一:确定时间不同
- 全局作用域是在js代码加载之后,开始执行代码前确定的;
在全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义的时候就已经确定了,而不是在函数调用的时候确定的 - 全局执行上下文是在全局作用域确定之后,js代码执行之前创建的;
函数执行上下文是在函数作用域确定之后,函数体代码执行之前创建的
- 全局作用域是在js代码加载之后,开始执行代码前确定的;
- 区别二:状态不同
- 作用域是静态的,只要函数定义好了就一直存在,且不会再变化
- 上下文环境是动态的,调用函数时创建,函数调用结束时上下文环境就会被释放
- 联系
- 上下文环境(对象)是从属于所在的作用域的
- 全局上下文环境 => 全局作用域
- 函数上下文环境 => 对应的函数作用域
- 区别一:确定时间不同
- 什么是作用域?
-
作用域链
- 作用域链是多个上下级关系的作用域形成的链,他的方向时从下向上的(从内到外),查找变量时就是沿着作用域链来查找的
- 作用域链查找一个变量的规则:
- 1.在当前作用域下的执行上下文中查找对应的属性,如果有直接返回,否则进入2
- 2.在上一级作用域的执行上下文中查找对应的属性,如果有直接返回,否则进入3
- 3.再次执行2中的相同操作,直到全局作用域,如果全局作用域中还是没有找到,就抛出找不到的异常
9. 闭包
-
如何产生闭包?
- 当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时,产生闭包
-
闭包是什么?
- 使用Chrome调试可以查看到闭包的对象
- 理解一:闭包是嵌套的内部函数
- 理解二:闭包是包含被引用的变量(函数)的Closure对象
- 注意:闭包存在于嵌套的内部函数中
-
闭包产生的条件
- 函数嵌套
- 内部函数引用了外部函数的数据(变量/函数)
- 外部函数被执行了,才会执行内部函数的定义,才能产生闭包(内部函数不用执行)
function f1 () { var a = 12; var b = 'abc', function f2 () { console.log(a) // 内部的闭包函数引用链外部函数作用域的变量,这个闭包对象才会有这个变量属性 } // var f2 = function () { console.log(a) } // 这样以函数表达式定义内部闭包函数,需要在这一句定义执行之后才会产生闭包函数及其内部引用的Closure闭包对象 // f2() 执不执行这个f2函数都已经产生闭包了 } f1() //外层函数要执行,才能产生内部定义的闭包函数 -
常见的闭包
- 将函数作为另外一个函数的返回值
function fn1 () { // 代码执行到这里,由于变量/函数声明的提升,内部函数对象就已经产生了,此时闭包产生 var a = 2; function fn2 () { a++; console.log(a) } return fn2 } var f = fn1() // 执行一次 fn1 就产生一个闭包。 // 如果只是执行了fn1.没有使用变量f将返回的闭包函数对象fn2引用, // 那返回的闭包函数对象fn2是一个垃圾对象,会被垃圾回收器会后,闭包也就没有了 f() // 3 f() // 4 f = null // 闭包死亡(唯一一个包含闭包的函数对象称为垃圾对象) - 将函数作为实参传递给另外一个函数调用
function showDelay (msg, time) { setTimeout(function () { // 将这个函数作为实参传给setTimeout函数调用 alert(msg) // 产生的闭包对象中有一个msg属性 }, time) } showDelay('hello world', 1000)
- 将函数作为另外一个函数的返回值
-
闭包的作用
- 使函数内部的变量在函数执行完之后,仍然存活在内存中(延长链局部变量的生命周期)
- 让函数外部可以操作(读写)到函数内部的数据(变量/函数)
-
闭包的应用
- 定义JS模块
- JS模块是具有特定功能的js文件
- 将所有的数据和功能都封装在一个函数内部(私有的)
- 只向外暴露一个包含n个方法的对象或函数
- 模块的使用者,只需要通过模块暴露的对象调用方法来实现对应的功能
// 模块文件: transStringModule.js (function(global){ var name = 'Never Gave Up'; // 模块的局部变量 function getUpperCase () { return name.toUpperCase() } function getLowerCase () { return name.toLowerCase() } global.transStringModule = { getUpperCase, getLowerCase } })(window)
- 定义JS模块
-
闭包的优缺点
- 缺点
- 函数执行完后,函数内部的局部变量没有释放,占用内存时间长
- 容易造成内存的泄露
- 解决的办法
- 能不使用闭包就不使用闭包
- 及时释放 (使用完之后将闭包的引用设置为null清除)
- 缺点
-
内存溢出和内存泄漏
- 内存溢出
- 一种程序运行出现的错误
- 当程序运行需要的内存空间超过了剩余的内存时,就会报错内存溢出的错误
for(var a =0; a < 1000; i++){ var arr = new Array(1000000) // 创建这么多的大数组。所需要的内存空间超出了设备空闲的内存空间 console.log(arr) } - 内存泄漏
- 占有的内存没有及时释放
- 内存泄漏积累多了就容易导致内存溢出
- 常见的内存溢出:
- 意外的全局变量,没有使用var声明的变量自动称为全局变量
- 没有及时清理的计时器或回调函数
- 闭包,闭包的变量没有处理
- 内存溢出
-
面试题
var name = 'The window';
var object = {
name: 'The object',
getName: function(){
return function(){
return this.name
}
}
}
alert(object.getName()()) // ? => "The window"
var name = 'The window';
var object = {
name: 'The object',
getName: function(){
var that = this;
return function(){
return that.name
}
}
}
alert(object.getName()()) // ? => "The object"
function fun(n, o){
console.log(o)
return {
fun: function(m){
return fun(m,n)
}
}
}
var a = fun(0); a.fun(1); a.fun(2); a.fun(3); // undefined; 0; 0; 0;
var b = fun(0).fun(1).fun(2).fun(3); // undefined; 0; 1; 2;
var c = fun(0).fun(1); c.fun(2); c.fun(3); // undefined; 0; 1; 1;
10. JS模块化
二、JS中底层运算
1. 进程和线程
- 进程(process)
- 程序的一次执行,都占有一片独有的内存空间
- 线程(thread)
- 是进程中的一个独立执行单元
- 只程序执行的一个完整流程
- 是CPU的最小调度单元
- 单线程
- 优先:顺序编程简单易懂
- 缺点:效率低
- 多线程
- 优先:能优先提升CPU的利用率。同时运行的多个线程可以在多个CPU内核上执行
- 缺点:创建多线程开销、线程间的切换开销、死锁与状态同步问题
- 何为多进程和多线程
- 多进程:一个应用程序可以同时启动多个实例运行
- 多线程:在一个进程内,同时有多个线程运行
- 图解:
- 应用程序必须运行在某个进程的某个线程上
- 一个进程中至少有一个运行的线程:主线程,进程启动后自动创建
- 一个进程中也可以同时运行多个线程,我们就说这个程序是多线程运行的
- 一个进程内的事件可以供其中多个线程直接共享
- 多个进程之间的数据是不能直接共享的
- 线程池(thread pool):保存多个线程对象的容器,实现线程对象的反复利用
- JS是单线程还是多线程的?
- JS是单线程运行的
- 但使用H5中的Web Workers可以多线程运行
- 浏览器是单线程还是多线程的?
- 都是多线程的
- 浏览器是单进程还是多进程的?
- 有的是单进程:firefox、 老版本IE
- 有的是多进程:chrome、新版IE
2. 浏览器及其内核
-
不同浏览器内核:
- chrome、Safari: webkit
- firefox:Gecko
- IE:Trident
- 360、搜狗等国内浏览器,双核双驱动运行:Trident + webkit
-
浏览器内核又很多模块组成:
主线程中:- js引擎模块:负责js程序等编译与运行
- html,css文档解析模块:负责页面文本的解析
- DOM/CSS模块:负责dom/css在内存中的相关处理
- 布局和渲染模块:负责页面的布局和效果绘制 分线程中:
- 定时器模块:负责定时器的管理
- DOM事件响应模块:负责事件的管理
- 网络请求模块:负责ajax请求
-
输入一个网址回车后的完整流程:
-
1、解析URL:首先会对 URL 进行解析,分析所需要使用的传输协议和请求的资源的路径。如果输入的 URL 中的协议或者主机名不合法,将会把地址栏中输入的内容传递给搜索引擎。如果没有问题,浏览器会检查 URL 中是否出现了非法字符,如果存在非法字符,则对非法字符进行转义后再进行下一过程。
-
2、缓存判断:浏览器会判断所请求的资源是否在缓存里,如果请求的资源在缓存里并且没有失效,那么就直接使用,否则向服务器发起新的请求。
-
3、DNS解析: 下一步首先需要获取的是输入的 URL 中的域名的 IP 地址,首先会判断本地是否有该域名的 IP 地址的缓存,如果有则使用,如果没有则向本地 DNS 服务器发起请求。本地 DNS 服务器也会先检查是否存在缓存,如果没有就会先向根域名服务器发起请求,获得负责的顶级域名服务器的地址后,再向顶级域名服务器请求,然后获得负责的权威域名服务器的地址后,再向权威域名服务器发起请求,最终获得域名的 IP 地址后,本地 DNS 服务器再将这个 IP 地址返回给请求的用户。用户向本地 DNS 服务器发起请求属于递归请求,本地 DNS 服务器向各级域名服务器发起请求属于迭代请求。
-
4、获取MAC地址: 当浏览器得到 IP 地址后,数据传输还需要知道目的主机 MAC 地址,因为应用层下发数据给传输层,TCP 协议会指定源端口号和目的端口号,然后下发给网络层。网络层会将本机地址作为源地址,获取的 IP 地址作为目的地址。然后将下发给数据链路层,数据链路层的发送需要加入通信双方的 MAC 地址,本机的 MAC 地址作为源 MAC 地址,目的 MAC 地址需要分情况处理。通过将 IP 地址与本机的子网掩码相与,可以判断是否与请求主机在同一个子网里,如果在同一个子网里,可以使用 APR 协议获取到目的主机的 MAC 地址,如果不在一个子网里,那么请求应该转发给网关,由它代为转发,此时同样可以通过 ARP 协议来获取网关的 MAC 地址,此时目的主机的 MAC 地址应该为网关的地址。
-
5、TCP三次握手: 下面是 TCP 建立连接的三次握手的过程,首先客户端向服务器发送一个 SYN 连接请求报文段和一个随机序号,服务端接收到请求后向客户端发送一个 SYN ACK报文段,确认连接请求,并且也向客户端发送一个随机序号。客户端接收服务器的确认应答后,进入连接建立的状态,同时向服务器也发送一个ACK 确认报文段,服务器端接收到确认后,也进入连接建立状态,此时双方的连接就建立起来了。
-
6、HTTPS握手: 如果使用的是 HTTPS 协议,在通信前还存在 TLS 的一个四次握手的过程。首先由客户端向服务器端发送使用的协议的版本号、一个随机数和可以使用的加密方法。服务器端收到后,确认加密的方法,也向客户端发送一个随机数和自己的数字证书。客户端收到后,首先检查数字证书是否有效,如果有效,则再生成一个随机数,并使用证书中的公钥对随机数加密,然后发送给服务器端,并且还会提供一个前面所有内容的 hash 值供服务器端检验。服务器端接收后,使用自己的私钥对数据解密,同时向客户端发送一个前面所有内容的 hash 值供客户端检验。这个时候双方都有了三个随机数,按照之前所约定的加密方法,使用这三个随机数生成一把秘钥,以后双方通信前,就使用这个秘钥对数据进行加密后再传输。
-
7、返回数据: 当页面请求发送到服务器端后,服务器端会返回一个 html 文件作为响应,浏览器接收到响应后,开始对 html 文件进行解析,开始页面的渲染过程。
-
8、页面渲染: 浏览器首先会根据 html 文件构建 DOM 树,根据解析到的 css 文件构建 CSSOM 树,如果遇到 script 标签,则判端是否含有 defer 或者 async 属性,要不然 script 的加载和执行会造成页面的渲染的阻塞。当 DOM 树和 CSSOM 树建立好后,根据它们来构建渲染树。渲染树构建好后,会根据渲染树来进行布局。布局完成后,最后使用浏览器的 UI 接口对页面进行绘制。这个时候整个页面就显示出来了。
-
9、TCP四次挥手: 最后一步是 TCP 断开连接的四次挥手过程。若客户端认为数据发送完成,则它需要向服务端发送连接释放请求。服务端收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明客户端到服务端的连接已经释放,不再接收客户端发的数据了。但是因为 TCP 连接是双向的,所以服务端仍旧可以发送数据给客户端。服务端如果此时还有没发完的数据会继续发送,完毕后会向客户端发送连接释放请求,然后服务端便进入 LAST-ACK 状态。客户端收到释放请求后,向服务端发送确认应答,此时客户端进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有服务端的重发请求的话,就进入 CLOSED 状态。当服务端收到确认应答后,也便进入 CLOSED 状态。
-
-
JS的单线程运行 (同一时刻只能执行一行代码)
- JS是单线程运行的,但使用H5中的Web Workers可以多线程运行
- 如何证明js执行是单线程的?
- setTimeout()的回调函数是在主线程执行的。其实JS代码都是在主线程的
var time1 = new Date(); setTimeout(function(){ console.log(new Date() = time1) //会发现这个事件间隔远大于200ms,由于这里的回调要等到耗时的循环结束结束之后才执行 // js的单线程让异步的回调函数代码只能等待同步代码执行完之后才能被执行 }, 200) for(var i = 0; i < 1000000000; i++){} - 定时器回调函数只有在运行栈中的代码全部执行完毕之后才有可能调用
setTimeout(function(){ console.log('timeout22222') alert('22222') }, 2000) setTimeout(function(){ console.log('timeout11111') alert('11111') }, 1000) setTimeout(function(){ console.log('timeout00000') }, 0) function fun(){ console.log('fun()') } fun() console.log('alert()之前') // alert语句会暂停主线程的执行,同时暂停计时,关闭alert弹框后,恢复程序的执行和计时 alert('=====') console.log('alert()之后') // 当前作用域中的 初始化代码 执行完毕之后,再在特定的时间执行回调函数的代码。 // alert() 函数的执行会暂停当前唯一主线程的执行,同时暂停计时,关闭alert弹框后,恢复程序的执行和计时。 // setTimeout的回调事件,也会因为alert语句暂停主线程的执行而暂停执行。 // 输出: // 'fun()' => 'alert()之前' => 弹出'====='同时程序暂停运行 => // 确定关闭弹框 => 'alert()之后' => 立即执行'timeout00000' => // 1s后输出'timeout11111'并弹出'11111'同时程序暂停运行 => 确定关闭弹框 => // 又过1s后输出'timeout22222'并弹出'22222'同时程序暂停运行 => 确定关闭弹框 => // 程序执行完毕
- setTimeout()的回调函数是在主线程执行的。其实JS代码都是在主线程的
- 为什么js要用单线程模式,而不用多线程模式?
- javascript的单线程,与它的用途有关
- 作为浏览器脚本语言,Javascript的主要用途是与用户互动,以及操作DOM
- 着决定来他只能是单线程,否则会带来很多复杂的同步问题
- 按代码的执行时机将代码分为两类:
- 初始化代码(同步代码):作用域中的主线代码,包含设置定时器、绑定事件监听、发送ajax请求的代码,不是指这些异步操作的回调处理逻辑
- 回调代码:处理回调的逻辑
- JS引擎执行代码的基本流程
- 先执行初始化代码,包含一些特别的代码
- 设置定时器
- 绑定事件监听
- 发送ajax请求
- 然后在某个时刻才会执行回调代码
- 先执行初始化代码,包含一些特别的代码
3. JS事件循环模型
-
js的所有代码都是在js引擎创建的执行栈(execution stack)中执行的
-
浏览器内核
- borwer core
- js引擎模块(在主线程处理)
- 其他模块(在主/分线程处理)
-
callback queue 回调队列中包含的事件:
- 任务队列
- 消息队列
- 事件队列
-
事件循环(event loop)
- 从任务队列中循环取出回调函数放入执行栈中处理(一个接一个)
-
请求响应模型(request-response model)
-
JS事件循环模型的运转流程
- 执行初始化代码,将时间回调函数交给对应的模块(DOM事件响应模块、定时器模块、网络请求模块)处理
- 当事件发生时,管理模块会将回调函数及其数据添加到回到队列中
- 只有当初始化代码执行完后(可能要一定的时间),才会遍历读取回调队列中的回调函数执行
- 宏任务:
- setTimeout / setInterver
- 事件绑定
- ajax
- 微任务:
- Promise (new Promise的时候exector执行器函数是同步执行的)
- async await
4. JS编译运行机制
JS之所以能够在浏览器中运行,是因为浏览器给JS提供咯执行代码的环境 => 栈内存(Stack)。
栈内存(Stack): 提供执行代码的环境
堆内存(Heap):存放东西(存放的是属性方法)
- 预编译:在作用域的创建阶段,就是预编译阶段。预编译处理的事情:
- 创建AO对象(AO对象时js的变量对象,供js引擎自己去访问的)
- 找形参和变量的声明作为AO对象的属性名,此时的值为undefined
- 实参和形参相统一
- 找函数声明,会覆盖变量的声明
- 变量声明提升: 在变量定义语句之前就可以访问变量,只是这个变量的值是undefined
- 通过var定义(声明)的变量,在定义语句之前就可以访问到
- 值:undefined
- 函数声明提升:在定义函数语句之前就可以访问变量,就可以直接调用这个函数
- 通过function声明的函数,在之前就可以直接调用
- 值:定义的函数(对象)
- 先执行变量声明的提升,在执行函数声明提升,所以同名的变量和函数,都是函数覆盖变量
-
执行上下文
EC(Execution Context):- 全局执行上下文
ECG(Execution Context Global)- 在执行全局代码之前将widnow确定为全局执行上下文
- 对全局对象进行预处理收集变量:
- var定义的全局变量在此时的值为undefined,添加为window的属性
- function声明的全局函数,直接赋值为定义的函数对象,添加为window的方法
- 全局执行上下文中的this赋值为window
- 开始执行全局代码
- 函数执行上下文
EC(Execution Context)- 在调用函数,准备执行函数体之前,创建对应的函数执行上下文(虚拟的,存在于栈内存中的一块区域)
- 对函数局部数据进行预处理收集变量:
- 行参变量 => 赋值(实参) => 添加为执行上下文的属性
- var定义的局部变量在此时的值为undefined,添加为执行上下文的属性
- function声明的局部方法,直接赋值为定义的函数对象,添加为执行上下文的方法
- this => 赋值(调用函数的对象)
- 开始执行函数体代码
- 块级执行上下文
EC(Execution Context)上下文中存储着当前代码运行的VO(Varibale Object)变量对象,用来存放创建的变量和值,并且每个执行上下文中都有一个自己的变量对象VO,在函数私有上下文中也叫做AO(Activation Object)活动对象,但也是变量对象。
- 全局执行上下文
-
执行上下文栈
ECStack(Execution Context Stack):- 在全局代码执行之前,JS引擎就会创建一个栈来存储管理所有的执行上下文对象
- 在全局执行上下文(window)确定后,将其添加到栈中(压栈/进栈)
- 在函数执行上下文创建后,将其添加到栈中(压栈/进栈)
- 在当前函数执行完后,将栈顶的执行上下文对象移除(出栈)
- 当所有的代码执行完后,栈中只剩下全局执行上下文(window)对象
当浏览器开始执行JS代码之前:
-
开辟一个栈内存(Stack)空间,供之后的代码运行;
-
开辟一个堆内存空间,将浏览器环境的全局对象
GO(Global Object)存储起来,并且在浏览器中,将window指向这个全局对象。这个对象里面存储这浏览器的全局属性和方法,比如Math对象、setTimeout方法... -
创建
ECG全局执行上下文。 -
形成的ECG全局执行上下文,进入到栈内存中执行,这个过程叫做进栈,全局执行上下文在程序关闭才会清理,而函数执行上下文和块级执行上下文在代码执行完成之后,回从栈内存中移除,叫做出栈。
开始执行代码:
- 基本数据类型:
- 基本类型的值都是直接存储在栈内存中的
- 声明一个基本数据类型:
var a = 12;,依次执行一下三步:- 创建一个值 12
- 创建一个变量 a
- 将变量和值关联起来 变量a 指向 数值12
- 将变量a的值赋给b,并修改变量b的值
- 引用数据类型:
- 引用类型的值都是存储在新开辟的堆空间中
- 声明一个引用类型:
var a = {n: 12};,依次执行一下三步:- 创建一个堆内存,内存的地址为一个十六进制的地址AAAFFF000
- 把键值对存储在堆内存中
- 将堆存地址放在栈中,共变量使用
- 将变量和值关联起来 变量a 指向 堆内存的地址AAAFFF000
- 将变量a的值赋给b,并修改变量b的值
var b = a; b = {n: 13};,- 创建一个变量 b
- 将变量b指向变量a指向的地址AAAFFF000
b = {n: 13}会有一个新的对象产生,就会再创建一个堆内存,内存的地址为一个十六进制的地址AAAFFF111- 将变量b指向新的堆内存地址AAAFFF111
var obj = {
name: '小明',
say: (function(name){
cobsole.log(`my name id ${name}`)
})(obj.name) // 在给赋值的时候,是把子执行函数执行的结果赋值给say属性
}
// 执行结果:报错 Uncaught ReferenceError: name is not defined
// 分析:JS创建一个变量
// 1. 第一步创建值:
// + 开辟一个堆空间
// + 存储键值对
// name: '小明'
// say: 子执行函数执行,需要读取obj.name的值当作实参传入,此时还没有关联obj,obj为undefined,报错
5. H5的多线程Web Worker
-
H5规范提供了js分线程的实现,取名为Web Workers。
- HTML5提供的一个javascript多线程解决方案
- 可以将一些大计算量大代码交由Web Worker运行而不阻塞用户界面的操作
- Web Worker子线程完全受主线程控制,且不能操作DOM,所以这个新标准并没有改变javascript单线程的本质
-
图解
-
相关api:
- Worker:构造函数,加载分线程执行的js文件
- Worker.prototype.onmessage: 用于接受另一个线程的回调函数
- Worker.prototype.postmessage: 用于向另一个线程发送消息
-
不足:
- Worker内代码不能操作DOM(不能更新UI),全局变量不是window,是Worker的一个实例,没法访问到DOM
- 不能跨域加载js
- 不是每个浏览器都支持这个新特性
-
使用demo
// main.js var input = document.getElementById('input') document.getElementById('btn').onclick = function () { var number = input.value var worke = new Worker('./worker.js'); worke.onmessage = function (event) { console.log('主线程接受分线程返回的数据:' + event.data) alert(event.data) } // 向分进程发送消息 worke.postMessage(number){ console.log('主线程接向分线程发送的数据:' + number) } } // worker.js function fibonacci (n) { return n <= 2 ? 1 : fibonacci(n-1) + fibonacci(n-2) } console.log(this) // 不是window,是Worker的实例 var onmessage = function (event) { var number = event.data console.log('分线程接受主线程发送过来的数据:' + number) var result = fibonacci(number) postMessage(result) console.log('分线程向主线程返回数据:' + result) // alert(result) 报错,在分线程中的全局对象不是widnow,没有alert方法,所以也不能操作DOM }
三、 JS常用功能函数的实现
1. JS原生操作符和函数的模拟实现
-
new 操作符的模拟实现
- new一个对象做的事情:
- 创建一个空对象
- 给对象设置__proto__的值为当前构造函数对象的prototype属性值
- 以当前对象执行构造函数的函数体(给对象设置属性和方法)
- 返回这个对象
- 模拟实现
function myNew (constructor, ...agrs) { if (typeof constructor !== 'function') { throw Error(`${constructor} is not a constructor`) } var obj = new Object(); obj.__proto__ = constructor.prototype; constructor.call(obj, ...agrs); return obj; } // 便捷写法 function _new(fn, ...arg) { const obj = Object.create(fn.prototype); const ret = fn.apply(obj, arg); return ret instanceof Object ? ret : obj; }
- new一个对象做的事情:
-
call 和 apply 的模拟实现
- call 和 apply 都是指定函数的this来调用函数,知识参数不一样而已
var myName = 'window' var obj = { myName: 'objName', sayName() { // 这里的sayName一般都是使用obj.sayName来调用的 // this就指向当前的obj对象,this.myName为'objName' console.log(this.myName) } } var bar = { myName: 'bar', // 将obj.sayName函数赋值给bar对象foo属性 // 此时使用bar.foo()来调用,sayName方法里面的this指向bar,,this.myName为'barName' foo: obj.sayName } bar.foo()- 模拟实现
// call的实现 Function.prototype.myCall = function (ctx, ...args) { var symbolFn = new Symbol() ctx[symbolFn] = this; ctx[symbolFn](...ages) delete ctx[symbolFn] } // apply的实现 Function.prototype.myApply = function (ctx, parmsArr) { var symbolFn = new Symbol() ctx[symbolFn] = this; ctx[symbolFn](...parmsArr) delete ctx[symbolFn] }
-
bind的模拟实现
- bind函数功能:
- bind 只是改变了fn中的this并返回一个新的函数,并没有执行函数
- 执行bind后会有一个返回值,这个返回值就是bind之后fn改变this的结果
- 模拟实现
Function.prototype.bind = function (ctx, ...bindArgs) { var that = this; return function (...callArgs) { that.call(ctx, ...bindArgs, ...callArgs) } }
- bind函数功能:
2. 防抖和节流函数
- 概念说明:
- 函数节流和函数防抖,两者都是优化高频率执行js代码的一种手段。
- 函数防抖就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。通常用在输入框出发input方法的时候,只有超过一定的时间没有再次输入才执行回到,否则重新计时
- 函数节流是指高频事件触发,但在n秒内只会执行一次,所以节流会稀释函数的执行频率。通常用在点击按钮时,一定的时间内只有第一次点击生效
- 代码实现:
-
防抖函数
// 非立即执行版 // 触发事件后函数不会立即执行,而是在 n 秒后执行,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。 function debounce (callback, times) { const timer = null; return function (...args) { const content = this; // 这里没有用箭头函数,可以不存储this直接调用即可 if (timer) clearTimeout(timer) // 清除计时 // 重新计时 timer = setTimeout(() => { callback.apply(content, args) }, times) } } // 立即执行版 // 触发事件后函数会立即执行,然后 n 秒内不触发事件才能继续执行函数的效果。 function debounce(func,wait) { let timeout; return function (...args) { if (timeout) { clearTimeout(timeout) // 清除定时器的事件,timeout依然还是有值的 } else { // 第一次点击timer是null会执行 method(...args) } timeout = setTimeout(function () { timeout = null; }, times) } } -
节流函数
// 1. 时间戳方式 防抖操作是立即执行的 function throttle1 (fn, times) { let time = 0 return function () { const now = Date.now() if (now - time >= times) { time = now fn.apply(this, arguments) } } } // 2. 定时器方式 非立即执行 function thorttle2 (fn, times) { let timer = null return function () { const ctx = this const args = arguments if (timer) { clearTimeout(timer) } timer = setTimeout(() => { fn.apply(ctx, args) }, times) } } // 3. 定时器标记方式 立即执行 function thorttle3 (fn, times) { let flag = false return function () { if (flag) return flag = true fn.apply(this, arguments) setTimeout(() => { flag = false }, times) } } // 4. 定时器标记方式 非立即执行 function thorttle4 (fn, times) { let flag = false return function () { const ctx = this const args = arguments if (flag) return flag = true setTimeout(() => { flag = false fn.apply(ctx, args) }, times) } }// 上面在非立即执行的时候,在最后一次触发的时候,也要等待指定的时间才会执行 // 结合定时器和时间错的方式 完成一个尽量精确的节流函数 function thorttle5 (fn, times) { let timer = null let start = Date.now() return function (...args) { const current = Date.now() const remaining = times - (current - start) const ctx = this clearTimeout(timer) if (remaining <= 0) { fn(args) start = Date.now() } else { timer = setTimeout(() => { fn.apply(ctx, args) }, remaining) } } }
-
3. 广度优先遍历 和 深度优先遍历
对于算法来说 无非就是时间换空间 空间换时间
- 深度优先不需要记住所有的节点, 所以占用空间小, 而广度优先需要先记录所有的节点占用空间大
- 深度优先有回溯的操作(没有路走了需要回头)所以相对而言时间会长一点 待处理的树结构数据:
var tree = [{
name: '1',
children: [{
name: '1-1',
children: [{
name: '1-1-1'
}, {
name: '1-1-2'
}]
}, {
name: '1-2',
children: [{
name: '1-2-1'
}]
}]
}, {
name: '2',
children: [{
name: '2-1'
}, {
name: '2-2',
children: [{
name: '2-2-1'
}]
}]
}]
- 广度优先遍历:创建一个执行队列, 当队列为空的时候则结束
function bfs (data) {
var result = [];
var queue = data;
while (queue.length) {
[...queue].forEach(item => {
queue.shift()
result.push(item.name);
if (item.children) {
queue.push(...item.children)
}
})
}
return result.join(',')
}
- 深度优先遍历: 使用递归
function dfs (data) {
data.forEach(item => {
console.log(item.name) // 深度遍历读取每一个数据
if (item.children) {
dfs(item.children)
}
})
}
function dfs (data) {
var result = []
data.forEach(item => {
const map = (data) => {
result.push(data.name); // 深度遍历读取顺序存贮每一个数据
if (data.children) {
data.children.forEach(child => map(child))
}
}
map(item)
})
return result.join(',')
}
4. 数组的常见处理操作
var arr = [1, 2, {a: 1}, ['a', 'b'], 1, true, 'str', {a: 2}, 'str', true, undefined, undefined, NaN, NaN]
- 数组去重
- 利用Set数据结构
arr = [...new Set(arr)]
arr = Array.from(new Set(arr)) - 使用双重for循环,再利用数组的splice方法去重
function unique (arr) { let len = arr.length; for (let i = 0; i < len; i++) { for (let j = i +1; j < len; j++) { // 这里的===比较不能比较出NaN if(arr[i] === arr[j] || (typeof arr[i] === 'Number' && typeof arr[j] === 'Number' && isNaN(arr[i]) && isNaN(arr[j])) ) { arr.splice(i, 1) len--; j-- } } } return arr } - 利用数组的indexOf方法去重
function unique (arr) { let result = []; for (let i = 0; i < arr.length; i++) { // 同样对多个NaN不能过滤 if (result.indexOf(arr[i]) === -1) { result.push(arr[i]) } } return result; } - 利用数组的includes去重
function unique (arr) { let result = []; for (let i = 0; i < arr.length; i++) { // 同样对多个NaN不能过滤 if (!result.includes(arr[i])) { result.push(arr[i]) } } return result; } - 利用数组的filter方法去重
function unique (arr) { // NaN会被全部过滤掉 return arr.filter((item, index) => { return arr.indexOf(item) === index }) } - 利用ES6中的Map方法去重
// 创建一个空Map数据结构,遍历需要去重的数组,把数组的每一个元素作为key存到Map中。 // 由于Map中不会出现相同的key值 function unique (arr) { const map = new Map(); const result = []; for (let i = 0; i < arr.length; i++) { // NaN在map数据结构中是同一个key,可以正确过滤重复的NaN if (!map.has(arr[i])) { map.set(arr[i], true); result.push(arr[i]) } } return result; }
- 利用Set数据结构
- 数组排序算法
- 类数组转化为数组
const domList = document.querySelectAll('div'); // 1. 扩展运算符 const arr = [...domList] // 2. Array.from const arr = Array.from(domList) // 3. Array.slice const arr = Array.prototype.slice.call(domList) // 3. Array.concat const arr = Array.prototype.concat.apply([], domList) - 数组扁平化
const arr = [1, [2, [3, [4, 5]]], 6]
// 1. 使用flat
cosnt result = arr.flat(Infinity);
// 2. 利用正则 (问题:数组的每一项都会被转换成字符串,同样存在JSON序列化的问题)
cosnt result = JSON.stringify(arr).replace(/\[|\]/g,'').split(',')
// 改进: (问题:同样存在JSON序列化的问题)
cosnt result = JSON.parse('[' + JSON.stringify(arr).replace(/\[|\]/g,'') + ']')
// 3. 递归使用reduce
const flatten = arr => {
return arr.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? flatten(cur) : cur)
}, [])
}
// 4. 循环遍历函数递归
const flatten = (function () {
let result = []
return function loop (arr) {
for(let i = 0; i < arr.length; i++) {
const item = arr[i];
if(Array.isArray(item)) {
loop(item)
} else {
result.push(item)
}
}
return result
}
})()
5. 对象(循环引用)的深浅拷贝
拷贝的对象数据:
const obj = {
a: 100,
b: [100,200,300],
c: {x: 1, y: 2},
d: /^\d+$/,
e: function a () {},
f: new Date(),
g: undefined,
h: null,
i: Symbol(1),
j: ['asd', {a: 2}, function(){}, undefined, null, Symbol(2)],
k: BigInt(2)
}
- 浅拷贝:多层的引用类型对象只克隆第一层,第二层的引用类的对象只是拷贝引用对象的地址值
- 方法一:for in循环遍历对象
const obj2 = {}; for(let key in obj) { if (obj.hasOwnProperty(key)) { obj2[key] = obj[key] } }- 方法二:ES6的展开运算符in循环遍历对象
const obj2 = {...obj}- 方法三:ES6的Object.assign()
const obj2 = Object.assign({}, obj) - 深拷贝:多层的引用类型递归进行拷贝,所有层级的引用类型对象都是递归进行数据值的拷贝
- 方法一:JSON转换
const obj2 = JSON.parse(JSON.stringfy(obj)); // 这种方式存在的问题: // 1. 对象的属性值为函数(不能序列号函数)、Symbol、undefined会被直接忽略去掉 // 2. 正则对象会被变成空对象 // 3. 时间对象Date会变成对应的时间字符串(拷贝时间对象,可以将时间对象转为数值或字符串就好) // 4. 不能解决循环引用的对象 // 5. 数组中的数组项如果是函数、symbol值、undefined,会被转换为null // 6. BigInt类型的属性会导致报错:Do not know how to serialize a BigInt- 方法二:递归遍历拷贝
function deepClone (obj) { // 递归时特殊情况的处理 // 1. 不是对象类型的数据,基本数据(值)类型,知节返回当前数据即可 if (typeof obj !== 'object' || obj === null) { return obj } // 2. 是正则的数据类型,重新创建一个正则实例返回 if (obj instanceof Regexp) { return new Regexp(obj) } // 3. 是时间Date对象的数据类型,重新创建一个Date实例返回 if (obj instanceof Date) { return new Date(obj) } // 4. 是函数对象的数据类型,重新创建一个Function实例返回 if (obj instanceof Function) { return new Function(obj) } // 让克隆的对象和原对象拥有一样的所属类构造函数和原型属性 const obj2 = new obj.constructor; // 对象类型的数据,遍历递归拷贝 for (let key in obj) { if (Obj.hasOwnProperty(key)) { obj2[key] = deepClone(obj[key]) } } return obj2 } // 这种方式存在的问题: // 1. 不能解决循环引用的对象
四、 DOM/BOM及事件处理机制
五、计算机网络
1. DNS
- DNS(Domain Name Serve) 域名服务器
- 作用:域名与对应的ip转换的服务器
- 特征:
- DNS中保存着一张域名于对应的ip地址的表
- 一个域名对应一个IP地址。一个IP地址可以对应多哥域名
- gTLD: generic Top-Level DNS Server 顶级域名服务器 为所有 .com、.net......后缀做顶级域名解析的服务器
- DNS解析过程:访问 www.baidu.com 的过程
- 浏览器询问DNS本地服务器,这个域名的IP是多少?
- 如果DNS本地服务器缓存中已存在,直接返回这个IP地址,如果没有就去询问根服务器.;
- 根服务器.有记录就直接返回,没有就会让其找.com的顶级域服务器;
- 顶级域服务器.com如果有记录就直接返回,没有就让那其找baidu.com的域服务器,
- 域服务器baidu.com如果有记录就直接返回,通常的域名备案之后,服务上的DNS映射表中肯定会有这条记录,就回返回这个域名对应的ip地址。
- IP
- IP(Internet Protocol Address) 互联网协议地址/IP地址
- 分配给用户上网使用的互联网协议
- 分类:IPv4、IPv6
- 形式: IPv4:192.6.20.223(长度32位(4个字节),十进制表示) IPv6:AC23:EF01:2345:ABCD:HG55:IKUH:7JU8:Q2W8 (8组长度128位,十六进制表示)
- IPv6的优势:
- IPv6地址空间更大
- 路由表更小
- 组播支持以及对流支持更强
- 对自动配置的支持
- 更高的安全性
- IP端口号PORT
- IP地址:IPv4 或 IPv6 (上海迪士尼乐园地址)
- 端口号:80、443... (迪士尼乐园的不同游乐设施)
- 解释:找到一个IP就像找到了上海迪士尼乐园的地址,就可以到乐园,相当于可以访问到IP对应的服务器,IP加端口号,相当于到乐园去玩不同的项目,每个项目其实就是对应一个端口号。
- 总结:每一个端口号对应的是一个服务器的一个业务,访问一个服务器的不同端口就相当于访问不同的业务
- 端口范围:0 ~ 65534
- 默认端口:http协议下(80)、https协议下(443)、FTP协议下(20、21)
- 根据展示的端口不同,服务端的业务也不同
- 对比:
- IP:上海市浦东新区
- 域名:上海迪士尼乐园
- 端口:乐园海盗船的入口
- TCP
- TCP(Transmission Control Protocal) 传输控制协议
- 特点:面向连接(收发数据前,必须建立可靠的连接)
- 建立可靠连接的基础:三次握手
- 应用场景:数据必须准确无误的收发
HTTP请求、FTP文件传输、邮件收发 - 优点:稳定、重传机制、拥塞控制机制、断开链接
- 缺点:速度慢、效率低、占有资源、容易被攻击(三次握手 => DOS、DDOS攻击)
- TCP/IP协议组:通过点对点的链接机制,制定了数据封装、定址、传输、路由、数据接受的标准
- UDP
- UDP(User Data Protocol) 用户数据协议
- 特点:面向无连接(不可靠的协议,不必须建立可靠的连接)
- 没有链接信息发送机制
- 应用场景:无须准确通讯质量且要求速度快、无需确保信息完整
消息收发、语音通话、直播(QQ) - 优点:安全、快速、漏洞少(UDP flood攻击)
- 缺点:不可靠、不稳定、容易丢包
- 总结:只要目的源地址、端口号、地址值、端口号正确,则可以i直接发送信息报文,但不能保证一定能接收到或收到完整的数据
- HTTP与HTPTPS
- HTTP(HyperText Transfer Protocol) 超文本传输协议
- 定义:客户端和服务器端请求和应答的标准,用于从WEB服务器传输超文本道本地浏览器的传输协议
- HTTP请求:按照协议规则向WEB服务器发送的将超文本传输到本地浏览器的请求
- HTPTPS(HyperText Transfer Protocol Secure) 超文本传输安全协议
- HTTP的安全版本(安全基础是SSL/TLS)
- SSL(Secure Sockets Layer) 安全套接层
- TLS(Transport Layer Security) 传输层安全
- 为网络通讯提供安全及数据完整性的一种安全协议,对网络链接进行加密
- 区别:
- HTTP是不安全的(监听和中间人攻击等手段,获取网络账户信息和敏感信息),而HTTPS是可以防止被攻击的
- HTTP协议的传输内容都是明文,直接在TCP链接上运行,客户端和服务器都无法验证对方身份;HTTPS协议的传输内容都被SSL/TLS加密,且运行在SSL/TLS上,SSL/TLS运行在TCP连接上,所以数据是安全的
- HTTP(HyperText Transfer Protocol) 超文本传输协议
- 建立TCP连接的前奏
- 标识位: 数据包
- SYN:Synchronize Sequence Numbers 同步序列编号
- ACK:Acknowledgement 确认字符
- LISTEN:侦听TCP端口的连接请求(我等着你发送连接请呢)
- SYN_SENT:在发送连接请求后等待匹配的连接请求(我发送了连接请求,我等你回复哈)
- SYN_RECEIVED:在收到和发送一个连接请求后等待对连接请求的确认(我收到你的连接请求了哈,我等你回复)
- ESTABLISHED:代表一个打开的连接,数据可以传送给用户(连接建立了哈,我跟你说一下)
- 三次握手的说明:
- 第一次握手:客户端向服务器发送SYN标志位(序号是J),并进入SYN_SEND状态(等待服务器确认状态)
- 第二次握手:服务器收到来自客户端的SYN J,服务端会确认该数据包已收到并发送ACK标识位(序号是J+1)和SYN标识位(序号是k),服务器进入SYN_RECEIVED(请求接受并等待客户端确认状态)
- 第三次握手:客户端进入连接建立状态后,向服务器发送ACK标志位(序号是k+1)确认客户端已收到建立确认,服务器收到ACK标志位后,服务器进入连接已建立状态
2. 浏览器工作原理
参考:《how browsers work》blog.csdn.net/zzzaquarius…
2. 浏览器缓存
参考:www.cnblogs.com/suihang/p/1…
浏览器缓存(Browser Caching)是为了节约网络的资源加速浏览,浏览器在用户磁盘上对最近请求过的文档进行存储,当访问者再次请求这个页面时,浏览器就可以从本地磁盘显示文档,这样就可以加速页面的阅览。
浏览器缓存(Browser Caching)其实就是浏览器保存通过HTTP获取的所有资源,是浏览器将网络资源存储在本地的一种行为。浏览器的缓存机制是根据HTTP报文的缓存标识进行的。
通常浏览器缓存策略分为两种:强缓存( Expires,cache-control)和协商缓存(Last-modified ,Etag),并且缓存策略都是通过设置 HTTP Header 来实现的。
-
强制缓存
- Expires: 是Response Header里设置的过期时间节点,浏览器再次加载资源时,如果在这个过期时间内,则命中强制缓存,并返回304。
- Cache-Control: 当值设为max-age=300时,则代表在这个请求正确返回时间(浏览器也会记录下来)的5分钟内再次加载资源,就会命中强缓存,返回的Status Code: 200 OK (from memery cache)。
- 区别:
- Expires 是http1.0的产物,Cache-Control是http1.1的产物
- 两者同时存在的话,Cache-Control优先级高于Expires;
- 在某些不支持HTTP1.1的环境下,Expires就会发挥用处。所以Expires其实是过时的产物,现阶段它的存在只是一种兼容性的写法
- Expires是一个具体的服务器时间,这就导致一个问题,如果客户端时间和服务器时间相差较大,缓存命中与否就不是开发者所期望的。Cache-Control是一个时间段,控制就比较容易
-
协商缓存
- ETag和If-None-Match:Etag是上一次加载资源时,服务器返回的response header,是对该资源的一种唯一标识,只要资源有变化,Etag就会重新生成。浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的Etag值放到request header里的If-None-Match里,服务器接受到If-None-Match的值后,会拿来跟该资源文件的Etag值做比较,如果相同,则表示资源文件没有发生改变,命中协商缓存,返回的Status Code: 304。
- Last-Modified和If-Modified-Since: Last-Modified是该资源文件最后一次更改时间,服务器会在response header里返回,同时浏览器会将这个值保存起来,在下一次发送请求时,放到request header里的If-Modified-Since里,服务器在接收到后也会做比对,如果相同则命中协商缓存,返回的Status Code: 304。
- 区别:
- 在方式上,Etag是对资源的一种唯一标识,而Last-Modified是该资源文件最后一次更改时间
- 在精确度上,Etag要优于Last-Modified。Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的Last-Modified也有可能不一致。
- 在性能上,Etag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值。
- 在优先级上,服务器校验优先考虑Etag。
-
浏览器缓存过程
- 浏览器第一次加载资源,服务器返回200,浏览器将资源文件从服务器上请求下载下来,并把response header及该请求的返回时间一并缓存;
- 下一次加载资源时,先比较当前时间和上一次返回200时的时间差,如果没有超过cache-control设置的max-age,则没有过期,命中强缓存,不发请求直接从本地缓存读取该文件(如果浏览器不支持HTTP1.1,则用expires判断是否过期);如果时间过期,则向服务器发送header带有If-None-Match和If-Modified-Since的请求;
- 服务器收到请求后,优先根据Etag的值判断被请求的文件有没有做修改,Etag值一致则没有修改,命中协商缓存,返回304;如果不一致则有改动,直接返回新的资源文件带上新的Etag值并返回200;
- 如果服务器收到的请求没有Etag值,则将If-Modified-Since和被请求文件的最后修改时间做比对,一致则命中协商缓存,返回304;不一致则返回新的last-modified和文件并返回200;
-
缓存存储位置
从存储位置来看,浏览器缓存一共分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络。- 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 中获取的内容。
-
- Memory Cache
- Memory Cache 也就是内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。
- 内存缓存在缓存资源时并不关心返回资源的HTTP缓存头Cache-Control是什么值,同时资源的匹配也并非仅仅是对URL做匹配,还可能会对Content-Type,CORS等其他特征做校验。
- Disk Cache
- Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。 它会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。绝大部分的缓存都来自 Disk Cache。
- Push Cache
- Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在Chrome浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令。他有如下的一些特性:
- 所有的资源都能被推送,并且能够被缓存,但是 Edge 和 Safari 浏览器支持相对比较差。
- Push Cache 中的缓存只能被使用一次
- 可以给其他域名推送资源
- 浏览器可以拒绝接受已经存在的资源推送
- 一旦连接被关闭,Push Cache 就被释放
- 可以推送 no-cache 和 no-store 的资源
- 多个页面可以使用同一个HTTP/2的连接,也就可以使用同一个Push Cache。这主要还是依赖浏览器的实现而定,出于对性能的考虑,有的浏览器会对相同域名但不同的tab标签使用同一个HTTP连接。
- Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在Chrome浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令。他有如下的一些特性:
- Service Worker
-
用户行为对浏览器缓存的控制
- 地址栏访问,链接跳转是正常用户行为,将会触发浏览器缓存机制;
- F5刷新,浏览器会设置max-age=0,跳过强缓存判断,会进行协商缓存判断;
- ctrl+F5刷新,跳过强缓存和协商缓存,直接从服务器拉取资源。
-
三级缓存原理 (访问缓存优先级)
- 先在内存中查找,如果有,直接加载。
- 如果内存中不存在,则在硬盘中查找,如果有直接加载。
- 如果硬盘中也没有,那么就进行网络请求。
- 请求获取的资源缓存到硬盘和内存。
-
内存缓存和硬盘缓存有什么区别
-
1.内存缓存(from memory cache) :内存缓存主要的两个特点,分别是快速读取和时效性:
- 快速读取:内存缓存会将编译解析后的文件,直接存入该进程的内存中,占据该进程一定的内存资源,以方便下次运行使用时的快速读取。一般JS,字体,图片等会放在内存缓存中
- 时效性:一旦该进程关闭,则该进程的内存则会清空。
-
2.硬盘缓存(from disk cache) :硬盘缓存则是直接将缓存写入硬盘文件中,读取缓存需要对该缓存存放的硬盘文件进行I/O操作,然后重新解析该缓存内容,读取复杂,速度比内存缓存慢。退出进程不会清空。一般JS,字体,图片等会放在内存中,而CSS则会放在硬盘缓存中
-
-
浏览器的缓存存放在哪里,如何在浏览器中判断强制缓存是否生效?
判断是否命中强制缓存:当命中强制缓存时,状态码为200, 请求对应的Size值则代表该缓存存放的位置,分别为from memory cache 和 from disk cache。from memory cache代表使用内存中的缓存,from disk cache则代表使用的是硬盘中的缓存,浏览器读取缓存的顺序为memory > disk。 -
为什么CSS会放在硬盘缓存中?
因为CSS文件加载一次就可渲染出来,我们不会频繁读取它,所以它不适合缓存到内存中,但是js之类的脚本却随时可能会执行,如果脚本在磁盘当中,我们在执行脚本的时候需要从磁盘取到内存中来,这样IO开销就很大了,有可能导致浏览器失去响应。 -
那么既然内存缓存这么高效,我们是不是能让数据都存放在内存中呢?
这是不可能的。计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。 -
浏览器缓存知识点图谱
3. 浏览器存储
参考:www.cnblogs.com/LuckyWinty/…
- Cookie
- Cookie最开始被设计出来其实并不是来做本地数据存储的,而是为了弥补HTTP在状态管理上的不足。
- HTTP协议是一个无状态的协议,客户端向服务器发送请求,服务器返回响应,之间的通讯就这样结速了,但是下次发送请求如何让服务器知道客户端是谁呢?就需要通过Cookie。
- Cookie本质上是浏览器里面存储的一个很小的文件,内部以键值对的方式来存储(在chrome开发者面板的Application栏中可以看到)。像同一个域名下发送请求,浏览器都会自动携带相同的Cookie,服务器拿到Cookie进行解析,便能拿到客户端的状态和信息。
- 生效时间:在设置的cookie过期时间之前一直有效
- Cookie的作用就是用来做状态存储的,但也有诸多致命的缺陷:
- 容量缺陷:Cookie的体积上线只有4kb,只能用来存储少量的信息
- 性能缺陷:Cookie紧跟域名,不管这个域名下的某个地址是否需要这个Cookie,请求都会自动携带上完整的Cookie请求头,这样随着请求数量的增多,其实会造成巨大的性能浪费,因为请求携带了很多不必要的内容
- 安全缺陷:Cookie以纯文本的形式在浏览器和服务器中传递,很容易被非法用户截获,然后进行一些列的篡改,在Cookie的有效期内重新发送给服务器,服务器无法是被是否是正常用户的请求,这是危险的。另外,在HttpOnly为false的情况下,Cookie信息能直接通过JS脚本来读取。
- WebStorage
- localStorage
- localStorage跟Cookie一样,也是针对同一个域名,即在同一个域名下,会存储相同的一段localStorage。
- 特点:
- 容量:localStorage的容量上限为5M,相比于Cookie的4kb大大增加。这5M空间的数据是针对这个域名的,因此对于一个域名是持久存储的,浏览器关闭后数据不会丢失,除非主动删除数据;
- 只存在客户端,默认不参与与服务器的通讯,这样就很好的避免了Cookie带来的新能问题和安全问题;
- 接口封装,通过localStorage暴露在全局,并通过它的setItem和getItem等方法进行操作,非常方便;
- 操作方式:
let obj = { name: 'zhangsan', age: 18 }; localStorage.setItem('name', 'lisi'); localStorage.setItem('userInfo', JSOn.stringify(obj)); // 进入相同的域名时就能获取到相应的值: let name = localStorage.getItem('name'); // 'lisi' let userInfo = JSON.parse(localStorage.getItem('userInfo')); // { name: 'zhangsan', age: 18 } - localStorage实际存储的都是字符串,如果是存储对象需要调用JSON的stringify方法,并且在获取时使用JSON.parse()来解析成对象
- sessionStorage
- sessionStorage一下方面和localStorage完全一致:
- 容量,容量上限也是5M
- 只存在于客户端,默认不参与与服务器的通讯
- 接口封装,除了sessionStorage名字有所变化,操作方式均和localStorage一样
- sessionStorage和localStorage本质的区别:
- sessionStorage是会话级别的存储,并不是持久化的存储。绘画结束,也就是标签页关闭,这部分的sessionStorage就不复存在。而localStorage存储的数据是持久的,标签/浏览器关闭后数据都不会丢失。
- sessionStorage 数据在页面会话结束时会被清除。页面会话在浏览器打开期间一直保留,并且重新加载或恢复页面仍会保持原来的页面回话。在新标签或窗口打开一个页面时会在顶级浏览器上下文中初始化一个新的会话。
- 应用场景
- 用它对表单信息进行维护,将表单信息存储在里面,可以保证页面即使刷新也不会让之前的表单信息丢失;
- 用它来存储本次浏览记录,如果关闭页面后不需要这些记录,用sessionStorage就在合适不过了
- sessionStorage一下方面和localStorage完全一致:
- localStorage
- IndexDB
- IndexDB 是运行在浏览器中的非关系型数据库,本质上就是数据库,绝不是和上面WebStorage的5M一个量级,理论上这个容量是没有上限的,是被纳入HTML5标准的数据库存储方案
- 特点:拥有数据库本身的特征,比如支持事物,存储二进制数据,还有一下几点:
- 键值对存储,内部采用对象仓库存放数据,在这个对象仓库中数据采用键值对的方式存储
- 异步操作,数据库的读写属于I/O操作,浏览器中对异步I/O提供了支持
- 受同源策略限制,即无法访问跨域的数据库 总结:
- cookie和localstorage,sessionstorage的区别:
- 作用域:
- sessionStorage只在同源的同窗口的同标签页中共享数据,也就是只能在当前回话中共享
- localStorage在所有同源窗口中都是共享的。
- Cookie在所有同源窗口中是共享的
- IndexDB在所有同源窗口中是共享的
- 使用:
- cookie不适合存储,而且存在非常多的缺陷,仅限需要和服务器通讯时使用
- WebStorage包括 localStorage 和 sessionStorage,默认不会参与和服务器的通讯,可根据不同的场景存储数据
- IndexDB 为运行在浏览器上的非关系型数据库,为大型数据的存储提供了接口
4. 浏览器同源策略及跨域
- 浏览器上下文
- 是浏览器展示文档的环境,如 每个tab标签页、也可以是页面的window 或者 页面的一部分iframe。实质上就是展示一段资源的一个标签,一个页面中可以有多个上下文。
- 浏览器上下文包括:特殊源、活动文档源(orange)、展示文档的历史
- 源:指活动文档源(orange),每一个标签都有一个源
- 浏览器同源策略
- 同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说 Web 是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。
- 它的核心就在于它认为自任何站点装载的信赖内容是不安全的。当被浏览器半信半疑的脚本运行在沙箱时,它们应该只被允许访问来自同一站点的资源,而不是那些来自其它站点可能怀有恶意的资源。
- 如果两个 URL 的 协议protocol、 主机host 和 端口port (如果有指定的话)都相同的话,则这两个 URL 是同源,即所谓同源是指:域名、协议、端口相同。这个方案也被称为“协议/主机/端口元组”,或者直接是 “元组”。(“元组” 是指一组项目构成的整体,双重/三重/四重/五重/等的通用形式)。
- 下表给出了与 URL
http://store.company.com/dir/page.html的源进行对比的示例:
- 下表给出了与 URL
- 有哪些是不受同源策略限制?
- 页面上的链接,比如 a 链接
- 重定向。
- 表单提交。
- 跨域资源的引入,比如:script, img, link, iframe。
- 跨域
- 只要两个地址之间相互通信时协议、域名、端口中有任意一个不同,就称之为跨域。同源限制是浏览器的行为,实际上双方通信是通的,但浏览器会拦截让客户端收不到服务器返回的信息。
- 请求跨域解决方案:
-
JSONP
- 是利用了script标签引用js文件不受同源策略影响的原理,通过动态创建script标签来实现的。
- 实现做法是:
- 客户端动态创建一个script标签,给其添加src属性,写上跨域url,并创建一个回调函数的querystring,比如定义为callback=myfunction, 并将该script标签添加到body上元素上
- 定义要执行的回调函数myfunction
- 服务器在接收到请求后,拿到对应资源后,通过键名callback拿到前端传递的方法名,并返回一个该回调函数执行的指令给客户端(对服务器来说,执行函数的指令是个字符串,所以不会执行)
- 客户端拿到服务器的应答时,就会执行这个回调函数,从而获取对应的资源
<!-- 客户端 --> <body> <button onclick="sendJsonp()">通过jsonp解决跨域</button> <script> // 1. JSONP function sendJsonp() { const script = document.createElement('script') // 通过querystring的方式,传递一个回调函数的参数, //这个回调参数的键值是前后端一起定义的并保持一致的 script.src = "http://localhost:4000/jsonpData?callback=customFunc" document.body.appendChild(script) } // 自定义的函数名,即使用jsonp传递给后台的回调函数名 function customFunc(res) { console.log(res) // 1. 200; 2. {name: "hahha", age: 3} } </script> </body>// 服务器端 const Koa = require("koa") const Router = require("koa-router") const app = new Koa() const router = new Router() router.get('/jsonpData', ctx => { const callback = ctx.query.callback let obj = { name: 'hahha', age: 3 } let objStr = JSON.stringify(obj) // 注意,如果参数直接传objStr,客户端会认为这是一个变量,会报未找到异常 ctx.body = `${callback}(${objStr})` }) app.use(router.routes()) app.listen(4000)
- 缺点
- 只支持get请求,限制了参数大小和类型
- 请求过程无法终止,导致弱网络下处理超时请求比较麻烦
- 无法捕获服务端返回的异常信息
-
CORS解决跨域
- CORS(cross-origin resource sharing),跨域资源共享,是浏览器为AJAX请求设置的一种跨域机制,让其可以在服务端允许的情况下进行跨域访问。它比jsonp更加优雅。
- 它主要是通过设置http响应头来告诉浏览器,服务端是否允许当前域的脚本进行跨域访问。
- 跨域资源共享将AJAX请求分为了两类:简单请求和复杂请求。
- 简单请求特征
- 请求方法为head、get、post
- 请求头只接受以下字段:
- Accept:浏览器能够接受的响应内容类型
- Accept-Language: 浏览器能接受的自然语言列表
- Content-Type: 请求对应的类型,只能为以下三种:
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
- Content-Language:浏览器希望采用的自然语言
- Save-Data:浏览器是否希望减少数据传输量
- 浏览器发出简单请求时,会在请求头增加一个origin字段,值为请求源的信息;
- 服务器收到请求后,根据请求头origin判断,返回相应的内容
- 浏览器收到响应后,根据响应头Access-Control-Allow-Origin进行判断,这个字段是服务端允许跨域请求的源,如果响应头没有包含这个字段或者这个响应头中的值没有包含当前源,则会抛出错误;如果有,则是允许当前源进行跨域请求。
- 复杂请求
- 只要不满足简单请求特征中的任意一条,就属于复杂请求。对于复杂请求:
- 会预先发个
options预检请求,浏览器会在请求头添加Access-control-Request-Method字段,值为跨域请求的请求方法,用于探查目标接口,允许那些请求方式; - 如果添加了不属性于简单请求的头部字段,浏览器还会添加一个
Access-Control-Request-Headers字段,值为跨域请求添加的请求头部字段 - 服务器接收到请求后,除了会返回
Access-Control-Allow-Origin的字段外,还会根据请求头,返回对应的响应头Access-control-Request-Methods和Access-Control-Allow-Headers,告诉浏览器服务端允许的源、方法和请求头字段,并返回 204 状态码。 - 浏览器得到预检请求的响应后,会判断当前请求是否在服务端的许可范围内,如果在,则继续发送跨域请求;否则,则直接报错
- 会预先发个
- 只要不满足简单请求特征中的任意一条,就属于复杂请求。对于复杂请求:
-
代理转发
- 既然同源策略是浏览器设置的安全策略,不存在于服务器。那么,我们只要不通过浏览器直接发送请求,而是通过服务器来发送请求,那么就不存在同源限制了。
- 所以我们可以把这个模式转换下:
浏览器 -> 不同源服务器 发送请求
改为:
浏览器 -> 同源服务器 -> 不同源服务器 发请求
这就是我们说的代理转发的原理。 - 在客户端使用的代理称为“正向代理”,在服务端设置的代理叫做“反向代理”。代理转发实现起来非常简单,在当前被访问的服务器配置一个请求转发规则就行了。
// 正向代理 // webpack.config.js module.exports = { //... devServer: { proxy: { '/api': 'http://localhost:3000' } } }; - 在 Nginx 服务器上配置同样的转发规则也非常简单,下面是示例配置(反向代理)。通过 location 指令匹配路径,然后通过 proxy_pass 指令指向代理地址即可。
location /api { proxy_pass http://localhost:3000; }
-
- 页面跨域解决方案
除了浏览器请求跨域之外,页面之间也会有跨域需求,例如使用 iframe 时父子页面之间进行通信。- postMessage:
HTML5 推出了一个新的函数 postMessage() 用来实现父子页面之间通信,而且不论这两个页面是否同源。// http://www.fahter.com // 父页面打开子页面 let son = window.open('http://www.son.com') // 父页面向子页面发消息 son.postMessage('I am your father', 'http://www.son.com'); // http://www.son.com // 子页面通过监听message获取父页面的消息 window.addEventListener('message', function(e) { console.log(e.data); },false); // 子页面通过window.opener.postMessage给父页面发消息 window.opener.postMessage('I am your son', 'http://www.fahter.com'); - 修改域名document.domain
-
由于JavaScript同源策略的限制,脚本只能读取和所属文档来源相同的窗口和文档的属性。
-
对于已经有成熟产品体系的公司来说,不同的页面可能放在不同的服务器上,这些服务器域名不同,但是拥有相同的上级域名,比如
id.qq.com、www.qq.com、user.qzone.qq.com,它们都有公共的上级域名qq.com。这些服务器上的页面之间的跨域访问可以通过document.domain来进行。 -
默认情况下,document.domain存放的是载入文档的服务器的主机名,可以手动设置这个属性,不过是有限制的,只能设置成当前域名或者上级的域名,并且必须要包含一个.号,也就是说不能直接设置成顶级域名。例如:id.qq.com,可以设置成qq.com,但是不能设置成com。
-
具有相同document.domain的页面,就相当于是处在同域名的服务器上,如果协议和端口号也是一致,那它们之间就可以跨域访问数据。
-
- postMessage:
5. 浏览器安全
参考:tech.meituan.com/2018/09/27/…
-
XSS
- 基本原理: XSS (Cross-Site Scripting),跨站脚本攻击是一种代码注入的攻击。通过是在用户的浏览器内运行非法的HTML标签或JavaScript进行的一种攻击。
- 攻击手段: 攻击者通常在目标网站页面里插入恶意脚本代码,当用户浏览该页面时,嵌入页面里面的恶意脚本代码会被执行。利用这些恶意脚本,攻击者可以获取用户的敏感信息如Cookie、SessionId等,从而达到盗取用户信息或其他侵犯用户安全隐私的目的,危害数据安全。
- 本质是 恶意脚本未经过滤,与网站正常的代码混在一起;浏览器无法分辨哪些脚本是可行的,导致恶意脚本被执行。由于是直接在用户的终端执行,恶意代码能够直接获取用户的信息,或者利用这些信息冒充用户向网站发起攻击者定义的请求。在部分情况下,由于输入的限制,注入的恶意脚本比较短,但是可以通过引入外部的脚本,并由浏览器执行,来完成比较复杂的攻击策略。
- XSS攻击分类
- 反射型XSS攻击 通过给被攻击者发送带有恶意脚本的URL或将不可信内容插入页面,当URL地址被打开或页面被执行时,浏览器解析、执行恶意脚本。
- 攻击步骤:
- 攻击者构造出特殊的 URL或特殊数据;
- 用户打开带有恶意代码的 URL 时,Web服务器将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器;
- 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行;
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
- 发生场景:
- 反射型XSS漏洞常见于URL传递参数的功能,如网站搜索、跳转等
- 由于需要用户主动打开恶意的URL才能生效,攻击者往往会结合多种手段诱导用户点击
- POST的内容也可以触发反射型XSS,只不过起出发的条件比较苛刻,需要构造表单提交页面,并引导用户点击,所以比较少见
- 防御:
- Web页面渲染的所有内容或数据都必须来自服务端;
- 客户端对用户输入的内容进行安全符转义,服务端对上交内容进行安全转义;
- 避免拼接html。
- 攻击步骤:
- 存储型XSS 恶意脚本被存储在目标服务器上。当浏览器请求数据时,脚本从服务器传回浏览器去执行。
- 攻击步骤:
- 攻击者将恶意代码提交到目标网站的数据库中;
- 用户浏览到目标网站时,前端页面获得数据库中读出的恶意脚本时将其渲染执行。
- 防御:
- 增加字符串的过滤:
- 前端输入时过滤;
- 服务端增加过滤;
- 前端输出时过滤。
- 发生场景:
- 常见于带有用户保存数据的网站功能,如 论坛发帖、商品评论、用户私信等
- 攻击步骤:
- DOM型XSS攻击
- 攻击步骤:
- 用户打开带有恶意代码的URL
- 用户浏览器接收到相应后解析执行,前端Javascript取出URL中的恶意代码并执行
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作
- 和前面两种XSS的区别:
- DOM型XSS攻击中,取出和执行恶意代码由浏览器端完成,属于前端JsavScript自身的安全漏洞,而其他两种XSS攻击都属于服务器端端安全漏洞
- 防御:
- 在使用.innerHtml、.outerHTML、document.write()时要特别小心,不要把不可信的数据作为HTML插入到页面上,尽可能使用.textContent、setAttribute()等
- DOM中的内联事件监听器,如location、onerror、onload、onmouseover等,a标签的href、Js的eval()、setTimeout()、setInterval()等,都能把字符串作为代码来运行。如果不科学的数据拼接到字符串中,传递给这些api,很容易造成安全隐患
- 小心使用框架的api,如vue的v-html等
- 攻击步骤:
- 反射型XSS攻击 通过给被攻击者发送带有恶意脚本的URL或将不可信内容插入页面,当URL地址被打开或页面被执行时,浏览器解析、执行恶意脚本。
- 可能注入XSS的情况
- 在HTML中内嵌的文本中,恶意脚本以script标签的形式注入;
- 在内联的JavaScript中,拼接的数据突破来原来的限制(字符串、变量、方法名等)
- 在标签属性中,恶意内容包含引号,从而突破属性限制,注入其他属性或者标签
- 在标签的href、src等属性中,包含
javascript:等可执行代码 - 在 onLoad、onerror、onclick等事件中,注入不受控制代码
- 在style属性和标签中,包含类似
background-image: url("javascript:...")的代码 或者expression(...)的CSS表达式代码。新版本浏览器已经可以防范。
- 通常有三种方式防御XSS攻击:
- Content Security Policy(CSP)。CSP 本质上就是建立白名单,开发者明确告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截是由浏览器自己实现的。我们可以通过这种方式来尽量减少 XSS 攻击。通常可以通过两种方式开启,例如只允许加载相同域下的资源:
- 设置 HTTP Header 中的 CSP(Content-Security-Policy: default-src 'self')
- 设置meta 标签的方式(<meta http-equiv="Content-Security-Policy"content="form-action 'self';">)
- 转义字符。用户的输入永远不可信任的,最普遍的做法就是转义输入输出的内容,对于引号、尖括号、斜杠进行转义
function escape(str) { str = str.replace(/&/g, '&') str = str.replace(/</g, '<') str = str.replace(/>/g, '>') str = str.replace(/"/g, '&quto;') str = str.replace(/'/g, ''') str = str.replace(/`/g, '`') str = str.replace(///g, '/') return str }-
但是对于显示富文本来说,显然不能通过上面的办法来转义所有字符,因为这样会把需要的格式也过滤掉。对于这种情况,通常采用白名单过滤的办法:
const xss = require('xss') let html = xss('<h1 id="title">XSS Demo</h1><script>alert("xss");</script>') console.log(html) <h1>XSS Demo</h1><script>alert("xss");</script>经过白名单过滤,dom中包含的
-
-
- HTTP-only Cookie: 禁止JavaScript读取某些敏感cookie,使得cookie只有http能够访问。
- Content Security Policy(CSP)。CSP 本质上就是建立白名单,开发者明确告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截是由浏览器自己实现的。我们可以通过这种方式来尽量减少 XSS 攻击。通常可以通过两种方式开启,例如只允许加载相同域下的资源:
-
CSRF
参考:tech.meituan.com/2018/10/11/…- 基本概念: CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。
- 一个典型的CSRF攻击有着如下的流程:
- 受害者登录a.com,并保留了登录凭证(Cookie)。
- 攻击者引诱受害者访问了b.com。
- b.com 向 a.com 发送了一个请求:a.com/act=xx。浏览器会默认携带a.com的Cookie。
- a.com接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求。
- a.com以受害者的名义执行了act=xx。
- 攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让a.com执行了自己定义的操作。
- XSS攻击类型
- GET类型的CSRF
- GET类型的CSRF利用非常简单,只需要一个HTTP请求,一般会这样利用:
 - 在受害者访问含有这个img的页面后,浏览器会自动向
http://bank.example/withdraw?account=xiaoming&amount=10000&for=hacker发出一次HTTP请求。bank.example就会收到包含受害者登录信息的一次跨域请求。
- GET类型的CSRF利用非常简单,只需要一个HTTP请求,一般会这样利用:
- POST类型的CSRF
- 这种类型的CSRF利用起来通常使用的是一个自动提交的表单,如:
<form action="http://bank.example/withdraw" method=POST> <input type="hidden" name="account" value="xiaoming" /> <input type="hidden" name="amount" value="10000" /> <input type="hidden" name="for" value="hacker" /> </form> <script> document.forms[0].submit(); </script> - 访问该页面后,表单会自动提交,相当于模拟用户完成了一次POST操作。
- POST类型的攻击通常比GET要求更加严格一点,但仍并不复杂。任何个人网站、博客,被黑客上传页面的网站都有可能是发起攻击的来源,后端接口不能将安全寄托在仅允许POST上面。
- 这种类型的CSRF利用起来通常使用的是一个自动提交的表单,如:
- 链接类型的CSRF
链接类型的CSRF并不常见,比起其他两种用户打开页面就中招的情况,这种需要用户点击链接才会触发。这种类型通常是在论坛中发布的图片中嵌入恶意链接,或者以广告的形式诱导用户中招,攻击者通常会以比较夸张的词语诱骗用户点击,例如:由于之前用户登录了信任的网站A,并且保存登录状态,只要用户主动访问上面的这个PHP页面,则表示攻击成功。<a href="http://test.com/csrf/withdraw.php?amount=1000&for=hacker" taget="_blank"> 重磅消息!! <a/>
- GET类型的CSRF
- 通常有三种方式防御CSRF攻击:
- 服务器验证http请求的referer头信息
- 利用ht头中的referer判断请求来源是否合法,Referer记录来改请求的来源地址
- 优点:简单易行,只需要在后台给所有安全敏感的请求统一增加一个拦截器来检查Referer的直接可以,特别是对于当前现有的系统,不需要改变当前系统的任何已有代码和逻辑,没有风险,非常便捷
- 缺点:Referer值是浏览器提供的,不可全信,低版本浏览器下Referer存在伪造的风险。用户自己可以设置浏览器使其在发送请求时不再提供Referer时,网站将拒绝合法用户的访问。
- 请求的时候传token
- 请求地址中添加token并验证。csrf之所有能够称过,是因为黑客可以完全伪造用户的请求,改请求中所有的用户验证信息都存在于cookie中。要抵御csrf。关键在于在请求中放入黑客不能伪造的信息,并且该信息不存在于cookie中。可以在http请求中已参数的形式加入一个随机产生的token,并在服务器端建立一个拦截器来验证这个token,如果请求中没有token或者token不正确,则拒绝请求。
- 优点:比检查Refrere要安全一些,token可以在用户的登录后产生并放于session中,然后每次请求时把token从session中拿出来,与请求中的token进行对比
- 缺点:对所有请求都添加token比较困难,难以保证token本身的安全,依然会被利用获取到token。通常可以将这个toke以自定义参数的形式放在HTTP请求头的自定义属性中,一次性给所有该类请求加上csrfToken
- 加验证码
- 短信验证码
- 图形验证码
- 服务器验证http请求的referer头信息
AST语法解析 词法解析 变量提升
var x=y=10;
内部步骤:
1. y=10
2. var x=10
所以这个y没有带var声明,是一个全局的变量
var num = 15;
var num; // 重新声明的变量,又没有给其赋值,是无效的。变量还是等于之前的值
console.log(num) // => 15 不是undefined
// 阿里 面试题
let a = {n: 1};
let b = a;
// 连等于的执行顺序,从左往右将最后面的值赋值给每一个参数
a.x = a = {n: 2}; // 等价于: a.x = {n: 2} a = {n: 2}
console.log(a) // {n: 2}
console.log(b) // {n: 1, x: {n: 2}}
HTML script标签的 saync 和 defer属性作用 requestAnimationFrame API
浏览器是如何运作的?