JavaScript
浏览器分成了两个部分:渲染引擎和js引擎
- 渲染引擎:用来解析HTML与CSS,俗称内核,比如chrome浏览器的blink,老版本的webkit
- JS引擎:俗称JS解释器,用来读取网页中JavaScript代码,对其处理后运行,比如chrome浏览器的V8
JS的组成:
- ECMAScript:javascript语法
- DOM:页面文档对象模型
- BOM:浏览器对象模型
数据类型的分类:
- 总:USONB(U: undefined; S: string, symbol; O: object; N: null, number; B:boolean)
- 简单数据类型: undefined、null、boolean、number、string
- 复杂数据类型:Object、Array、Date
- 简单数据类型在存储变量中存储的是值本身,而复杂数据类型(引用类型)在存储变量时存储的仅仅是地址
- 简单数据类型变量的值直接存放在栈空间中,而引用类型变量栈空间存放的是地址,真正的对象实例存放在堆空间中
数据类型的隐式转换
// 转换成string:+(字符串连接符) +=(后面跟着一个字符串)
let num = 1, bool = true
console.log( 1 + '1' ) // '11'
console.log( 1 + 'true' ) // '1true'
console.log( num + '' ) // '1'
console.log( bool += '1') // 'true1'
// 转换成number:++/--(自增自减运算符) +-*/%(算数运算符)> < >= <= == != === !== (关系运算符)
let str = '1', bool = true
console.log( str ++ ) // 输出1,但是str=2
console.log( '6' + 7 ) // 输出'67',因为这里的加号是字符串连接符
console.log( '7' - 6 ) // 输出1
console.log( true + 1 ) // 输出2,true被转换为1
// 转换成Boolean类型:!(逻辑非运算)
console.log( !!1122 ) // true
console.log( !'laka' ) // false
console.log( !'' ) // true
console.log( !0 ) // true
数据类型隐式转换常见面试题
1.字符串连接符与算数运算符
console.log( 1 + 'true' ) // '1true'
console.log( 1 + true ) // 2
console.log( 1 + undefined ) // NaN
console.log( 1 + null ) // 1
console.log( 1 + [] ) // '1'
console.log( 1 + [1, 22] ) // '11,22'
// 原因分析
// 1. 当+左右两边存在字符串时,+会变成字符串连接符
// 2. Number(true) = 1
// 3. Number(undefined) = NaN, NaN与任何数做任何运算都是本身
// 4. Number(null) = 0, 但是 null == undefined 结果为true
// 5. 此时在做运算的时候, [] 先转换为字符串'', 因此结果为'1'
// 6. 同理,[1, 22]先转换为'1,22'
2.关系运算符
console.log( '2' > 10 ) // false
console.log( '2' > '10' ) // true
console.log( 'abc' > 'b' ) // false
console.log( 'abc' > 'aab' ) // true
console.log( NaN == NaN ) // false
console.log( undefined == null ) // true
// 原因分析
// 当关系运算符左右两边为数字和字符串时,字符串先转换为数字
// 当两边都为字符串时,按照字符串对应的Unicode编码比较
// 1. 2 > 10
// 2. '2'.charCodeAt() > '10'.charCodeAt(), 值得注意的是,使用charCodeAt方法只会返回字符串第一个字符的unicode编码,因此'10'.charCodeAt == '1'.charCodeAt()
// 3. 与2同理
// 4. 当两个字符串同一位置的字符相同时,会比较下一个字符
// 5和6均为特殊情况,Number(undefined) != Number(null)
3.复杂数据类型
console.log( [11, 22] == '11,22' ) // true
console.log( [67] == 67 ) // true
// 分析:
// 当复杂类型与字符串比较时,首先调用valueOf()方法返回对象的原始值
// 然后再调用toString()方法得到字符串进行比较
// 如果是与number进行比较的话,还会使用Number()函数将字符串转换成数字进行比较
let o = {}
console.log( o == '[object, Object]' ) // true
console.log( o.valueOf() ) // {}
console.log( o.valueOf().toString() ) // '[object, Object]'
// 分析:
// 任何对象调用toString()方法后得到的字符串都是'[object Object]'
// 前后两个object的o大小写不一样
let o = {
value: 0,
valueOf: function(){
return ++ this.value
}
}
console.log( o == 1 ) // true
console.log( o == 2 ) // true
console.log( o == 3 ) // true
console.log( o == 3 ) // false
// 分析:
// 值得注意的是,对象的valueOf()方法是可以重写的
// 每次进行关系比较时,都会隐式调用valueOf()方法,因此value不断加一
4.一些坑
console.log( [] == 0 ) // true
console.log( ![] == 0 ) // true
// 分析:
// 1.当对象与数字进行比较时,对象会进行转换Number([].valueOf().toString())
// 空字符串会转换成0,因此[]==0
// 2.此时的空数组前面有个取反符!,逻辑运算符的优先级比算数运算符高,因此左边会先转换成Boolean类型
// 此时值得注意的是 Boolean([]) === true
// 这是因为空数组的类型为Object,而Boolean()函数中的参数,只要数据类型为对象那么都会得到true
console.log( [] == ![] ) // true
console.log( [] == [] ) // false
// 分析:
// 1.可以发现==右边又有一个!,那么右边的数据类型为Boolean,为false
// 左边的数据类型为对象,此时,对象并不是直接转换为Boolean类型与右边比较的
// 而是将对象先转换为字符串,在转换为数字进行比较,Number([].valueOf().toString())==0
// 同理Boolean类型也要转换为数字进行比较,![] == 0
// 2.数组是对象,为引用类型数据,数据存储在堆中,栈中存储的是地址
// 虽然都是空数组,但是起始地址不相同,所以两个空数组也不同,因此[1, 2] == [1, 2]也为false
console.log( {} == {} ) // false
console.log( {} == !{} ) // false
// 分析:
// 1.经过上一题的洗礼,不难得出结果,两个空对象的地址不同,因此为false
// 2.同理,左右两边转换为number进行比较
// 先看右边,对象转换为Boolean一定为true,取反后再用Number()函数可以得到0
// 再看左边,调用toString()方法后得到'[object, Object]',再使用Number()函数得到NaN
// NaN == 0 为false
5.做个总结?
- 任何数据类型与对象相加都会变成string类型
- '2' > '10'
- undefined == null
- Boolean([]) === Boolean({}) === true
- Obje转换为number都会得到NaN
- Object(valueOf()和toString())=>String(Number())=> Number <= Boolean
数据类型的判断
typeof
typeof 变量,返回变量的数据类型,返回的结果是一个字符串
【返回结果】
返回值包括:'undefined', 'object', 'boolean', 'number', 'bingint', 'string', 'symbol', function'
instanceof
object instanceof constructor 用于判断constructor是否出现在object实例对象的原型链上,一般是用来判断具体的对象类型
【返回结果】
返回值为波尔类型,只有true或者false
var simpleStr = 'this is a simple string'
var myStr = new String()
simpleStr instanceof String // false
myStr instanceof String // true
【手写instanceof】
function instance_of (L, R) {
let l = L.__proto__
let r = R.prototype
while (true) {
if (l === null) {
return false
}
if (r === l) {
return true
}
l = l.__proto__
}
}
IIFE(Immediately-Invoked Function Expression)
【作用】
- 隐藏实现
- 不会污染外部命名空间
- 用来编码js模块
;(function () {
var a = 1
function test () {
console.log(++a)
}
window.$ = function () {
return {
test: test
}
}
})
$().test() // 2
预解析
JS引擎运行JS的时候分为两步:预解析和代码执行
预解析:JS引擎会把JS里面所有的var还有function提升到当前作用域的最前面
代码执行:按照代码书写的顺序从上往下执行
预解析分为 变量预解析(变量提升) 和 函数预解析(函数提升)
变量提升:把所有的变量声明提升到当前的作用域最前面,不提升赋值操作
函数提升:把所用的函数声明提升到当前的作用域最前面,不调用函数
先执行变量提升后执行函数提升
一个小案例
f()
console.log(c)
console.log(b)
console.log(a)
function f(){
var a = b = c = 9
console.log(a)
console.log(b)
console.log(c)
}
// 输出:9 9 9 9 9 error(报错)
// 问题出在这一句:var a = b = c = 9
// 相当于 var a = 9; b = 9; c = 9;
// 所以a只是局部变量,而b和c是全局变量
var a
function a() {}
typeof a // 'function'
var fun = 1
function fun() {}
fun() // 报错
// 当函数与变量同名的时候,函数会覆盖变量
// 但是后面又执行了赋值操作,所以fun = 1
对象
构造函数:
// 构造函数的语法格式
function 构造函数名() {
this.属性 = 值;
this.方法 = function() {}
}
// var object = new 构造函数名()
// new关键字执行过程
// 1. new 构造函数在内存中创建了一个空的对象
// 2. this 就会指向刚创建的空对象
// 3. 执行构造函数里面的代码,给这个空对象添加属性和方法
// 4. 返回这个对象(所以构造函数里面不需要return)
function _new (constructor, ...arg) {
// 创建一个空对象
let obj = {}
// 空对象的隐式原型指向构造函数的显式原型,为这个新对象添加属性
obj.__proto__ = constructor.prototype
// 构造函数的作用域赋给新对象
let res = constructor.apply(obj, arg)
// 返回新对象,如果没有显式return语句,则返回this
return Object.prototype.toString.call(res) === '[object Object]' ? res : obj
}
原型对象:
解析器会向每一个函数中添加一个prototype属性,无论是构造函数还是普通的函数,这个属性对应着一个对象,这个对象就是原型对象
如果函数作为普通的函数调用,则prototype没有任何作用,但是如果作为构造函数创建对象时,它所创建的对象都会有一个隐含属性__proto__,该属性指向构造函数的原型对象
原型对象就相当于一个公共区域,所有同一个类的实例对象都可以访问到这个原型对象,值得注意的是,原型对象中也有__proto__属性,只有Object对象的原型没有原型
检查对象是否含有某种属性或方法以及原型链
function Person(name, age){
this.name = name
this.age = age
}
Person.prototype.sayHello(){ console.log('hello') }
let xiaoli = new Person('xiaoli', 18)
console.log( 'sayHello' in xiaoli ) //true
console.log( xiaoli.hasOwnProperty('sayHello') ) // false
console.log( xiaoli.__proto__) // [object Object] xiaoli的原型
console.log( xiaoli.__proto__.__proto__ ) // [objrct Obiect] xiaoli原型的原型,相当于Object的原型
console.log( xiaoli.__proto__.__proto__.__proto__ )// null Object的原型没有原型
总结:
- 所有的引用类型(数组、对象、函数),都具有对象的特性,即可以自由扩展属性('null')除外
- 所有的引用类型,都有一个__proto__(隐式原型)属性,属性值是一个普通的对象
- 所有的函数,都有一个prototype(显式原型)属性,属性值也是一个普通的对象
- 所有的引用类型,__proto__属性值指向(完全相等)它的构造函数的prototype属性值
- 当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么就会去__proto__(即它的构造函数的prototype中)寻找
- 函数的显式原型指向的对象默认是空Object对象(除Object外)
- 所有函数都是Function的实例(包括Function)
- Object的对象是原型链的尽头(Object.prototype.__proto__ === null)
this
this出现的原因:
JavaScript允许在函数体内部,引用当前环境的其他变量,由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context),所以this出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境
解析器再调用函数的时候会向函数内部传递一个隐含参数this
this为函数执行的上下文对象,谁调用它,this就指向谁
严格模式下,this默认绑定在undefined上,非严格模式下才是window
var name = 'window'
var fun = function () {
console.log(this.name);
}
var obj1 = {
name: 'obj1',
sayName: fun
}
var obj2 = {
name: 'obj2',
sayName: fun
}
fun() // 'window'
obj1.sayName() // 'obj1'
obj2.sayName() // 'obj2'
function foo () {
console.log(this.a)
}
var a = 2
var obj1 = {
a: 3
}
var obj2 = {
a: 4
}
var bar = function () {
foo.call(obj1)
}
bar() // 3
setTimeout(bar, 100) // 3
bar.call(obj2) // 3 !!!
// 虽然bar被显式绑定到了obj2上,但是foo已经被绑定在了obj1上,所以在foo函数内部,this指向obj1
var a=1;
function printA(){
console.log(this.a);
}
var obj={
a:2,
foo:printA,
bar:function(){
printA();
}
}
obj.foo(); //2
obj.bar(); //1
var foo=obj.foo;
foo(); //1
var length = 10;
function fn() {
console.log(this.length);
}
var obj = {
length: 5,
method: function(fn) {
fn();
arguments[0]();
}
}
obj.method(fn, 1)
// 输出 10 2
// 第一次输出10很好理解,fn无人调用,所以this指向window
// 第二个输出2是因为,arguments是一个类数组对象
// arguments[0]()相当于arguments这个对象调用了fn函数,因此输出2
bind改变this指向的例子:
var name = 'LK'
function Person (name) {
this.name = name
this.sayName = function () {
setTimeout(function () {
console.log(this.name)
}, 50);
}
}
var person = new Person ('lk')
person.sayName() // 输出'LK'
// setTimeout定时函数相当于window.setTimeout(),由window这个全局对象调用,因此this指向window
// 使用bind
var name = 'LK'
function Person (name) {
this.name = name
this.sayName = function () {
setTimeout(function () {
console.log(this.name)
}.bind(this), 50);
}
}
var person = new Person ('lk')
person.sayName() // 输出'lk'
call 、 apply和bind
call()和apply()方法都是函数对象的方法,需要通过函数对象来调用
当函数调用call()和apply()都会调用函数执行
function f(){
console.log('LKing')
}
f() // LKing
f.call() // LKing
f.apply() // LKing
在调用call()和apply()可以将一个对象指定为第一个参数,修改this的指向
obj = {}
function f(){
console.log(this);
}
f() // Window
f.call(obj) // Object
f.apply(obj) // Object
call和apply的小不同
obj = {}
function f(a, b){
console.log(a);
console.log(b);
}
f()
f.call(obj, 11, 22)
f.apply(obj, [11, 22])
// call()方法可以将实参在对象之后依次传递
// apply()方法需要将实参封装到一个数组中同一传递
bind与call和apply作用类似,不同的是bind()方法返回的是一个函数
<script>
var name = 'Lily',
age = 13
var obj = {
name: 'Sam',
objAge: this.age,
myFunc: function (param1, param2) {
console.log(this.name + ' ' + this.objAge + ' ' + this.age + ' ' + param1 + ' ' + param2)
// Amy 20 21 1 2
}
}
var db = {
name: 'Amy',
age: 21,
objAge: 20
}
obj.myFunc.call(db, '1', '2') // db为this要指向的对象,后面传入的是参数列表,参数可以是任意类型,当第一个参数为null、undefined的时候,默认指向window;
obj.myFunc.apply(db, [1, 2]) // db为this要指向的对象,参数必须放在一个数组里面;
obj.myFunc.bind(db, '1', '2')() // db为this要指向的对象,返回的是一个新函数,必须调用才会去执行。
</script>
手写apply(call同理)
Function.prototype.myApply = function(context, args) {
// 如果不传值则默认为window
context = context || window
args = args ? args : []
// 给context新增一个独一无二属性以免覆盖原有属性
const key = Symbol()
context[key] = this
// 通过隐式绑定的方法调用函数
const res = context[key](...args)
// 删除添加的属性
delete context[key]
// 返回函数调用的结果
return res
}
手写bind
// 简单版
Function.prototype.myBind = function(context, ...args) {
const self = this
context = context || window
args = args ? args : []
return function () {
return self.apply(context, ...args, ...arguments)
}
}
// 复杂版
Function.prototype.myBind = function(context, ...args) {
// 判断调用bind是否为一个函数
if (typeof(this) !== 'function') {
throw new Error('not a function!')
}
// this表示调用这个函数的对象
const self = this
context = context || window
return function newFn() {
// 如果bind的返回值用new去调用
if (this instanceof newFn) {
return new self(...args, ...arguments)
}
return self.apply(context, [...args, ...arguments])
}
}
闭包
定义
有权访问另一个函数作用域中的变量的函数;一般情况下就是在一个函数中包含另一个函数
function func () {
let closure = '闭包'
function inner () {
console.log(closure)
}
return inner
}
let f = func()
f() // '闭包'
f() // 同上,说明变量closure并没有被销毁,一直存在内存中
原理:
闭包实现的原理,其实是利用了作用域链的特性,作用域链就是在当前执行环境下访问某个变量时,如果不存在就一直向外层寻找,最终寻找到最外层就是全局作用域,这样就形成了一个链条
function func () {
let num = 0
function add () {
return ++num
}
return add
}
func()() // 1
func()() // 1
let f = func()
f() // 1
f() // 2
宏任务和微任务
宏任务(macrotask)和微任务(microtask)是异步任务的两种分类
在挂起任务时,JS引擎会将所有异步任务分到这两个队列中,首先在marcotask的队列中取出第一个任务,执行完毕后取出microtask队列中所有任务顺序执行;之后再取macrotask任务,周而复始,直到两个队列的任务都取完
对调两个变量
var a = 1
var b = 2
// 临时变量法
var c = b // 临时变量
b = a
a = c
// 加减法
a = a + b
b = a - b
a = a - b
// 数组法
a = [a, b]
b = a[0]
a = a[1]
// 对象法
a = {a: b, b: a}
b = a.b
a = a.a
// 数组运算法
a = [b, b = a][0]
// 按位异或法
a = a ^ b
b = a ^ b
a = a ^ b
// 解构赋值法
[a, b] = [b, a]
AJAX的五个步骤
// 创建XMLHttpRequest异步对象
var xhr = new XMLHttpRequest()
// 设置回调函数
xhr.onreadystatechange = callback
// 使用open方法与服务器建立连接
// get
xhr.open('get', 'test.php', true)
// post
// xhr.open('post', 'test.php', true)
// xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
// 向服务器发送数据
// get不需要传递参数,post需要
xhr.send(null)
// 在回调函数中针对不同的响应状态进行处理
function callback () {
// 状态码
// 0: 请求未初始化
// 1: 服务器已建立连接
// 2: 请求已接收
// 3: 请求处理中
// 4:请求已经完成
if (xhr.readyState === 4) {
if (xhr.status === 200) {
var res = xhr.responseText
res = JSON.parse(res)
}
}
}
fetch
fetch的功能与XMLHttpRequest基本相同,但是有三个主要差异
- fetch使用Promise,不使用回调函数
- fetch采用模块化设计,API分散在多个对象上(Request对象,Response对象,Header对象),更合理一些,相比之下,XMLHttpRequest的API设计并不是很好,输入输出状态都在同一个接口管理
- fetch通过数据流(Stream对象)处理数据,可以分块读取,有利于提高网站性能表现
fetch()接受一个URL字符串作为参数,默认向网站发出GET请求,返回一个Promise对象
fetch('https://api.github.com/users/ruanyf')
.then(response => response.json())
.then(json => console.log(json))
.catch(err => console.log('Request Failed', err));
// async函数写法
async function getJSON() {
let url = 'https://api.github.com/users/ruanyf';
try {
let response = await fetch(url);
return await response.json();
} catch (error) {
console.log('Request Failed', error);
}
}
fetch()接收到的response是一个Stream对象,response.json()是一个异步操作,取出所有内容,将其转化为JSON对象
fetch()发出请求后,只有两种情况会报错:网络错误或者无法连接,所以要通过Response.status属性,得到HTTP回应的真是状态码,判断是否请求成功
async function fetchText() {
let response = await fetch('/readme.txt');
if (response.status >= 200 && response.status < 300) {
return await response.text();
} else {
throw new Error(response.statusText);
}
}
读取方法:
response.text()
:得到文本字符串。response.json()
:得到 JSON 对象。response.blob()
:得到二进制 Blob 对象。response.formData()
:得到 FormData 表单对象。response.arrayBuffer()
:得到二进制 ArrayBuffer 对象。
Stream对象只能读取一次,读取完就没了,所以说五个读取方法,只能使用一次,response.clone()对象就可以复制一份Reaponse对象
Array
【Array.from()】
可将两类对象转化为真正的数组:类数组对象和可遍历对象。该方法还可以接受第二个参数,作用类似于数组的map方法,用来对每一个元素进行处理,将处理后的值放入数组中
【copyWithin()】
数组实例方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组,该方法会修改当前数组,有三个参数
- target(必需):从该位置开始替换数据,如果为负值,则表示倒数
- start(可选):从该位置开始读取数据,默认为0,可为负值
- end(可选):到该位置前停止读取数据,默认等于数组的长度,可为负值
【find()和findIndex()】
find方法,用于找出第一个符合条件的数组成员,它的参数是一个回调函数,findIndex就是找出第一个符合条件的数组成员的下标
【entries(), keys(),和values()】
用于遍历数组,它们都会返回一个遍历器对象,可以用for...of循环遍历,keys是对键名的遍历,values是对键值的遍历,entries是对键值对的遍历
深拷贝
function isObject(obj) {
return Object.prototype.toString.call(obj) === '[object Object]' || Array.isArray(obj)
}
function deepClone(obj, hash=new Map()) {
if (!isObject(obj)) return obj
if (hash.has(obj)) return hash.get(obj)
let objClone = Array.isArray(obj) ? [] : {}
hash.set(obj, objClone)
for (let key in obj) {
if (isObject(obj[key])) {
objClone[key] = deepClone(obj[key], hash)
} else {
objClone[key] = obj[key]
}
}
return objClone
}
ES6
let和const
- 没有变量提升
- 存在块级作用域
- 不能重复命名
- const声明后不能修改
与var相比的作用
- for循环
- 不会造成全局污染
Object.is()
NaN === NaN // false
Object.is(NaN, NaN) // true
-0 === +0 // true
Object.is(-0, +0) // false
class-extends-super
class属于一种语法糖,让代码看起来更像是面向对象
class Point {
constructor (x, y) {
this.x = x
this.y = y
}
toString () {
return '(' + this.x + ',' + this.y + ')'
}
}
// 等同于
function Point(x, y) {
this.x = x
this.y = y
}
Point.prototype.toString = function () {
return '(' + this.x + ',' + this.y + ')'
}
constructor是类的构造函数,一个类必须有一个构造函数,如果没有显式的,一个默认的constructor方法也会被默认添加,一般constructor方法返回实例对象this,但是也可以指定返回一个全新的对象,让返回的实例对象不是该类的实例
在ES6中使用关键字extends实现继承
class Point {
constructor (x, y) {
this.x = x
this.y = y
}
toString () {
return '(' + this.x + ',' + this.y + ')'
}
}
class ColorPoint extends Point {
constructor (x, y, color) {
super(x, y) // 调用父类的constructo(x, y)
this.color = color
}
toString () {
return this.color + ' ' + super.toString() // 调用父元素的toString()
}
}
在子类的constructor中必须调用super方法,因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工,而super就代表着父类的构造函数,其返回的是子类的实例,即super内部的this指向子类。super.toString()相当于Point.prototype.toString()
Object.getPrototypeOf()方法可以用来从子类上获取父类,可以用这个方法检查一个类是否继承另一个类
关于super:
class A {
constructor() {
this.x = 1;
}
}
class B extends A {
constructor() {
super();
this.x = 2;
super.x = 3;
console.log(super.x); // undefined
console.log(this.x); // 3
}
}
let b = new B();
由于this指向子类实例,所以如果通过super对某个属性赋值,这时super就是this,赋值的属性也会变成子类实例的属性
super总结:
- 可以当作函数使用,可以当作对象使用
- 当作函数使用,代表的是父类的构造函数
- 当作对象时,在普通方法中,指向父类的原型对象
- 当作对象时,在静态方法中,指向父类
- 当作对象时,方法内部的this指向当前子类实例
箭头函数
var f = v => v
// 等同于
var f = function (v) {
return v
}
箭头函数格式问题:
// 报错
let getObj = id => {id: id, name: 'LK'}
// 正确
let getObj = id => ({id: id, name: 'LK'})
箭头函数的使用问题:
- 箭头函数没有自己的this对象
- 不可以当作构造函数,也就是说,不可以对箭头函数使用new命令,否则会抛出错误
- 不可以使用arguments对象,该对象在函数体里不存在,如果要用,可以用(...变量)名代替
- 不可以使用yield命令,因此箭头函数不能用作Generator函数
箭头函数this指向问题:
对于普通函数来说,内部的this指向函数运行时所在的对象,但是这一点对箭头函数不成立。它没有自己的this对象,内部的this,就是定义时上层作用域中的this。也就是说,箭头函数内部的this指向是固定的,相比之下,普通函数的this指向是可变的
function foo () {
return () => {
return () => {
return () => {
console.log(this.id)
}
}
}
}
var f = foo.call({id: 1})
var t1 = f.call({id: 2})()() // 1
var t2 = f().call({id: 3})() // 1
var t3 = f()().call({id: 4}) // 1
所有的内层函数都是箭头函数,都没有自己的this,它们的this其实都是最外层foo函数的this。
箭头函数不适用的场景:
第一个场所是定义对象的方法,且该方法内部包含this
var obj = {
func: () => {
console.log(this)
}
}
obj.func() // window
第二个场景是需要动态this的时候,也不应该使用箭头函数
Symbol
ES6新引进的原始数据类型,表示独一无二的值,Symbol函数前面不能使用new命令,否则会报错,这是因为生成的Symbol是一个原始类型的值,不是对象
let s1 = Symbol()
let s2 = Symbol()
s1 === s2 // false
Symbol值作为对象的属性名时,该属性还是公共属性,不是私有属性,所以还是可以用[]访问到该属性值,但是不会出现在for...in和for...of循环中,也不会被Object.keys()、Object.getOwnPropertyName()返回,但是有一个Object.getOwnPropertySymbols方法,可以获取对象的所有Symbol属性名
【Symbol.for()】
let s1 = Symbol.for('foo')
let s2 = Symbol.for('foo')
s1 === s2 // true
Symbol()和Symbol.for()都会生成新的Symbol,区别就是,后者会被登记再全局变量中供搜索,前者不会,因此调用Symbol.for()不会每次调用就返回一个新的Symbol类型的值,而是会检查给定的key时候已经存在,如果不存在才会返回一个新的
【Symbol.keyFor()】
let s1 = Symbol.for('foo')
Symbol.keyFor(s1) // 'foo'
let s2 = Symbol('foo')
Symbol.keyFor(s2) // undefined
Symbol.keyFor()方法返回一个已登记的Symbol类型值的key
Map
map初始化
let map = new Map([['a', 1], ['b', 2]])
// 表示有两个键值对,键分别时a和b,值分别是1和2
Iterator(遍历器)
遍历器是一个接口,为各种不同的数据结构提供一个统一的访问机制,任何数据结构只要部署了iterator接口就可以完成遍历操作(for...of)
Iterator的遍历过程:
- 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质是一个指针对象
- 第一次调用指针对象的next方法,可以将指针指向数据的第一个成员
- 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员
- 不断调用指针对象的next方法,直到它指向数据结构的结束位置
默认的Iterator接口部署在数据结构的Symbol.iterator属性
手动添加Symbol.iterator属性
Generator(生成器)
Generator函数是ES6提供的一种异步编程解决方案
执行Generator函数会返回一个遍历器对象,也就是说,Generator函数除了状态机,还是一个遍历器对象生成函数
形式上Generator函数是一个普通函数,但是有两个特征,一是function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同内部状态
yield的格式:
如果yield表达式用在另一个表达式之中,必须放在圆括号里面
next函数的参数:
yield表达式本身没有返回值,next方法可以带一个参数,该参数就会被当作是上一个yield表达式的返回值
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }
Generator函数的作用 :
1. 可以给无法遍历的对象部署Iterator接口
function* iterEntries(obj) {
let keys = Object.keys(obj);
for (let i=0; i < keys.length; i++) {
let key = keys[i];
yield [key, obj[key]];
}
}
let myObj = { foo: 3, bar: 7 };
for (let [key, value] of iterEntries(myObj)) {
console.log(key, value);
}
// foo 3
// bar 7
2. 异步操作的同步化表达
Generator函数的暂停执行的效果,意味着可以把异步操作写在yield表达式里面,等到调用next方法后再往后执行
function* loadUI() {
showLoadingScreen();
yield loadUIDataAsynchronously();
hideLoadingScreen();
}
var loader = loadUI();
// 加载UI
loader.next()
// 卸载UI
loader.next()
Promise
Promise对象是一个构造函数,用来生成promise对象;Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject,它们也是函数,由Javascript引擎提供,不需要部署
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
resolve函数的作用是,将Promise对象的状态从“未完成”变成“成功”(pending到resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数同理,只是将状态变为“失败”
Promise实例生成后,可以用then方法分别指定resolved状态和rejected状态的回调函数
promise.then(function(value) {
// success
}, function(error) {
// failure
});
【Promise.prototype.then()】
then方法的作用是为Promise实例添加状态该表时的回调函数,then方法的第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数,它们都是可选的;then方法返回的是一个新的Promise实例,因此可以在then方法后面再调用另一个then方法
getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {
// ...
});
【Promise.prototype.catch()】
catch方法是.then(null, rejection)或.then(undefined, rejection) 的别名,用于指定发生错误时的回调函数
getJSON('/posts.json').then(function(posts) {
// ...
}).catch(function(error) {
// 处理 getJSON 和 前一个回调函数运行时发生的错误
console.log('发生错误!', error);
});
【Promise.prototype.finally】
finally()方法用于不管Promise对象状态如何,都会执行的操作,实现如下
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};
【Promise.all()】
Promise.all()方法用于将多个Promise实例,包装成一个新的Promise实例,接受一个数组作为参数,p1、p2和p3都是Promise实例,参数也可以不是数组,但是必须具有Iterator接口,且返回的每个成员都是Promise实例
const p = Promise.all([p1, p2, p3]);
p的状态分情况决定:
- 只有全部Promise实例状态都变为resolved,p的状态才会变成resolved。此时所有Promise实例的返回值组成一个数组,传递给p的回调函数
- 只要一个Promise实例被rejected,p的状态就会变成rejected,此时第一个rejected的实例的返回值,会传递给p的回调函数
(Promise.any()方法与之相反,下面是手写Promise.any)
MyPromise.any = function(promises){
return new Promise((resolve,reject)=>{
promises = Array.isArray(promises) ? promises : []
let len = promises.length
// 用于收集所有 reject
let errs = []
// 如果传入的是一个空数组,那么就直接返回 AggregateError
if(len === 0) return reject(new AggregateError('All promises were rejected'))
promises.forEach((promise)=>{
promise.then(value=>{
resolve(value)
},err=>{
len--
errs.push(err)
if(len === 0){
reject(new AggregateError(errs))
}
})
})
})
}
【Promise.race()】
const p = Promise.race([p1, p2, p3]);
只要p1、p2、p3之中有一个实例率先改变状态,p的状态就会跟着改
【Promise.resolve()】
可以将现有对象转化为Promise对象
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))
async
async函数返回一个Promise对象
async函数内部return语句返回的值,会成为then方法回调函数的参数
async function f() {
return 'hello world';
}
f().then(v => console.log(v))
// "hello world"
async函数内部抛出错误,会导致返回的Promise对象变为reject状态,抛出的错误对象会被catch方法回调函数收到
async function f() {
throw new Error('出错了');
}
f().then(
v => console.log('resolve', v),
e => console.log('reject', e)
)
//reject Error: 出错了
async
函数返回的 Promise 对象,必须等到内部所有await
命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return
语句或者抛出错误。也就是说,只有async
函数内部的异步操作执行完,才会执行then
方法指定的回调函数。
有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个await
放在try...catch
结构里面,这样不管这个异步操作是否成功,第二个await
都会执行。
async function f() {
try {
await Promise.reject('出错了');
} catch(e) {
}
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
// hello world
另一种方法是await
后面的 Promise 对象再跟一个catch
方法,处理前面可能出现的错误。
async function f() {
await Promise.reject('出错了')
.catch(e => console.log(e));
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
// 出错了
// hello world
DOM
- DOM,Document Object Model,文档对象模型
- JS中通过DOM来对HTML文档进行操作,只要理解了DOM就可以随心所欲的操作WEB页面
- 文档,表示的就是整个HTML页面文档
- 对象,表示将网页中的每一个部分都转化为了一个对象
- 模型,使用模型来表示对象之间的关系,这样方便我们获取对象(DOM树)
节点
节点:Node,构成HTML文档最基本的单元
常用的节点分为四类:
- 文档节点:整个HTML文档
- 元素节点:HTML文档中的HTML标签
- 属性节点:元素的属性
- 文本节点:HTML标签中的文本内容
节点属性(文本节点的值用的比较多而已)
nodeName | nodeType | nodeValue | |
文档节点 | #document | 9 | null |
元素节点 | 标签名 | 1 | null |
属性节点 | 属性名 | 2 | 属性值 |
文本节点 | #text | 3 | 文本内容 |
浏览器已经为我们提供了文档节点document对象,这个对象是window属性
事件
事件,就是用户和浏览器之间的交互行为(比如:点击按钮、鼠标移动、关闭窗口......)
HTML DOM Event对象:HTML DOM Event 对象
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button id="btn">i am a button</button>
<script>
// 获取按钮对象
var btn = document.getElementById('btn')
// 可以为按钮的对应事件来绑定处理函数的形式来相应事件
// 绑定一个单击事件
// 像这种为单击事件绑定的函数,称之为单击响应函数
btn.onclick = function(){
alert('hello')
}
</script>
</body>
</html>
鼠标移动事件onmousemove中,鼠标的坐标属性:
clientX和clientY:用于获取鼠标在当前可见窗口的坐标
pageX和pageY:用于获取鼠标相对于整个页面的坐标
DOM查询
通过documen对象调用获取元素节点
- getElementById():通过id属性获取一个元素节点对象
- getElementsByTagName():通过标签名获取一组元素节点对象
- getElementsByName():通过name属性获取一组元素节点对象
- getElementsByClassName():通过class属性获取一组元素节点对象
- querySelector():需要一个选择器的字符串参数,可以根据CSS选择器查询一个元素节点对象
- querySelectorAll():与querySelector()类似,不同的是会返回一组元素节点对象
通过具体的元素节点调用
- getElementsByTagName():-方法,返回当前节点的指定标签后代节点
- childNodes:-属性,表示当前节点的所有子节点
- firstChild:-属性,表示当前节点的第一个子节点
- lastChild:-属性,表示当前节点的最后一个子节点
- parentNode:-属性,表示当前节点的父节点
- previousSibling:-属性,表示当前节点的前一个兄弟节点
- nextSibling:-属性,表示当前节点的后一个兄弟节点
childNode属性会获取包括文本节点在内的所有节点
而children属性只会获得元素节点
DOM增删改
创建一个元素节点对象,需要标签名作为参数
document.createElement()
创建一个文本节点对象,需要一个文本内容作为参数
document.createTextNode()
向一个父节点中添加一个新的子节点
父节点.appendChild(子节点)
在指定的子节点前插入新的子节点
父节点.insertBefore(新节点, 旧节点)
将新的节点替代指定的子节点
父节点.replaceChild(新节点, 旧节点)
删除指定的子节点
父节点.removeChild(子节点)
父节点的获取方式:子节点.parentNode
DOM与CSS
修改内联样式的语法如下(样式名中含有-,如background-color,需要将样式名修改成驼峰命名法):
元素.style.样式名 = 样式值
获取当前元素正在显示的样式
元素.currentStyle.样式名 // 只有IE可用
// 其他浏览器的方法
// 第一个参数为要获取样式的元素
// 第二个参数可以传递一个为元素,一般情况下为null
// 会返回一个对象,对象中封装了当前元素对应的样式
getcomputedStyle()
事件冒泡
事件的冒泡(Bubble):所谓的冒泡就是事件的向上传导,当后代元素上的事件被触发时,其祖先元素的相同事件也会被触发
<body>
<div>
<span></span>
</div>
</body>
// 如果分别给body、div和span绑定一个点击事件
// 点击span后,会最先触发span的点击事件
// 然后再是div,最后是body
在开发中,大部分的冒泡都是有用的,如果不希望事件冒泡则可以用事件对象来取消冒泡
event.cancelBubble = true
事件的委派:将事件统一绑定给元素的共同祖先元素,这样后代元素上的事件触发时,会一直冒泡到祖先元素,从而通过祖先元素的响应函数来处理
事件的绑定
// 事件的绑定
// 假设btn是获取到的按钮元素
btn.onclick = function() {
// 响应函数一
}
btn.onclick = function() {
// 响应函数2
}
// 上述绑定方式会使得后面的响应函数覆盖前面的响应函数
addEventListener()
-通过这个方法也可以为元素绑定响应函数
-参数:
1. 事件的字符串,不要on(如:click)
2. 回调函数,当事件触发时函数会被调用
3. 是否在捕获阶段触发事件,需要一个布尔值,一般都传false
事件触发后,会按照绑定顺序执行
btn.addEventListener(
'click',
function () {
// 我是一个回调函数
},
false
);
btn.addEventListener(
'click',
function () {
// 我是一个回调函数
},
false
);
事件的传播分为三个阶段:
1、捕获阶段
-从最外层的祖先元素,向目标元素进行事件的捕获,但是默认不会触发事件(false)
2、目标阶段
-事件捕获到目标元素,捕获结束开始在目标元素上触发事件
3、冒泡阶段
-事件从目标元素向它的祖先元素传递,依次触发祖先元素上的事件
鼠标滚轮事件
事件对象onwheel
// 案例为滚动滑轮改变div的height
<!DOCTYPE html>
<html lang="en">
<head>
<title>鼠标滚轮</title>
</head>
<body>
<div id="box"></div>
<script>
let box = document.getElementById('box');
box.onwheel = function (event) {
// console.log();
box.style.height = box.clientHeight + event.deltaY / 10 + 'px';
// 取消默认行为
event.preventDefault();
// return false
};
</script>
</body>
<style>
#box {
height: 200px;
width: 200px;
background-color: lightsalmon;
transition: 0.5s;
}
</style>
</html>
键盘事件
键盘事件一般会给绑定一些可以获取到焦点的对象或者是document(如:input)
键盘事件通常提供以下三个属性,可以用来判断是否对应的键是否有事件发生(用来判断同时按两个键):
- altKey
- ctrlKey
- shiftKey
docume.write和innerHTML ![](https://i-blog.csdnimg.cn/blog_migrate/a147053581682059036100a651122eb3.png)
clientHeight等
网页可见区域高:documen.body. clientHeight
网页正文全文高:documen.body. scrollHei
网页可见区域高(包括边线的高):documen.body. offsetHei
网页被卷去的高:documen.body. scrollTop
BOM
- Browser Object Model浏览器对象模型
- BOM可以使我们通过JS来操作浏览器
- BOM提供了一组对象用来完成对浏览器的操作
BOM对象
Window
- 代表的是整个浏览器的窗口,同时window也是网页中的全局对象
Navigator
- 代表的是当前浏览器的信息,通过该对象可以来识别不同的浏览器
由于历史原因,Navigator对象中的大部分属性都已经不能帮助我们识别浏览器了
Location
- 代表当前浏览器地址栏的信息,通过Location可以获取地址栏信息,或者跳转页面
- assign()
用来跳转到其他页面,会产生历史记录
- reload()
重新加载当前页面,如同刷新,若传入一个true参数,可以强制清空缓存
- replace()
用新的页面替换当前页面,不产生历史记录,无法回退
History
- 代表浏览器的历史记录,可以通过该对象来操作浏览器的历史记录,
由于隐私问题,该对象不能具体获取到具体的历史记录,只能操作浏览器向前或向后翻页
而且该操作只在当此访问有效
- length:
属性,可以获取到当前访问的链接数量
- back()
可以回退到上一个页面,作用和浏览器的回退按钮一样
- forward()
可以跳转到下一个页面,作用和浏览器的前进按钮一样
- go()
可以跳转到指定页面,需要一个整数作为参数,正数表示向前跳转n个页面,负数相反
Screen
- 代表用户的屏幕信息,通过该对象可以获取到用户的显示器的相关信息
window定时器
setInterval()
- 定时调用
- 可以将一个函数,每隔一段时间执行一次
- 参数
- 回调函数,该函数会每隔一段时间被调用一次
- 每次调用的时间间隔,单位是毫秒
- 返回值:返回一个Number类型的数据,作为定时器的唯一标识
clearInterval()
- 用于关闭定时器
var timer = setInterval(function () {
// 回调函数......
if (/*关闭定时器的条件*/) {
// 关闭定时器
clearInterval(timer);
}
}, 1000);
setTimeout()
- 延时调用
- 一段时间后调用函数,且只调用一次
clearTimeout()
- 关闭一个延时调用
CSS
单位
宽度单位:
- %:相对单位,相对于父元素的大小
- px:对于父元素来说是绝对单位,相对于显示屏分辨率来说是相对单位
- vw:viewport width,相对于视口的相对单位(vh同理)
HTML代码
CSS代码
运行结果
字体单位:
- rem:r表示root,相对于根元素的字体大小
- em:相对于父元素的字体大小
- %:相对于父元素的字体大小
HTML代码
CSS代码
运行结果
渐变
渐变是图片的一种,需要通过background-image来设置
1. 线性渐变:
颜色沿着一条直线发生变化
使用函数 linear-gradient()
background-image:linear-gradient(direction, color-stop1, color-stop2, ...)
参数 | 作用 |
direction | 指定渐变的方向 |
color-stop | 指定渐变的起止颜色 |
-线性渐变的开头,我们可以指定一个方向
to top / left / bottom / right
xxx deg (deg表示度数)
xxx turn (turn表示圈)
-渐变可以指定多个颜色,默认平均分配
可以手动分配颜色渐变分布,如下:
linear-gradient(red 50px, yellow 150px)
red 50px表示0-50px的位置都是red
yellow 150px表示从150px开始剩下的位置都是yellow
重复的线性渐变
2. 径向渐变
放射性的渐变效果
radial-gradient(大小 at 位置,颜色 位置,颜色 位置,...)
多行省略 ![](https://i-blog.csdnimg.cn/blog_migrate/5d6db593a04fb66675457e64fe82de4f.png)
![](https://i-blog.csdnimg.cn/blog_migrate/ac0c4f127c20b9fa996ca6d588b76a45.png)
![](https://i-blog.csdnimg.cn/blog_migrate/c18203de161ee12af51c7f6ad208214b.png)
清除浮动
浮动的问题:
设置浮动后,会造成浮动塌陷,因为设置浮动后,元素会脱离文档流,如果包含该元素的块没有设置高度的话,此时包含块的高度会塌陷
设置浮动前
设置浮动后
清除浮动的方法:
1.在浮动元素后增加一个带有clear:both的元素
2.将父元素设置成BFC,增加属性overflow:hidden
3.与第一种方法类似,给父元素设置一个伪元素,赋予clear的属性
外边距塌陷
两个在正常流中相邻(兄弟或者父子关系)的块级元素的外边距。组合在一起变成单个外边距
外边距塌陷不是bug,它的设计初衷是为了段落垂直方向之间的空隙
【什么时候会塌陷】
- 垂直方向,不是水平方向
- 块级元素,不是行内元素,也不是行内块元素
【如何计算外边距】
- 正数&正数,取最大的正数
- 负数&负数,取最小的负数
- 正数&负数,取相加之和
【如何解决】
1. 绝对定位
父元素设置相对定位,子元素设置绝对定位,子元素设置外边距就不会塌陷了
2. 行内块级元素
将子元素设置成行内块级元素,因为外边距塌陷只存在于块级元素中
3. 相对定位
给子元素设置相对定位,然后设置相对于原来位置的偏移
4. 浮动
5. BFC
将父元素设置成BFC
6. 内边距
给父元素设置内边距
7. 边框
给父元素设置边框
8. 空元素
给相邻元素之间设置空元素,要注意的是,空元素的上下外边距会重合
BFC
Block Formatting Context,块级格式化上下文,它是一个独立的渲染区域,它规定了内部Block-level Box如何布局,并且与这个区域外部毫不相干
【BFC的布局规则】
- 内部的Box会在垂直方向,一个接着一个地放置
- Box垂直方向的距离由margin决定,属于同一个BFC的两个Box的margin会发生塌陷
- BFC中子元素的左外边距会紧挨着父元素的左内边距,即使存在浮动也是如此
- BFC区域不会与float box发生重叠
- BFC就是页面上一个隔离的独立容器,容器里面的子元素不会影响到外面的元素
- 计算BFC的高度时,浮动元素也参与计算
【如何创建BFC】
- 根元素(有说html也有说是body)
- 浮动元素:float除了none以外的值
- 绝对定位元素:position为absolute或者fixed
- display为inline-block、table-cells、flex
- overflow除了visible以外的值(hidden、auto、scroll)
line-height
行高就是两个基线之间的距离
【行距】
行距 = line-height - font-size,半行距就是行距除以二,上下半行距的大小一定相同,这就是line-height能实现垂直居中的原因
【line-height的值】
- normal:浏览器默认的值
- 百分数:font-size * 百分数
- 数字:line-height = 数字 * font-size
【行框属性】
- 内边距外边距和边框不会影响到行框的高度,即不影响行高
- 行内元素的边框边界是由font-size而不是line-height控制的
vertical-align
vertical-align用来设置垂直对齐方式,所有对齐元素都会影响行高,只能应用在行内元素
【vertical-align的值】
- baseline:默认,元素放置在父元素的基线上
- sub:垂直对齐文本的下标
- super:垂直对齐文本的上标
- top:把元素的顶端与行中最高元素的顶端对齐
- text-top:把元素的顶端与父元素字体的顶端对齐
- middle:元素的中垂点与父元素的基线加1/2父元素中字母x的最高对齐
- bottom:把对齐的子元素的底端与行框底端对齐
- text-bottom:把元素的底端与父元素内容区域的底端对齐
- (+-n)px:元素相对于基线上下偏移npx
- x%:相对于元素的line-height值
- inherit:从父元素继承vertical-align属性值
【行盒baseline的移动】
行盒的baseline受该行所有元素的影响。我们假设有个元素以这种方式对齐(相对于自身baseline对齐),行盒的baseline就不得不移动。因为大多数竖直对齐(除了top和bottom)都是相对其baseline的,导致该行所有其他元素也跟着调整位置
- 如果一行有个人元素横跨了整个高度,vertical-align对它不起作用了,它顶部之上和底部之下已经没有能提供它移动的空间了。为了满足其他行盒baseline的对齐关系,行盒baseline就是不得不移动了。矮方块具有vertical-align:baseline,左边高方块是text-bottom对齐,右边是text-top对齐,可以发现baseline带着矮盒子一起跳上去了
- 一行里面放两个大元素,竖直对齐它们会移动baseline到满足它们对齐方式的位置,然后行盒的高度也会调整。添上第三个元素,其对齐方式不会让它超出行盒的边界的话,既不影响行盒的高度也不影响baseline的位置(中图)。如果它超出了行盒的边界,行盒的高度和baseline就会再次调整,这种情况下我们最初的两个方块被推下去了
垂直居中
垂直居中一直都是一个难点,对于垂直居中,水平居中显得尤为简单
【水平居中】
- 行内元素,对其父元素应用text-align:center
- 块级元素,对其自身margin:auto
接下来是垂直居中的解决方案
【基于绝对定位】
利用绝对定位,相对于父元素的宽高偏移50%,然后再用translate()变形函数,以自身的宽高为基准偏移50%,核心代码如下
缺点:
- 有时候绝对定位对整个布局影响太过剧烈
- 如果需要居中的元素已经在高度上超过了视口,那它的顶部会被视口裁剪掉
- 在某些浏览器中,这个方法可能会导致元素的显示有一些模糊,因为元素可能会被放置在半个像素上
【基于视口单位】
利用视口单位,vw是与视口宽度相关的,vh是与视口高度相关的,1vw就是视口宽度的1%,对于水平居中,至于需要让左右两边的外边距为auto,然后再让上外边距为50vh,即视口高度的50%,然后再用translateY来相对自身的高度移动,核心代码如下
缺点:只适用于视口中居中的场景
【基于Flexbox】
给父元素设置display:flex,再给父元素本身设置margin:auto即可,代码如下
同时flexbox也可以让可以让匿名容器(即没有被标签包裹的文本节点),代码如下
弹性布局(display:flex)
Flexbox是flexible box的简称,是CSS3新的布局模式
基本概念
采用Flex布局的元素,称为flex容器(flex container),简称“容器” ,它的所有子元素自动成为容器成为,成为flex项目(flex item),简称“项目”
容器属性
【flex-direction】
决定主轴(main axis)的方向
- row(默认): 主轴为水平方向,起点在左端
- row-reverse:主轴为水平方向,起点在右端
- column:主轴为垂直方向,起点在上沿
- column-reverse:主轴为垂直方向,起点在下沿
【flex-wrap】
决定是否换行
- nowrap(默认):不换行
- wrap:换行,第一行在上方
- wrap-reverse:换行,第一行在下方
【flex-flow】
flex-direction和flex-wrap的简写,默认row和nowrap
【justify-content】
定义了项目在主轴上的对齐方式
- flex-start(默认):左对齐
- flex-end:右对齐
- center:居中
- space-between:两端对齐,项目之间的间隔都相等
- space-around:每个项目之间的间隔相等。所以项目之间的间隔比项目与边框的间隔大一倍
【align-items】
定义了在交叉轴上的对齐方式,对齐方式与交叉轴有关
- flex-start:交叉轴的起点对齐
- flex-end:交叉轴的终点对齐
- center:交叉轴的中点对齐
- baseline:项目的第一行文字基线对齐
- stretch(默认):如果项目未设置高度或设为auto,将占满整个容器的高度
【align-content】
定义 多根轴线的对齐方式,如果项目中只有一个轴线,该属性就不起作用。当你不给项目设置高度但是容器设置align-content不为stretch,同一轴线上的项目高度将等于项目中高度最高的项目
- flex-start:交叉轴的起点对齐
- flex-end:交叉轴的终点对齐
- center:交叉轴的中点对齐
- space-between:交叉轴的两端对齐,轴线之间的间隔平均分布
- space-around:每根轴线之间的间隔相等
- stretch(默认):轴线占满整个交叉轴
项目属性
【order】
数值越小,排列越靠前,默认是0,可以是负值
【flex-grow】
默认是0,即如果空间有剩余,也不放大,可以是小数,按比例占据剩余空间
【flex-shrink】
默认值是1,即如果空间不足将等比例缩小,如果有一个项目的值为0,其项目为1,当空间不足时,该项目不缩小
【flex-basis】
默认值为auto,浏览器根据此属性检查主轴是否有多余空间,权重高于width
- npx:分配多余空间之前项目占据主轴npx,如果空间不足则缩小
【flex】
flex-grow、flex-shrink和flex-basis三个属性的缩写,默认值为:0 1 auto
flex:none(0 0 auto)表示不放大也不缩小
flex:auto(1 1 auto)表示放大也缩小
flex:1(1 1 0%)
【align-self】
align-self属性允许单个项目有与其他项目不一样的对齐方式,可覆盖align-items属性
CSS优先级
- important无条件优先
- 内联样式 1000
- id选择器 100
- class、伪类、属性 10
- 标签、伪元素 1
CSS选择器
element>element | div > p | 选择父元素是<div>的所有<p>元素 |
element+element | div + p | 选择紧跟着<div>元素的首个<p>元素 |
element1~element2 | p ~ ul | 选择前面有<p>元素的每个<ul>元素 |
包含块
当设置某些元素的属性值(width、heigh、padding等)使用到了%,此时的百分数是相对于元素包含块的宽度
包含块确定规则:
1、根元素所在的包含块叫初始包含块initial containingblock。对于连续媒体设备(continuous media),初始包含块的大小等于视口(viewport)的大小,基点在视口的左上角;对于分页媒体(page media),初始包含块是页面区域。初始包含块的direction属性与根元素相同
2、对于其他元素,如果position属性是relative或static,他的包含块是由最近的祖先块盒容器的内容区域(width和height属性确定的区域)创建的
3、如果一个元素的position属性为fixed,他的包含块由视口或者页面区域创建的
4、如果一个元素的position为absolute,它的包含块由最近的position不为static的祖先元素创建,具体创建方式如下:
a、如果创建包含块的祖先元素是行内元素,包含块的范围是这个祖先元素中的第一个和最后一个行内盒的padding box围起来的区域
b、如果这个元素不是行内元素,包含块的范围是这个祖先元素的内边距+width区域,如果没有找到这样的祖先元素,这个绝对定位的元素包含块为初始包含块
回流和重绘
回流:
当我们操作DOM时,使其结构发生改变,从而影响整个布局,这个过程就会发生回流
- 当元素的width、height、margin、padding、文字图片等发生变化
- DOM节点的增加和减少
- 读写offset、client、scroll时
- 使用window.getComputedStyle的时候
对于第三点,现代浏览器都有渲染队列的机制,当某个元素触发浏览器发生重绘或者回流时,它就会进入一个渲染队列中,如果下面还有样式修改,那么同样入队,直到下面没有样式修改,浏览器会按照渲染队列批量执行回流,但是读取client等相关数据时,就会迫使浏览器清空渲染队列,发生回流,读取以下数据均会
offsetTop、offsetLeft、offsetWidth、offsetHeight
clientTop、clientLeft、clientWidth、clientHeight
scrollTop、scrollLeft、scrollWidth、scrollHeight
getComputedStyle()(IE中currentStyle)
重绘:
当改变元素时,只是改变了它的外观,比如背景颜色等,而没有影响到它的布局,这个时候会发生重绘,回流必定引起重绘,重绘不一定会引起回流
减少及避免:
- 尽量避免频繁使用style,而是使用修改class的方式
- 使用createDocumentFragment
- 对于resize和scroll进行防抖节流处理
隐藏元素
盒模型
标准盒模型
- 总宽度 = width + padding + margin + border
- box-sizing:content-box
怪异盒模型
- 总宽度 = width(包含了padding、border和内容宽度) + margin
- box-sizing:border-box
HTML
<head>
head部分包括的元素:<base>, <link>, <meta>, <script>, <style>, <title>,其中<title>定义文档的标题,它是head部分唯一必需的元素
<meta>
<meta>元素可提供有关页面的元信息(meta-infomation),比如针对搜索引擎和更新频度的描述和关键词;<meta>标签位于文档的头部,不包含任何内容。<meta>标签的属性定义了文档相关联的名称/值对
<meta>标签是HTML语言头部的一个辅助性标签,我们可以定义页面编码语言、搜索引擎优化、自动刷新并指向新的页面、控制页面缓冲、响应式视窗等
charset属性:
定义文档的字符编码,是HTML5新加的属性,一般放在第一个meta标签中,否则会引起乱码
content属性:
定义与http-equiv或name属性相关的元信息
http-equiv属性:
把content属性关联到HTTP头部,http-equiv类似于HTTP头部协议,它会给浏览器一些有用的信息,一帮助正确和精确地显示网页内容,与之对应的content属性,其内容就是各个参数的变量值
- expires(期限):指网页在缓存中的过期时间,一旦网页过期,必须到服务器上重新传输
- pragma(cache模式):禁止浏览器从本地计算机的缓存中访问页面内容
- refresh(刷新):自动刷新并指向新页面
- Set-Cookie(cookie设定):浏览器访问某个页面时将它存入缓存中,下次再次访问时就可以从缓存中读取,以提高速度。
- Window-target(显示窗口的设定):强制页面在当前窗口以独立页面显示,可以用来防止别人在框架里调用你的页面
- content-Type(显示字符集的设定):设定页面使用的字符集
- imagtoolbar:指定是否显示图片工具栏
- X-UA-Compatible:优先使用IE最新版本和Chrome
name属性:
name属性主要用于描述页面,对应的属性时content,以便于搜索引擎机器人查找、分类(目前所有的搜索引擎都使用网上机器人自动查找meta值来给网页分类)
- keywords(关键字):为搜索引擎提供关键字列表
- description(简介):用来告诉搜索引擎你的网站主要内容
- robots(机器人向导):用来告诉机器人哪些页面需要索引,哪些页面不需要
- author(作者):标注网页的作者
- copyright(版权):标注版权
- generator:用来说明网站采用什么编辑器制作
- revisit-after(重访):网站重访
- viewport:能优化移动端浏览器的显示(屏幕的缩放)
- format-detection:忽略电话号码和邮箱
计算机网络
cookie
存储在用户本地终端上的数据
翻译成人话:cookie是一些数据,存储在用户电脑的文本文件中
cookie的作用:
当web服务器向浏览器发送web页面时,在连接关闭后,服务端不会记录用户的信息,所以cookie的作用就是用于解决“如何记录客户端的用户信息”:
- 当用户访问web页面时,他的名字可以记录在cookie中
- 当用户下一次访问该页面时,可以在cookie中读取用户访问记录
cookie是如何工作的:
当我们访问某个网站时,服务器首先根据浏览器的编号生成一个cookie返回给客户端;客户端下次再访问的时候就会将自己本地的cookie加上url访问地址一同给服务器;服务器读出来以此来辨别用户的状态。
cookie的缺陷:
- 数据大小:作为一个存储容器,cookie的大小限制在了4KB,只能存储一些简单信息
- 安全性问题:在HTTP请求中cookie是明文传输(HTTPS不是)
- 网络负担:cookie会被附加在每一个HTTP请求中,在HttpRequest和HttpResponse的header中都是要被传输的,增加了一些不必要的流量损失
session
“会话控制”,session对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的Web页面之间跳转时,存储在session对象中的变量将不会丢失,而是在整个用户会话中一直存下去
session的作用:
HTTP协议是无状态的,每次请求都是相互独立的,那么如果我们希望几个页面之间的请求是有关联的,比如,A页面是记录用户的信息,B页面是记录用户的订单记录,需要登录才能看到用户的信息,那我们不可能A页面登录后B页面再次登录,或者每次请求都带上用户密码。所以session出现了,它可以在一次会话中解决2次HTTP的请求关联,使客户端与服务器之间保持状态
session的机制:
当不同用户访问站点时,服务端会产生两个SessionID来区分该用户,而客户端会将相应的SessionID存放在cookie中
WebStorage
WebStorge的目的:
- 提供一种在cookie之外存储会话数据的途径
- 提供一种存储大量可以跨会话存在的数据机制
WebStorage分为两种:sessionStorage和localStorage,sessionStorage将数据保存在session中,浏览器关闭也就没了;而localStorage则一直将数据保存在客户端本地,除非主动删除数据。
localStorage:
- 生命周期:关闭浏览器后数据依然保留,除非手动清除,否则一直在
- 作用域:相同浏览器的不同标签在同源情况下可以共享localStorage
sessionStorage:
- 生命周期:关闭浏览器或者标签后即失效
- 作用域:只在当前标签可用,当前标签的iframe中且同源可共享
GET和POST
- GET把参数包含在URL上,POST通过request body传递参数
- GET在浏览器回退时是无害的,而POST会再次提交请求
- GET产生的URL地址可以被bookmark,而POST不行
- GET请求会被浏览器主动cache,而POST不会,除非手动设置
- GET请求只能进行URL编码,而POST支持多种编码方式
- GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留
- GET请求在URL中传送的参数是有长度限制的,而POST没有
- 对于参数的数据类型,GET只接受ASCII字符,而POST没有限制
- GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息
GET和POST还有一个重大区别,GET产生一个TCP数据包,而POST产生两个,这是因为,对于GET请求,浏览器会把http head和data一并发送出去,服务器响应200;而对于POST来说,浏览器先发送header,服务器响应100,浏览器再发送data,服务器响应200
缓存的优缺点
优点:
- 减少了不必要的数据传输,节省宽带
- 减少服务器的负担,提升网站性能
- 加快了客户端加载网页的速度
- 用户体验友好
缺点:
- 资源如果由更改但是客户端不及时更新会造成用户获取信息滞后
强缓存
强缓存的强指的是强制,当浏览器去请求某个文件的时候,服务端就在response header里面对该文件做了缓存配置,缓存的时间、缓存类型都由服务端控制,具体表现为response header的cache-control
强缓存就是给资源设置个过期时间,客户端每次请求资源都会看是否过期,只有过期了才会去询问服务器
cache-control:
1. max-age=xxx, public
客户端和代理服务器都可以缓存该资源;客户端在xxx秒的有效期内,如果由请求该资源的需求就直接读取缓存,status code为200,如果客户端做了刷新操作,就向服务器发起HTTP请求
2. max-age=xxx, private
只让客户端可以缓存该资源,代理服务器不缓存,客户端在xxx秒内直接读取缓存,status code为200
3. max-age=xxx, immutable
客户端在xxx秒的有效期内,如果请求该资源的需求的话就直接读取缓存,status code为200,即使用户做了刷新操作,也不向服务器发送HTTP请求
4. no-cache
跳过设置强缓存,但是不妨碍设置协商缓存;一般如果你做了强缓存,只有在强缓存失效了才走协商缓存,设置了no-cache就不会走强缓存,每次请求都会询问服务端
5. no-store
不缓存,这个会让客户端、服务器都不缓存,也就没有所谓的强缓存和协商缓存了
协商缓存
当用户给资源设置了过期时间,并且在过期后请求该资源时,这时会去请求服务器,这时候去请求服务器的过程就可以设置协商缓存,协商缓存时需要客户端和服务器两端进行交互的
设置协商缓存:
协商缓存的设置只需要在response header里面设置etag和last-modified即可
etag:
每一个文件都有一个,文件发生改变就会变,就是个文件的hash,每一个文件唯一
last-modified:
文件的修改时间,精确到秒
每次请求返回的response header中的etag和last-modified,在下次请求时request header就把这俩带上,服务器作比较,判断资源是否修改,如果资源不变,那就不变etag和last-modified,这时对客户端来说每次请求都是要进行协商缓存,请求过程:
发请求--> 看资源是否过期-->过期-->请求服务器-->服务器对比资源是否真的过期-->没过期-->返回304-->客户端用缓存的老资源
发请求-->看资源是否过期-->过期-->请求服务器-->服务器对比资源是否真的过期-->过期-->返回200-->客户端跟第一次接收资源一样,记下cache-control、etag和last-modified
补充一点:
request header的if-none-matched和if-modified-since就是和response header的etag和last-modified相对应的
etag的作用:
etag的出现后于last-modified,它的出现是为了解决last-modified的一些问题:
- 有些文件也许会周期性的更改,但是它的内容并没有改变,仅仅只是修改了更新时间
- 有些文件修改十分频繁,在1秒内修改多次,而if-modified-since的精度只有秒级
- 有些服务器不能精确的得到文件的最后修改时间
如此看来,etag的准确度似乎比last-modified还要高,那为什么etag不完全替代last-modified呢?原因就是etag是hash值,有可能存在哈希碰撞, 因此etag和last-modified两个同时验证准确度最高
跨域
url的组成
http://www.example.com:8080/path/file.html?key1=val1&key2=val2#SomewhereInTheDocument
url是由协议、域名、端口、请求路径、请求参数和锚点组成的,其中,只有当协议、域名和端口完全一致才是同源
跨域的解决办法:
JSONP
JSONP的实现是基于script标签允许在别的源请求脚本,因为HTML5的type默认是text/javascript,因此获取数据会被当作js代码执行,所以后端需要在以json为格式的数据外,包裹一个函数,然后再外包一层传输用的JSON格式,函数的名字需要在前端设置好,因此需要在url的请求参数部分加上函数的名字,因为一般都是用js函数包裹json数据,因此称为JSON with Padding
【局限性】
只能使用GET方法请求
CORS
跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的 Web 应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器「不同的域、协议或端口」请求一个资源时,资源会发起一个「跨域 HTTP 请求」。
反向代理
前端不直接访问服务器,而是通过反向代理访问服务器,因为方向代理与服务器不受浏览器同源政策的影响,因此可以直接访问,只需要将浏览器与反向代理设置成同源即可
TCP
三次握手
三次握手,客户端先向服务端发起一个SYN包,进入SYN_SENT状态,服务端收到SYN后,给客户端返回一个ACK+SYN包,表示已收到SYN,并进入SYN_RECEIVE状态,最后客户端再向服务端发送一个ACK包表示确认,双方进入establish状态。
之所以是三次握手而不是两次,是因为如果只有两次,在服务端收到SYN后,向客户端返回一个ACK确认就进入establish状态,万一这个请求中间遇到网络情况而没有传给客户端,客户端一直是等待状态,后面服务端发送的信息客户端也接受不到了。
四次挥手
之所以等待两个周期是因为最后客户端发送的ACK包可能会丢失,如果不等待2个周期的话,服务端在没收收到ACK包之前,会不停的重复发送FIN包而不关闭,所以得等待两个周期
TCP与UDP:
- 连接方面:TCP面向连接,UDP不需要连接
- 可靠性:TCP是可靠传输,一旦传出过程中丢包的话会进行重传;UDP是不可靠传输,但会最大努力交付
- 工作效率:UDP实时性高,比TCP工作效率高
- 支持多对多:TCP是点对点的,UDP支持一对一、一对多和多对多
- 首部大小:TCP首部占20字节,UDP首部占8字节
TCP可靠传输:
- 校验和
- 序列号和确认应答
- 超时重传、滑动窗口和拥塞控制
TCP的四元组:
- 四元组:源IP地址、目标IP地址、源端口、目标端口
- 五元组:源IP地址、目标IP地址、源端口、目标端口、协议号
- 七元组:源IP地址、目标IP地址、源端口、目标端口、协议号、服务类型、接口索引
HTTP和HTTPS
区别:
- HTTP是超文本传输协议,信息是明文传输,存在安全风险。HTTPS则解决HTTP不安全的缺陷,在TCP和HTTP网络层之间加入SSL/TLS安全协议,使得报文能够加密传输
- HTTP建立连接相对简单,TCP三次握手之后便可以进行HTTP的报文传输。而HTTPS在TCP三次握手之后,还需要进行SSL /TLS安全协议,才可进入加密报文传输
- HTTP的端口号是80,HTTPS的端口号是443
- HTTPS协议需要向CA(证书权威机构)申请数字证书,来保证服务器的身份是可信的
HTTPS实现原理:
- 首先客户端向服务端发送一个随机值和一个客户端支持的加密算法,并连接到443端口。
- 服务端收到以后,会返回另外一个随机值和一个协商好的加密算法,这个算法是刚才发送的那个算法的子集
- 随后服务端会再次发送一个 CA 证书,这个 CA 证书实际上就是一个公钥,包含了一些信息(比如颁发机构和有效时间等)
- 客户端收到以后会验证这个 CA 证书,比如验证是否过期,是否有效等等,如果验证未通过,会弹窗报错。
- 如果验证成功,会生成一个随机值作为预主密钥,客户端使用刚才两个随机值和这个预主密钥组装成会话密钥;再使用刚才服务端发来的公钥进行加密发送给服务端;这个过程是一个非对称加密(公钥加密,私钥解密)
- 服务端收到以后使用私钥解密,随后得到那两个随机值和预主密钥,随后再组装成会话密钥。
- 客户端在向服务端发起一条信息,这条信息使用会话秘钥加密,用来验证服务端是否能收到加密的信息
- 服务端收到以后使用刚才的会话密钥解密,在返回一个会话密钥加密的信息,双方收到以后 SSL 建立完成;这个过程是对称加密(加密和解密是同一个)
服务器渲染和客户端渲染
互联网早期,用户使用浏览器浏览的都是一些没有复杂逻辑的、简单的页面,这些页面都是在后端将html拼接好的然后将之返回给前端完整的html文件,浏览器拿到这个html文件之后就可以直接解析展示了,而这也就是所谓的服务器端渲染了。而随着前端页面的复杂性提高,前端就不仅仅是普通的页面展示了,而可能添加了更多功能性的组件,复杂性更大,另外,彼时ajax的兴起,使得业界就开始推崇前后端分离的开发模式,即后端不提供完整的html页面,而是提供一些api使得前端可以获取到json数据,然后前端拿到json数据之后再在前端进行html页面的拼接,然后展示在浏览器上,这就是所谓的客户端渲染了,这样前端就可以专注UI的开发,后端专注于逻辑的开发。
服务器渲染(SSR Server Site Rendering):
- 有利于SEO,首屏加载快,但是重复请求次数多,开发效率低,服务器压力大
- 渲染的时候返回的是完整的html格式
客户端渲染(CSR Client Site Rendering):
- 不利于SEO,首屏加载慢,前后端分离开发,交互速度快,体验好
- 渲染的时候返回的是json数据格式,由浏览器完成渲染
https://www.cnblogs.com/zhuzhenwei918/p/8795945.html
JWT(JSON Web Token)
在JWT出现之前,验证客户端的方式就是通过token,方式如下:
- 用户输入账号密码后,向服务端发起请求,服务端生成token返回给客户端,然后下次客户端请求数据的时候会携带token,服务器收到之后,会与之前保存的token进行验证,验证通过后返回数据
- 如果有大量用户请求数据,服务端要保存很多token,压力巨大
JWT相当于把数据转化为了JSON对象,分为三个部分:头部、负载和签名,之间用点(.)连接形成一个字符串
头部(header):
保存JWT的元数据,表明自己所使用的hash算法,以JSON对象形式存储,转化为Base64URL格式
{
"alg": "HS256",
"typ": "JWT"
}
负载(payload):
用来存放自己的数据,包含需要传递的数据,JWT指定默认七个字段供选择
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT
签名(signature):
由三部分组成:header(base64后)、payload(base64)和secret ,header和payload之间用点(.)连接组成字符串,然后通过header声明的加密方式和算法的密钥(secret)加密,形成一个签名。secret是保存在服务器中的,是服务端的私钥,在任何场景都不应该流露出去
CDN缓存
如果接入CDN的话,DNS解析权也是会改变的,会把DNS解析权交给CNAME指向的CDN专用DNS服务器
其次就是缓存问题,正常情况下,浏览器在发起请求之前,会先查看本地缓存,如果过期的话再去向服务端发起请求
如果使用了CDN,浏览器会先查看本地缓存,如果未命中,会向CDN边缘节点发起请求,如果没有过期直接返回数据,如果过期,则CDN还需要向源站发起回源请求,来拉取最新数据
从浏览器输入url后都经历了什么
- 浏览器查找是否存在缓存,并比较缓存是否过期
- DNS解析URL对象的IP
- 根据IP建立TCP连接
- HTTP发起请求
- 服务器处理请求,浏览器接收HTTP响应
- 渲染页面,构建DOM树
- 关闭TCP连接
算法
Fisher-Yates shuffle
// 洗牌算法
// 打乱数组内元素的顺序
const arr = [1, 2, 3, 4, 5]
let length = arr.length, index
for (let i=length-1; i>=0; i--) {
index = Math.floor(Math.random() * i)
[arr[i], arr[index]] = [arr[index], arr[i]]
}
Vue
vue的基本概念 ![](https://i-blog.csdnimg.cn/blog_migrate/fc856ebf3e5d755421dedeab4077c247.webp?x-image-process=image/format,png)
一个Vue需要运行起来,需要模板通过编译生成AST,再由AST生成Vue的render函数(渲染函数),渲染函数结合数据生成Virtual DOM树(虚拟DOM),Diff和Patch后生成新的UI
- 模板:Vue的模板基于纯HTML,基于Vue的模板语法,我们可以比较方便地声明数据和UI的关系
- AST:AST是Abstract Syntax Tree(抽象语法树)的简称,Vue使用HTML的Parser(解析器)将HTML模板解析为AST,并且对AST进行一些优化的标记处理,提取最大的静态树,方便Virtual DOM时直接跳过Diff
- Virtual DOM:虚拟DOM树,Vue的Virtual DOM Patching算法是基于Suabbdom的实现,并在些基础上作出了很多的调整和改进
- Watcher:每个Vue组件都有一个对应的watcher,这个watcher将会在组件render的时候收集所依赖的数据,并在依赖有更新的时候,触发组件重新渲染
上图中,render函数可以作为一道分割线,render函数的左边可以称为编译期,将Vue的模板转换为渲染函数,render函数的右边是Vue的运行期,主要是基于渲染函数生成的Virtual DOM树,Diff和Patch
Vue的渲染流程:
- new Vue,执行初始化
- 挂载$mount方法,通过自定义render方法、template、el等生成render函数
- 通过watcher监听数据的变化
- 当数据发生变化的时候,render函数执行生成VNode对象
- 通过patch方法,对比新旧VNode对象,通过DOM Diff算法,添加、修改、删除真正的DOM元素
reder函数
每一个元素都是一个DOM节点,每段文字甚至注释也是,高效更新这些节点十分困难,不过我们只需要告诉Vue我们希望页面上的HTML是什么,这可以是在一个模板里:
<h1>{{title}}</h1>
或者在一个渲染函数里:
reder: function (createElement) {
return createElement('h1', this.title)
}
当我们的数据title发生变化的时候,vue会自动更新页面
createElement返回的不是一个实际的DOM元素,而是一个虚拟节点Virtual Node,简称VNode,虚拟DOM就是我们对由Vue组件树建立起来的整个VNode树的称呼
createElement:
在Vue中,h被视为createElement的别名,const h = this.$createElement,因此render函数也可以写作:
reder: h => {
return h(...)
}
下列代码来自Vue官网,展现了createElement函数的参数:
// @returns {VNode}
createElement(
// {String | Object | Function}
// 一个 HTML 标签名、组件选项对象,或者
// resolve 了上述任何一种的一个 async 函数。必填项。
'div',
// {Object}
// 一个与模板中 attribute 对应的数据对象。可选。
{
// 与 `v-bind:class` 的 API 相同,
// 接受一个字符串、对象或字符串和对象组成的数组
'class': {
foo: true,
bar: false
},
// 与 `v-bind:style` 的 API 相同,
// 接受一个字符串、对象,或对象组成的数组
style: {
color: 'red',
fontSize: '14px'
},
// 普通的 HTML attribute
attrs: {
id: 'foo'
},
// 组件 prop
props: {
myProp: 'bar'
},
// DOM property
domProps: {
innerHTML: 'baz'
},
// 事件监听器在 `on` 内,
// 但不再支持如 `v-on:keyup.enter` 这样的修饰器。
// 需要在处理函数中手动检查 keyCode。
on: {
click: this.clickHandler
},
// 仅用于组件,用于监听原生事件,而不是组件内部使用
// `vm.$emit` 触发的事件。
nativeOn: {
click: this.nativeClickHandler
},
// 自定义指令。注意,你无法对 `binding` 中的 `oldValue`
// 赋值,因为 Vue 已经自动为你进行了同步。
directives: [
{
name: 'my-custom-directive',
value: '2',
expression: '1 + 1',
arg: 'foo',
modifiers: {
bar: true
}
}
],
// 作用域插槽的格式为
// { name: props => VNode | Array<VNode> }
scopedSlots: {
default: props => createElement('span', props.text)
},
// 如果组件是其它组件的子组件,需为插槽指定名称
slot: 'name-of-slot',
// 其它特殊顶层 property
key: 'myKey',
ref: 'myRef',
// 如果你在渲染函数中给多个元素都应用了相同的 ref 名,
// 那么 `$refs.myRef` 会变成一个数组。
refInFor: true
},
// {String | Array}
// 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
// 也可以使用字符串来生成“文本虚拟节点”。可选。
[
'先写一些文字',
createElement('h1', '一则头条'),
createElement(MyComponent, {
props: {
someProp: 'foobar'
}
})
]
)
虚拟DOM、Patch、Diff算法
虚拟DOM:
使用原生的js去操作DOM是十分麻烦的一件事,并且一遍又一遍的渲染DOM是非常消耗性能的事,因此出现了虚拟DOM
虚拟DOM实际上是JavaScript对象,是对真实DOM的抽象,该对象包含了真实DOM中的结构及其属性,用于对比和真实DOM的差异,从而进行局部渲染来达到优化性能的目的
真实元素节点:
<div id="app">
<p class="title">Hello world!</p>
</div>
虚拟DOM节点:
VNode = {
tag: 'div',
attrs: {
id: 'app'
},
children: [
{
tag: 'p',
text: 'Hello world!',
attrs: {
class: 'title'
}
}
]
}
Patch和Diff算法:
一开始Vue回根据真实DOM生成虚拟DOM,当虚拟DOM某个节点的数据改变后会生成一个新的VNode,然后VNode和oldVNode对比,把不同的地方修改在真实DOM上,最后再使得oldVNode的值为VNode
Diff的过程就是调用patch函数,比较新老节点,一边比较一边打补丁(patch)
Patch源码:
//patch函数 oldVnode:老节点 vnode:新节点
function patch (oldVnode, vnode) {
...
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode) //如果新老节点是同一节点,那么进一步通过patchVnode来比较子节点
} else {
/* -----否则新节点直接替换老节点----- */
const oEl = oldVnode.el // 当前oldVnode对应的真实元素节点
let parentEle = api.parentNode(oEl) // 父元素
createEle(vnode) // 根据Vnode生成新元素
if (parentEle !== null) {
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素
api.removeChild(parentEle, oldVnode.el) // 移除以前的旧元素节点
oldVnode = null
}
}
...
return vnode
}
//判断两节点是否为同一节点
function sameVnode (a, b) {
return (
a.key === b.key && // key值
a.tag === b.tag && // 标签名
a.isComment === b.isComment && // 是否为注释节点
// 是否都定义了data,data包含一些具体信息,例如onclick , style
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b) // 当标签是<input>的时候,type必须相同
)
}
可以看出,patch函数大致分为两种情况:
- 如果是同一节点,执行patchVnode进行子节点比较
- 如果不是,新节点直接替换老节点
因为Diff是同层比较,不存在跨级比较,因此如果不是同一节点,子节点一样也视为情况二
patchVnode:
function patchVnode (oldVnode, vnode) {
const el = vnode.el = oldVnode.el //找到对应的真实DOM
let i, oldCh = oldVnode.children, ch = vnode.children
if (oldVnode === vnode) return //如果新老节点相同,直接返回
if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
//如果新老节点都有文本节点且不相等,那么新节点的文本节点替换老节点的文本节点
api.setTextContent(el, vnode.text)
}else {
updateEle(el, vnode, oldVnode)
if (oldCh && ch && oldCh !== ch) {
//如果新老节点都有子节点,执行updateChildren比较子节点[很重要也很复杂,下面展开介绍]
updateChildren(el, oldCh, ch)
}else if (ch){
//如果新节点有子节点而老节点没有子节点,那么将新节点的子节点添加到老节点上
createEle(vnode)
}else if (oldCh){
//如果新节点没有子节点而老节点有子节点,那么删除老节点的子节点
api.removeChildren(el)
}
}
}
从上述代码可以看出,当两个节点为同一节点时:
- 新老节点一样,直接返回
- 老节点有子节点,新节点没有:删除老节点的子节点
- 老节点没有子节点,新节点有:新节点的子节点直接append到老节点
- 都只有文本节点:直接用新节点的文本节点替换老的文本节点
- 都有子节点:updateChildren
updateChildren:
updateChildren (parentElm, oldCh, newCh) {
let oldStartIdx = 0, newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx
let idxInOld
let elmToMove
let before
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) { // 对于vnode.key的比较,会把oldVnode = null
oldStartVnode = oldCh[++oldStartIdx]
}else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx]
}else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx]
}else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx]
}else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
}else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
}else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode)
api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
}else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode)
api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
}else {
// 使用key时的比较
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表
}
idxInOld = oldKeyToIdx[newStartVnode.key]
if (!idxInOld) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
newStartVnode = newCh[++newStartIdx]
}
else {
elmToMove = oldCh[idxInOld]
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
}else {
patchVnode(elmToMove, newStartVnode)
oldCh[idxInOld] = null
api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
}
newStartVnode = newCh[++newStartIdx]
}
}
}
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
}else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
在新老两个VNode节点的头尾都有一个指针
在遍历的时候,oldStartVnode、oldEndVnode与newStartVnode、newEndVnode会先进行两两比较,一共有四种比较方式,当其中两个能匹配上那么真实DOM中相应节点会移到VNode相应的位置:
- oldStartVnode和newStartVnode匹配,位置不动,oldStartIdx,newStartIdx指针后移
- oldEndVnode和newEndVnode匹配,位置不动,oldEndIdx,newEndIdx指针前移
- oldStartVnode和newEndVnode匹配,oldStartVnode移动到newEndVnode所在的位置, oldStartIdx指针前移,newEndIdx指针后移
- oldEndVnode和newStartIdx匹配,oldEndVnode移动到newStartVnode所在位置, oldEndIdx指针后移,newStartIdx指针前移
此时已完成了新旧节点首位子节点的匹配,倘若以上4种方式都没能匹配上,如果设置了key,就会用key进行比较,遍历剩下的节点,如果在newVNode中找到一致key的旧的VNode节点,并且同时满足sameVnode,patchVnode,那么这个节点将得到复用。
key的作用为:
- 决定节点是否可以复用
- 建立key-index的索引,主要是替代遍历,提升性能
最后通过指针位置来判断oldCh和newCh哪一个先遍历完,oldStartIdx > oldEndIdx表示oldCh先遍历完,那么将多余的vCh根据index添加到DOM中;newStartIdx > newEndIdx表示newCh先遍历完,那么就在DOM中删除多余的节点
参考:掘金
双向数据绑定
Vue是一个MVVM框架,即数据双向绑定,即当数据发生变化的时候,视图也就发生变化,当视图发生变化的时候,数据也会跟着同步变化。在Vue中,如果使用vuex,实际上数据还是单向的,之所以说数据双向绑定,这是相对于UI控件来说的,对于我们处理表单,vue的双向数据绑定用起来会十分舒服
Vue是采用数据劫持结合发布者-订阅者模式的方式,通过Object.definePropoty()来劫持各个属性的getter,setter,在数据变动时发布消息给订阅者,触发相应的监听回调
实现双向数据绑定主要包含两个方面,数据变化更新视图,视图变化更新数据。view更新data可以通过事件的监听;data更新view,则需要通过Object.defineProperty()对属性设置一个set函数,当数据改变了就会触发这个函数,所以只需要将一些更新的方法放在里面就可以实现data更新view了。
MVVM实现过程:
设置一个数据监听器Observer,用来监听所有属性。如果属性发生了变化,就需要告诉订阅者Watcher看是否需要更新。因为订阅者是有很多个的,所以我们需要一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理的。接着,还需要有一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令对应初始化为一个订阅者Watcher,并替换模板数据或者绑定的相应函数,此时当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。因此实现数据的双向绑定需要三个步骤:
- 实现一个监听器Observer,用来劫持并监听所有属性,如果有变动,就通知订阅者
- 实现一个订阅者Watcher,用来收到属性的变化通知并执行相应的函数,从而更新视图
- 实现一个解析器Compile,用来扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器
Object.defineProperty
通过Object.defineProperty()为对象定义属性有两种形式:
- 数据描述符(特有属性:value,writable)
- 存取描述符(是由一对getter、setter函数功能来描述的属性)
生命周期
beforeCreate:
创建之前,此时还没有data和method
created:
实例已经创建完成之后被调用,实例已完成以下配置:数据观测、属性和方法的以运算,watch/event事件回调,完成了data数据的初始化,el没有。在这个生命周期中可以调用methods中方法,改变data中的数据
beforeMounte:
在渲染之前,实例完成以下配置:编译模板,把data里面的数据和模板生成html,完成了el和data初始化,此时还没有挂载html到页面上
mounted:
页面已经渲染完成,并且vm实例中已经添加完$el了,已经替换掉那些DOM元素(双括号中的变量),这个时候可以操作DOM,但是要获取元素高度等属性需要使用nextTick(),mounted只执行一次
beforeUpdate:
data改变之后,对应的组件重新渲染之前
updated:
data改变之后,对应的组件重新渲染完成
beforeDestroy:
在实例销毁之前,此时实例仍可以使用
destroyed:
实例销毁后
vue-router
传统的页面应用,是用一些超链接来实现页面切换和跳转的,在vue-router单页面应用中,则是路径之间的切换,也就是组件的切换,路由模块的本质,就是建立起url和页面之间的映射关系
至于我们为啥不能用a标签,这是因为用Vue做的都是单页应用(当你的项目准备打包时,运行npm run build
时,就会生成dist文件夹,这里面只有静态资源和一个index.html页面),所以你写的<a></a>标签是不起作用的,你必须使用vue-router来进行管理。
原理:
SPA:单一页面应用程序,只有一个完整的页面;它在加载页面时,不会加载整个页面,而是只更新某个指定的容器中内容。SPA的核心之一是:更新视图而不重新请求页面;
hash模式:
vue-router默认hash模式,使用url的hash来模拟一个完整的url,于是当url改变的时候,页面不会重新加载。hash(#)是url的锚点,代表网页中的一个位置,单单改变#后的部分,浏览器只会滚到相应位置,不会重新加载网页,同时会在浏览器的访问历史里增加一个记录。原理是onchange事件(检测hash值变化),可以在window对象监听这个事件
history模式:
由于hash模式会在url中自带#,如果不想要很丑的 hash,我们可以用路由的 history 模式,只需要在配置路由规则时,加入"mode: 'history'",这种模式充分利用了html5 history interface 中新增的 pushState() 和 replaceState() 方法。这两个方法应用于浏览器记录栈,在当前已有的 back、forward、go 基础之上,它们提供了对历史记录修改的功能。只是当它们执行修改时,虽然改变了当前的 URL ,但浏览器不会立即向后端发送请求。
不过这种模式要玩好,还需要后台配置支持。因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问 http://oursite.com/user/id 就会返回 404,这就不好看了。
所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。
$route:
$route是路由信息对象,包括path、params、hash、query、fullPath、matched、name等路由信息参数
$router:
$router是路由实例对象,即使用new VueRouter创建的实例,包括路由跳转方法,钩子函数等
$router.push和$router.replace的区别:push方法会向history栈中添加新纪录,而replace是替换当前的history记录
导航钩子:
vue-router的导航钩子,主要作用是拦截导航,让他完成跳转或取消
【全局导航钩子】
- 前置守卫:beforeEach((to, from, next)=>{})
- 解析守卫:beforeResolve
- 后置钩子:afterEach()
【路由独享守卫】
- beforeEnter:只在进入路由时触发
【组件内的守卫】
- beforeRouteEnter
- beforeRouteUpdate
- beforeRouteLeave
const UserDetails = {
template: `...`,
beforeRouteEnter(to, from) {
// 在渲染该组件的对应路由被验证前调用
// 不能获取组件实例 `this` !
// 因为当守卫执行时,组件实例还没被创建!
},
beforeRouteUpdate(to, from) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
// 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
},
beforeRouteLeave(to, from) {
// 在导航离开渲染该组件的对应路由时调用
// 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
},
}
完整的导航解析流程:
- 导航被激活
- 在失活的组件里调用beforeRouteLeave守卫
- 调用全局的beforeEach守卫
- 在重用的组件里调用beforeRouteUpdate守卫
- 在路由配置里调用beforeEnter
- 解析异步路由组件
- 在被激活的组件里调用beforeRouteEnter守卫
- 调用全局的beforeResolve守卫
- 导航被确认
- 调用全局的afterEach守卫
- 触发Dom更新
- 调用beforeRouteEnter守卫中传给next的回调函数,创建好的组件实例会作为回调函数的参数传入
Vuex
Vuex是实现组件全局状态(数据)管理的以中机制,可以方便的实现组件之间数据的共享
优点:
- 能够在Vuex中集中管理共享的数据,易于开发和后期维护
- 能够高效地实现组件之间的数据共享,提高开发效率
- 存储在Vuex中的数据都是响应式的,能够实现保持数据与页面的同步
如果是小型单页面开发,就不太推荐使用Vuex
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Viex.Store({
})
state:
所有共享的数据都要同意放在Store中的state中进行存储
访问方式有两种:
- this.$store.state.全局数据名称
- 从vuex中按需引入mapState函数,将当前组件需要的全局数据,映射为当前组件的computed计算属性
import { mapState } from 'vuex' computed : { ...mapState(['数据名称']) }
mutation:
Mutation用于更改Store中的数据
- 只能通过mutation变更store数据,不可以直接操作store中的数据
- 通过这种方式,可以集中监控所有数据变化。直接操作store数据是无法进行监控的
mutations: {
// 自增
add(state) {
state.count++
},
// 增加一定量
addNum(state, payload) {
state.count += payload.number
}
}
在mutations中定义的函数会有一个默认参数state,这个就是存储在Store中的state对象,有时需要携带一些额外的参数,此处参数被称为mutation的载荷payload,如果仅有一个参数时,那payload就是这个参数值,如果时多个参数,那就会以对象的形式传递
Vuex的store中的State是响应式的,当State中的数据发生改变时,Vue组件也会自动更新,所以我们需要提前在store中初始化好所需的属性,当给state中的对象添加新的属性时,使用如下方法:
- Vue.set(obj, 'newProp', 'propValue')
- 用新对象给旧对象重新赋值
updateUserInfo(state) {
// 方式一
Vue.set('user', 'address', 'shenzhen')
// 方式二
state.user = {
...state.user,
'address': 'shenzhen'
}
}
调用mutation有两种方法:
// 方法一
this.$store.commit(方法名)
// 方法二
import { mapMutations } from 'vuex'
// ...
methods: {
...mapMutations('add', 'addN'),
// 当前组件设置的click方法
addCount() {
this.add()
}
}
action:
Action类似于Mutation,但是是用于处理异步任务的,比如网络请求等,如果通过异步操作变更数据,必须通过Action,而不能使用Mutation,但在Action中还是通过触发Mutation的方式间接变更数据
在actions中定义方法,都会有默认值context
- context是和store对象具有相同方法和属性的对象,但不是同一个
- 可以通过context进行commit相关操作,可以获取context.state数据
export default new Vuex.Store({
state: {
count: 0
},
//只有 mutations 中定义的函数,才有权力修改 state 中的数据
mutations: {
// 自增
add(state) {
state.count++
}
},
actions: {
addAsync(context) {
setTimeout(() => {
//在 action 中,不能直接修改 state 中的数据
//必须通过 context.commit() 触发某个 mutation 才行
context.commit('add')
}, 1000);
}
}
})
调用方法:
- this.$store.dispatch(方法名)
- 导入mapActions函数
getter:
Getters用于对Store中的数据进行加工处理成新的数据,类似于Vue中的计算属性
Store中数据发生变化,Getters的数据也会跟随变化
//定义 Getter
const store = new Vuex.Store({
state:{
count: 0
},
getters:{
showNum(state){
return '当前Count值为:['+state.count']'
}
}
})
调用方法:
- this.$store.getters.名称
- 导入mapGetters 函数,将需要的getters函数映射为当前组件的computed方法
modules:
Module
是模块的意思,为什么会在Vuex
中使用模块呢?
-
Vues
使用单一状态树,意味着很多状态都会交给Vuex
来管理 -
当应用变的非常复杂时,
Store
对象就可能变的相当臃肿 -
为解决这个问题,
Vuex
允许我们将store
分割成模块(Module)
,并且每个模块拥有自己的State、Mutation、Actions、Getters
等
浏览器
渲染引擎工作流程