对象
let a = {x:100}
let b = {y:200}
let obj = {}
obj[a] = '珠峰'
obj[b] = "培训"
console.log(obj)
// obj输出:
// {
// '[object Object]':'培训'
// }
而数组对象调用toString是调用Array原型上toString
[].toString() // => ""
// 对象的属性名可以是数字、boolean
let newObj = {
0:100,
true:"cyj"
}
结论:对象的属性名一定不是引用类型, 只能是基本数据类型
/**
* 编辑器
* 词法解析
* AST抽象语法树
* 构建出浏览器能够执行的代码
* 引擎(v8 / webkit内核)
* 变量提升
* 作用域 / 闭包
* 变量对象
* 堆栈对象
* 堆栈内存
* GO/VO/AO/EC/ECStack
*/
浏览器会在计算机中开辟一块内存,专门提供代码执行的 => 栈内存
ECStack: excution context stack 执行环境栈 也是栈内存
栈内存:提供代码的执行环境
堆内存:存放属性和方法
== 在进行比较的时候,如果左右两边数据类型不相等,先转化为相同的类型,在进行比较
对象 == 字符串 对象转化为字符串
- null == undefined true (三个等号下不相等),但是和其他任何不相等
- 0 == null false
- 剩下的情况都是转化为数字在进行比较
[] == false 判断true还是false // -> false
[].toString() -> 0
Number(false) -> 0
![] == false // -> false
![] -> 转化为布尔值进行取反(只有 0、""、NaN、undefined、null)五个是false,其余都是true
![] -> 变成了false
变量提升
浏览器为了能够让代码自上而下的执行,首先会开辟一块内存空间,供代码执行,这块区域也叫执行环境栈、执行上下文
在当前上下文中(全局/私有/块级),JS代码自上而下执行之前,浏览器会提前处理一些事情(可以理解成词法解析的一个环节,词法解析一定发生在代码解析之前)
会把带var和function的 -> 变量提升
-
带var的会提前声明( declare ),
-
带function的会提前声明并定义
console.log(a) // -> undefined
var a = 12 // 创建值12 不需要在声明a了(变量提升阶段完成了)
a = 13 // 创建值13
console.log(a) // -> 13
Identifier 'a' has already been declared
// 全局上下文中的变量提升
fn() // -> 不会报错
function fn(){
var a = 12 // 不会执行,因为不在当前上下文
console.log('ok')
}
a = 13
console.log(a) // -> 13
console.log(window.a) // -> 13
//-----------------------------------------------//
var a = 13
console.log(a) // -> 13
console.log(window.a) // -> 13
// -------------------------------------------- //
var a = 13
var a = 14
console.log(a) // -> 14 var能重复声明
// ------------------------------------------- //
let a = 13
let a = 14 // Uncaught SyntaxError: Identifier 'a' has already been declared
console.log(a) // -> let 不允许重复声明
总结
-
在相同的作用域中(或执行上下文中)如果用var/function关键词声明并且重复声明,是不会有影响的(声明第一次后,之后遇到就u不重复声明了)
2. 但是使用let/const就不行,浏览器会校验当前作用域中是否已存在这个变量了,如果已经存在了,则再次基于let等声明就会报错
console.log(1) // 第一行都不会执行
let a = 14
console.log(a)
let a = 15
console.log(a)
// 会直接报错,并不会输出1和14
// -> 浏览器在开辟栈内存供代码自上而下执行之前,不仅有变量提升的操作,还有很多其他的操作-> “词法解析”或者“词法检测”:就是检测当前即将要执行的代码是否会出现"语法错误(SyntaxError)",如果出现,代码就不会执行(第一行都不会执行)
console.log(1) // -> 1
console.log(a) // -> Uncaught ReferenceError: Cannot access 'a' before initialization
let a = 12 // ReferenceError这是引用错误,而词法解析只检查语法错误
console.log(a) // -> Uncaught SyntaxError: Identifier 'a' has already been declared
var a = 1
let a = 2
console.log(a)
// -> 所谓重复是:不管之前通过什么办法,只要当前栈内存中存在了这个比纳凉,我们使用let/const等重复声明这个变量就是无法错误 -> 无法通过词法解析
fn()
function fn(){ console.log(1) }
fn()
function fn(){ console.log(2) }
fn()
let fn = function (){ console.log(3) }
fn()
function fn(){ console.log(4) }
fn()
function fn(){ console.log(5) }
fn()
// 栈内存(全局作用域、全局上下文)
// 5 5 5 3 3 3
console.log(a) // -> undefined
if('fn' in window){
var a = 13
}
console.log(a) // -> undefined
// 全局作用域
// 1. 变量提升
// 不管条件是否成立都会声明这个变量,但是不会赋值,但是在老版本浏览器中,var会提前声明,function会提前声明加定义,不管调教是否成立。而在现在的浏览器中,function只会声明,不会定义
// 例如
console.log(fn) // -> undefined
if('fn' in window){ // -> true
// 条件成立,劲来后的第一件事情是给fn赋值,然后在执行代码
fn() // -> fn
function fn(){
console.log('fn')
}
}
fn() // -> 输出 fn
f = function (){ return true }
g = function (){ return false }
(function(){
if(g() && [] == ![]){
f = function (){ return false }
function g(){ return true }
}
})()
console.lgo(f())
console.lgo(g())
注意
var a = 10,b = 13 // -> 等价于 var a = 10 var b = 13
// 而
var c = d = 10 // -> 等价于 var c = 10, d = 10(d不带var)
来一道题:
console.log(a,b) // undefined undefiend
var a = 12,b = 12; // -> 等价于 var a = 12; var b = 12
function fn(){
console.log(a,b) // -> 等价于 var a = 13; b = 13 函数执行形成的私有作用域里面声明了a为undefined,b没有带var不会变量提升,会在上级作用域中查找b,为12,如果找不到就报错(这就是作用域链查找机制)。所以返回 undefined 12
var a = b = 13 // 这里的a一定是私有的,而b是全局的,把全局的b修改成13
console.log(a,b) // 13 13
}
fn()
console.log(a,b) // 12 13
// 函数执行会形成一个全新的私有栈内存,在栈内存中代码执行的时候,遇到一个变量如果不是自己私有的,会在上级作用域查找,上级没有继续查找,一直找到到window,这种就是作用域链查找的机制
// 函数执行形成的私有栈内存,会把内存中所有的私有变量保护起来,和外面没有任何关系 => 函数执行的这种 保护机制 就是“闭包”
在来一道题
console.log(a,b,c) // undefined undefined undefined
var a = 12,b=13,c=14
function fn(a){ // a为私有变量,b、c在私有作用域中找不到,会在上级查找
console.log(a,b,c) // 10 13 14
a = 100
c = 200 // 全局的c被修改成了200
console.log(a,b,c) // 100 13 200
}
b = fn(10) // function fn没有返回值,所以b为undefined
console.log(a,b,c) // 12 undefined 200
再来一道题,关于作用域链的查找
var n = 1
function fn(){
var n = 5
function f(){
n--
console.log(n)
}
f()
return f
}
var x = fn() // -> 4 ,f里面没有私有变量n,向上一级查找
x()
console.log(n)
// 作用域链查找机制,关键在于如何查找上级作用域
// 1. 从函数创建开始,作用域链就已经形成了,(并不是函数在哪儿执行作用域就是哪儿,并不是的)
// 2. 当前函数是在那个作用域(N)下创建的,那么函数执行形成的作用域(M)的上级作用域就是N(和函数在哪儿执行的没有关系,和在哪儿创建的有关系)
// 例如fn()执行形成的全新的作用域,它的上级作用域就是全局作用域,f函数是在fn里面创建的,所以f()执行形成的全新的作用域就是fn, 而上上级作用域就是全局作用域
浏览器的暂时性死区
console.log(a) // -> Uncaught ReferenceError: a is not defined
// -----------------------------------------------------------------
console.log(typeof a) // -> undefined
// -> 这是浏览器的BUG,本应该报错的,因为没有a这个变量,可以理解成typeof的BUG或浏览器的BUG
// -----------------------------------------------------------------
console.log(typeof a) // -> Uncaught ReferenceError: a is not defined
let a;
// -> let解决了typeof检测时的暂时性死区的问题
let 与 var 的区别
-
var能重复声明,而let不能重复声明
-
var有变量提升,而let没有
-
let能解决typeof检测时出现的暂时性死区的问题(更加严谨)
GO
GO 全局对象window 堆内存 浏览器内置的API
!==
VO(G) 全局变量对象 上下文中的空间 全局上下文中创建的变量
基于VAR/FUNCTION在全局上下文中声明的全局变量也会给GO赋值一份(映射机制)
但是就LET/CONST等ES6方式在全局上下文中创建的全局变量和GO没有关系
浏览器的垃圾回收机制
浏览器的垃圾回收机制(自己内部处理):
[谷歌等浏览器是“基于引用查找“来进行垃圾回收的]
- 开辟的堆内存,浏览器自己默认会在空闲的时候,查找所有内存的引用,把那些不被引用的内存释放掉
- 开辟的栈内存(上下文)一般在代码执行完都会出栈释放,如果遇到上下文中的东西被外部占用,则不会释放
[IE等浏览器是“基于计数器”机制来进行内存管理的] - 创建的内存被引用一次,则计数1,在被引用一次,计数2... 移除引用减去1... 当减为零的时候,浏览器会把内存释放掉
=>真实项目中,某些情况导致计数规则会出现一些问题,造成很多内存不能被释放掉,产生“内存泄漏”;查找引用的方式如果形成相互引用,也会导致“内存泄漏“
闭包
函数执行会形成全新的私有上下文,这个上下文可能被释放,也可能不被释放,不论是否被释放,它的作用是:
-
保护:划分一个独立的代码执行区域,在这个区域中有自己私有变量存储的空间,而用到的私有变量和其它区域中的变量不会有任何的冲突(防止全局变量污染)
-
保存:(防止全局变量污染)如果上下文不被销毁,那么存储的私有变量的值也不会被销毁,可以被其下级上下文中调取使用
我们把函数执行,形成私有上下文,来保存和保护私有变量的机制,称之为“闭包” =>它是一种机制
原型及原型链模式
- 每一个函数都有一个叫prototype的属性,这个属性是一个对象,保存了类的公共属性和方法
- 普通的函数
- 类(自定义类和内置累)
- 在prototype这个对象中,也又一个天生自带的属性教constructor,这个属性存储的是当前函数本身
- 每一个类的实例,都有一个_proto_,它指向类的prototype
function Fn() {}
let f1 = new Fn()
let f2 = new Fn()
f1.say = function(){
console.log('f1 say')
}
Fn.prototype.say = function(){
console.log('Fn say')
}
console.log(f1.say === Fn.prototype.say) // -> false
console.log(f1.__proto__ === Fn.prototype) // -> true
console.log(f1.__proto__.say === Fn.prototype.say) // -> true
let arr1 = [10,20]
let arr2 = [30,40]
arr1.hasOwnProperty() // -> 也是基于原型链查找机制,找到对象基类Object.prototype上的hasOwnProperty方法,然后执行
// 输出document的原型链
dir(document)
document -> HTMLDocument.prototype -> Document.prototype -> Node.prototype -> EventTarget.prtotype -> Object.prototype
JS中THIS的指向问题
function Fn(){
this.x = 100
this.y = 200
this.say = function(){
console.log(this.x)
}
}
Fn.prototype.say = function(){
console.log(this.y)
}
Fn.prototype.eat = function(){
console.log(this.x + this.y)
}
Fn.prototype.write = function(){
this.z = 1000
}
let f1 = new Fn
f1.say() // this:f1 -> console.log(f1.x) -> 100
f1.eat() // this:f1 -> console.log(f1.x + f1.y) -> 300
f1.__proto__.say() // this:f1.__proto__ -> console.log(f1.__proto__.y) 原型上没有y,在通过原型上查找到Object上也没有y,输出 undefined
Fn.prototype.eat() // this:Fn.prototype -> undefined + undefined -> NaN
f1.write() // -> this:f1 -> f1.z = 1000 -> 给f1设置一个私有的属性z=1000
Fn.prototype.write()// this:Fn.prototype -> 给原型上设置一个属性z=1000(属性上实例的公有属性)
/**
* 面向对象当中有关私有/公有方法中的THIS问题
* 1. 方法执行看起那面是否有点,点前面是谁,this就是谁
* 2. 把方法中的THIS进行替换
* 3. 再基于原型链查找的方式确定结果
*/
实现hasPublicProperty
hasOwnProperty能够获取是否为当前类的私有属性,如何实现一个hasPublicProperty
function hasPublicProperty(property){
// 传入的应为基本类型, 并且不为undefined、null
if(!["number","string","boolean"].includes(typeof property)){
return false
}
// 首相property必须在原型上(也就是in为true), 并且hasOwnProperty为false
let m = property in this
let n = this.hasOwnProperty(property)
return m && !n
// return property in this && this.hasOwnProperty(property)
}
Object.prototype.hasPublicProperty = hasPublicProperty
基于constructor实现数据类型检测
let arr = []
console.log(arr.constructor === Array) // -> true
// 但是这种方式有很大的弊端
// 因为用户能去随意的给修改constructor
来一道原型链的题:
function Fn(){
this.x = 100
this.y = 200
this.getX = function(){
console.log(this.x)
}
}
Fn.prototype.getX = function(){
console.log(this.x)
}
Fn.prototype.getY = function(){
console.log(this.y)
}
let f1 = new Fn()
let f2 = new Fn
console.log(f1.getX === f2.getX) // -> f1.getX === f2.getX -> 都是自己私有的方法, 不同的地址 -> false
console.log(f1.getY === f2.getY) // -> 200 === 200 -> true
console.log(f1.__proto__.getY === Fn.prototype.getY) // -> undefined === undefined -> true
console.log(f1.__proto__.getX === f2.getX) // -> undefined === 200 -> false
console.log(f1.getX === Fn.prototype.getX) // -> 100 === Fn.prototype.x undefined -> false
console.log(f1.constructor) // ->
console.log(Fn.prototype.__proto__.constructor)
f1.getX() // this:f1.x -> 100
f1.__proto__.getX() // this:f1.__proto__ -> undefined
console.log(Fn.prototype.getY()) // -> undefined
重构类的原型: 让某个类的原型指向新的对内存地址(重定向指向)
问题:重定向后空间中不一定有constructor属性( 只有浏览器默认给prototype开辟的对内存中才存在constror,这样导致类和原型机制不完善,所以我们需要手动的给新的原型空间设置constructor属性 )
问题:在重定向之前,我们需要确保所有原型的堆内存中没有设置属性和方法,因为重定向后,原有的属性和方法就没有什么用了( 需要额外处理 ) => 但是内置类的原型,是没发修改的,改了没有作用
自写一个new方法
function Fn(x) {
this.x = x
}
function _new(Fn, ...args) {
let obj = Object.create(Fn.prototype)
let res = Fn.call(obj, ...args)
console.log(obj)
console.log(res)
if (
res !== 'null' &&
(typeof res === 'function' || typeof res === 'object')
)
return res
return obj
}
let f1 = _new(Fn, 'cyj')
console.log(f1)
//-----------------------------
// 将类数组转化为数组
let lis = document.getElementsByTagName('li')
console.log(lis)
let arr = Array.prototype.slice.call(lis)
console.log(arr)
自写一个queryURLParams方法
/*
* 编写queryURLParams方法实现如下的效果(至少两种方案)
*/
// let url="http://www.zhufengpeixun.cn/?lx=1&from=wx#video";
// console.log(url.queryURLParams("from")); //=>"wx"
// console.log(url.queryURLParams("_HASH")); //=>"video"
function queryURLParams(key) {
// "lx=1&from=wx#video"
if (key === '_HASH') {
return this.split('#')[1]
}
let params = this.split('?')[1].split('#')[0]
let obj = {}
let paramsArr = params.split('&') // ["lx=1","from=wx"]
for (let i in paramsArr) {
let item = paramsArr[i]
let _key = item.split('=')[0]
let _value = item.split('=')[1]
obj[_key] = _value
}
return obj[key]
}
String.prototype.queryURLParams = queryURLParams
let url = "http://www.cyj.com?name=cyj&age=19#video"
重构slice方法
THIS
每一个函数(普通函数/构造函数/内置类)都是Function这个内置类的实例,所以:函数._proto_ === Function.prototype,函数可以直接调用Function原型上的方法。
call、apply、bind
原型上提供的三个公有属性的方法
call方法
window.name = 'window'
let obj = {
name:'obj'
}
let fn = function(){
console.log(this.name)
}
fn() // -> this: window -> window
fn.call(obj) // -> this: obj -> obj
fn.call() // 非严格模式下,this指向window,严格模式下,this指向undefined
call方法的第一个参数,是改变方法的this,其余参数是前面fn的参数,例如:
let fn = function(a,b){
console.log(a)
console.log(b)
console.log(this.a)
console.log(this.b)
}
let obj = {
a:1,
b:2
}
fn.call(obj,4,5) // this:obj -> 4 5 1 2
重构call方法
~(function () {
/**
* call: 改变函数中this的指向
*/
function call(context, ...args) {
// this: fn
context = context || window
let result
context.$fn = this
result = context.$fn(...args)
delete context.$fn
return result
}
Function.prototype.call = call
})()
let obj = {
name: 'obj',
}
function fn(a, b, c) {
console.log(a, b, c)
console.log(this)
}
fn.call(obj, 10, 20)
apply方法
和call方法一样,但是传递的第二个参数为数组
let obj = {
a:1,
b:2,
c:3
}
function fn(a,b,c){
this.a = a
console.log(this.b)
console.log(this.c)
}
fn.call(obj, 4, 5, 6) // -> this:obj -> 4,2,3
fn.apply(obj, [4,5,6]) // -> 4,2,3
bind方法
let obj = {name:'obj'}
function fn(){
console.log(this.name)
}
// 想让点击body的时候打印body
document.body.onclick = fn.call(obj) // -> 错误,call会立即执行,将fn改变this执行之后的结果返回
document.body.onclick = fn.bind(obj) // -> 和call/apply一样,bind也是用来改变函数执行中的this关键字的,只不过基于bind改变this,当前方法并没有执行,类似于预先改变this
// bind的好处是:通过bind方法只是预先把fn中的this修改为obj,此时fn并没有执行,当点击事件之后执行fn(call/apply都是改变this的同时立即把方法执行) => 在IE6~8不支持bind方法 预先做什么事的思想被称为"柯里化函数"的思想
bind与call和apply的区别:
call和apply改变函数this的时候立即将函数执行,而bind是预先将函数的this改变。apply传递的参数为数组,而call是一个一个参数进行传递的
ES6中
let 、var的区别
- let不存在变量提升 ( 当前作用域中,不能在乐天、声明前使用变量 )
- 同一个作用域中,let不允许重复声明
- let解决了typeof的暂时性死区的问题
- 全局作用域中,使用let声明的变量并没有在window加上对应的属性
- let会存在块级作用域 ( 除对象以外的大括号都可以看做块级私有作用域 )
箭头函数THIS的问题
ES6中新增了创建函数的方式:"箭头函数"
真实项目中是箭头函数和FUNCTION这种普通函数混合使用
-
箭头函数简化的函数创建的代码
// 箭头函数的创建方式都是函数表达式的方式,这种方式不存在变量提升,也就是函数只能在函数创建之后在执行 const fn = ([形参]) => { // 函数体 } fn([实参]) // 1. 形参只有一个小括号可以不加: const f1 = i => { console.log(i) } // 2. 函数体中只有一句话,并且是return xxx的时候,可以省略大括号: const f2 = i => i + 1 // 将这个函数改成箭头函数 function f3(n){ return function(m){ return n+m } } const f4 = n => m => n + m // 3. 箭头函数中没有arguments,但是可以基于剩余运算符获取实参集合,而且是ES6中支持给形参设置默认值 const f5 = ...args => { console.log(args) } // 4. 箭头函数没有THIS,它里面的THIS,都是自己所处上下文中的THIS window.name = 'win' let obj = {name:'obj'} const f6 = n => { console.log(this.name) } f6(10) // -> this是window f6.call(obj, 10) // ->this还是window document.body.onclick = f6 // -> 点击之后输出还是win,this还是window,不是body obj.fn = fn obj.fn() // -> this:window // 用call无法改变箭头函数中的THIS // ------------ let obj = { name:"obj", fn:function(){ let f = () => { console.log(this) } f() } } obj.fn() // -> this:obj let f = obj.fn f() // -> this:window
总结:1. 形参只有一个小括号可以不加:
2. 函数体中只有一句话,并且是return xxx的时候,可以省略大括号:
3. 箭头函数中没有arguments,但是可以基于剩余运算符获取实参集合,而且是ES6中支持给形参设置默认值
4. 箭头函数没有THIS,它里面的THIS,都是自己所处上下文中的THIS
5. 用call无法改变箭头函数中的THIS
解构赋值
let arr = [1,2,3,4]
let [n,m] = arr
console.log(n,m) // -> 1,2
let [a,,b] = arr // -> 1,3
//------------------
let arr = [1,[2,3,[4,5]]]
let [a,[,,[,b]]] = arr
console.log(a,b) // -> 1,5
//------------------
let obj = {
name:'cyj',
lianling:19,
friends:['a1','a2','a2']
}
let {
name,
lianling:age,
friends:[firstFriends]
} = obj
console.log(name,age,fistFriends) // -> cyj 19 a1
ES6中创建class
class Fn{
constructor(n,m){
// 等价于之前的构造函数体
this.x = n
this.y = m
x = 1
y = 2
}
// 直接写的方法就是加在原型上的方法 例如:Fn.prototype.xxx = function(){...}
getX(){
console.log(this.x)
}
z = 300 // 还是在给实例设置私有属性
static a = 5 // Fn的私有属性 Fn.a
// 前面设置static的:把当前Fn当作普通对象设置的键值对 例如: Fn.queryX = function(){...}
static queryX(){
}
}
// 也可以在外面这样写
// 类似于getX
Fn.prototype.getY = function(){
console.log(this.y)
}
// 类似于queryY
Fn.queryY = function(){
console.log(y)
}
let f = new Fn(10,20)
JS的DOM操作
DOM:document object model 文档对象模型,提供系列的属性和方法,让我们能在JS中操作页面中的元素
获取元素的属性和方法
document.getElementById([ID])
[context].getElementsByTagName([TAG-NAME])
[context].getElementsByClassName([CLASS-NAME])
//=>在IE6~8中不兼容
document.getElementsByName([NAME])
//=>在IE浏览器中只对表单元素的NAME有作用
[context].querySelector([SELECTOR])
[context].querySelectorAll([SELECTOR])
//=>在IE6~8中不兼容
//---------------------
document
document.documentElement
document.head
document.body
childNodes 所有子节点
children 所有元素子节点
//=>IE6~8中会把注释节点当做元素节点获取到
parentNode
firstChild / firstElementChild
lastChild / lastElementChild
previousSibling / previousElementSibling
nextSibling / nextElementSibling
//=>所有带Element的,在IE6~8中不兼容
DOM的增删改操作
document.createElement([TAG-NAME])
document.createTextNode([TEXT CONTENT])
字符串拼接(模板字符串),基于innerHTML/innerText存放到容器中
[PARENT].appendChild([NEW-ELEMENT])
[PARENT].insertBefore([NEW-ELEMENT],[ELEMENT])
[ELEMENT].cloneNode([TRUE/FALSE])
[PARENT].removeChild([ELEMENT])
//=>设置自定义属性
[ELEMENT].xxx=xxx;
console.log([ELEMENT].xxx);
delete [ELEMENT].xxx;
[ELEMENT].setAttribute('xxx',xxx);
console.log([ELEMENT].getAttribute('xxx'));
[ELEMENT].removeAttribute('xxx');
获取元素样式和操作样式
//=>修改元素样式
[ELEMENT].style.xxx=xxx; //=>修改和设置它的行内样式
[ELEMENT].className=xxx; //=>设置样式类
//=>获取元素的样式
console.log([ELEMENT].style.xxx); //=>获取的是当前元素写在行内上的样式,如果有这个样式,但是没有写在行内上,则获取不到
JS盒子模型属性
基于一些属性和方法,让我们能够获取到当前元素的样式信息,例如:clientWidth 、offsetWidth等
- client
- width / height 获取盒子padding+内容的区域大小,不包括border
- top / left 获取盒子左边框和上边框的大小
- offset
- width / height 就是在clientWidth/clientHeight的基础上加上了border边框,就是盒子本身的大小
- top / left
- parent
- scroll
- width / height 在没有内容溢出的情况下和clientWidth/clientHeight一样
- top / left
方法:window.getComputedStyle([ELEMENT],[伪类]) / [ELEMENT].currentStyle
getComputedStyle
获取当前元素所有经过浏览器计算过的样式
- 只要元素在页面中呈现出来,那么所有的样式都是经过浏览器计算的
- 哪怕你没有设置和见过的样式也都计算了
- 不管你写或者不写,也不轮写在哪,样式都在这,可以直接获取
在IE6~8浏览器中不兼容,需要基于currentStyle来获取
//=>第一个参数是操作的元素 第二个参数是元素的伪类:after/:before
//=>获取的结果是CSSStyleDeclaration这个类的实例(对象),包含了当前元素所有的样式信息
let styleObj = window.getComputedStyle([element],null);
styleObj["backgroundColor"]
styleObj.display
//=>IE6~8
styleObj = [element].currentStyle;
图片加载
图片延时加载
- 结构中,用一个盒子包裹图片(在图片不展示的时候,可以占据这个位置,并设置默认的加载图)
- 最开始,img的src中不设置任何图片地址,把图片的真实地址设置给自定义属性,data-src/true-img
- 当浏览器窗口完全展示到图片位置的时候,再去加载真实的图片,并且让图片显示出来(第一屏的图片一般都会延迟加载,等待其他资源加载完)
经典面试题:
window.onload 和 document.ready($(document.read()) 的区别?
- window.onload 是等待所有资源都加载完成才会触发执行,而我之前研究过部分jQuery源码,发现$(document.read())是用的DOMContentLoaded事件,事件本身是DOM结构加载完成之后才会触发执行,所以$(document.ready)要优先于window.onload触发
- window.onload是基于DOM0事件绑定,只能帮定一个方法,所以页面中只能使用一次,而jquery中是使用DOM2完成的,所以可以在相同页面中帮定多个不同的方法,也就是能使用很多次$(document.ready),我之前在jQuery的开发中,经常把编写的模块放在$(document.ready)中,既能形成必报,也能保证DOM结构加载完成
- 不论是哪一种方法,都是为了保证DOM结构加载完成在执行的,这样在方法中坑定能获取到DOM元素,防止把JS放到DOM之前加载,导致元素无法获取的问题。
小知识
捕获与冒泡
box.onclick = function(ev){
console.log(ev)
// type:事件类型 click
// target: 事件源 触发的元素
// clientX/clientY: 当前鼠标距离当前窗口左上角的X/Y的坐标
// pageX/pageY:当前鼠标距离当前页面BODY的左上角的X/Y的坐标 不兼容IE低版本浏览器
}
document.onkeydown = function(ev){
console.log(ev) // 键盘的事件对象
// keyCode:按键对应的键盘码,例如按下空格,keyCode为32,enter为13
// 基本的键盘码
// 空格:32
// ENTER:13
// BACKSAPCE 8 ,回退键
// DEL 46 删除键
// shiftkey:true 按下了shift组合键
}
box.ontouchstart = function(ev) {
console.log(ev.touches[0])
console.log(ev.changedTouches[0])
}
/** 事件的传播机制
* 1 CAPTURING_PAHASE: 捕获阶段
* 2 AT_TARGET: 目标阶段
* 3 BUBBLING_PHASE: 冒泡阶段
*
*/
Ajax状态码
UNSEND: 0 未发送 开始创建XHR,默认状态就是0
OPENED: 1 已打开 执行了open
*HEADERS_RECEIVED: 2 服务器已经返回了响应头的信息
LOADING: 3 响应主体正在加载中
*DONE: 4 响应主体的信息也返回了
-
以2开始的
- 200 服务正常返回数据 ( 客服端向服务器发送请求,服务器正常吧数据放回 )
-
以3开始的
-
304 读取的是协商缓存数据
-
301 永久重定向( 域名转移 )
-
302/307 临时转移 临时重定向 ( 这个一般用于 服务器 负载均衡 )
-
-
以4开头的 基本都是错误 【 一般是客户端的错误 】
-
400 请求参数有误
-
401 无权访问
-
403 服务器拒绝执行
-
404 地址错误
-
-
以5开头的 【 一般是服务器的错误 】
- 500 服务器发生未知的错误
- 503 服务器超负荷
浏览器底层渲染机制
进程:进程是一个应用程序
线程:线程是应用程序中具体做事情的
所以关系就是一个进程包含多个线程
一个线程只能同时干一件事情,多个线程就能同时干多件事情
浏览器本身是多线程的,渲染页面的 GUI线程,请求资源的HTTP网络线程
所以浏览器自上而下渲染页面的时候,就是开辟了GUI渲染线程自上而下渲染页面,遇到了link
构建DOM树,CSSOM树,Render-Tree渲染树
link和@import都是导入外部样式 ( 从服务器获取样式文件 ),他们的区别是什么
- 遇到link,浏览器会新派发一个线程 ( HTTP网络请求线程 ),去加载资源文件,与此同时,GUI渲染线程会继续向下渲染代码...所以导致了一个问题,不论css是否请求回来,代码继续渲染
- 但是遇到@improt,是GUI渲染线程会暂时结束( 暂时停止渲染 ),去服务器加载资源文件,资源文件,没有返回之前,是不会去渲染的,所以所@import阻碍了浏览器的渲染,项目中尽量少用,但是vue、react项目中使用webpack打包后都是link了,不存在其他问题
- 如果是style,GUI直接渲染
正常情况下JS也会阻碍GUI的渲染,所以:
- JS一般放在页面的尾部,就是为了确保DOM树生成后才会加载JS
- 可能会基于defer和async异步去管控JS的请求。defer等待所有JS加载完,更具顺序分别渲染JS
浏览器渲染机制
页面渲染第一步,在CSS资源没有请求回来之前,先生成DOM树
页面渲染第二部:当所有的CSS请求回来的之后,浏览器按照CSS的导入顺序,依次进行渲染,最后生成CSSOM树
页面渲染第三部:把DOM树和CSSOM树结合在一起,生成有样式有结构的Render-Tree树
最后一步:浏览器按照渲染树,在页面中进行渲染和解析,分为以下连个步骤
(1) 计算元素在设备视口中的大小和位置,布局(Layout)或重排/回流( reflow )
(2) 格局渲染树以及回流得到的几何信息,得到节点的绝对像素-> 绘制/重绘( painting )
所以根据上面的浏览器渲染的机制,能做出以下优化
性能优化:
- 减少DOM树渲染的时间(HTML层级不要太深,标签语义化...)
- 减少CSSOM渲染时间(选择器树从右向左解析,所以尽可能的减少选择器的层级)
- 减少HTTP的请求次数和请求大小
- 一般把css放在页面的开始位置( 提前做资源请求,用link而不用@import,对于移动端来讲,如果css比较少,尽可能使用内嵌式即可 )
- 为了避免白屏,可以进来的第一件事,快速生成一套loading的渲染树( 前端骨架屏 ),服务器的SSR骨架屏所提高的渲染是避免了客户端再次单独请求数据,而不是样式和结构上的。
服务器渲染:服务器直接把数据放在页面中在将页面放回给客户端,浏览器直接渲染出来。
两个缺点:
- 服务器压力大
- 不能实现局部刷新,数据改变后只能刷新页面( 整个页面刷新 )
优点:
- 有利于SEO优化( 搜索引擎优化 )。百度/谷歌等搜索浏览器,会不定期到网络上爬去数据( 搜索引擎收录数据 ),收录的东西越多,网站的权重越大( 不同标签的权重是不一样的 [ 标签语义化 ] )。用户在搜索框中输入关键词,搜索引擎去自己的收录信息库中匹配结果,最后按照网站的权重和收录的关键词的匹配结果,有排名的先出来
当代浏览器的预测解析,chrome的瑜伽在扫描器html-preload-scanner通过扫描节点中的src、link等属性,找到外部链接资源后进行预加载,避免了资源加载的等待时间,同样实现了提前加载以及加载和执行分离 ( 在GUI渲染之前就去发送请求,当然也有并发限制6-7个 )
DOM的重绘和回流 Repaint & Reflow
-
重绘:元素样式的改变( 但是宽高、大小、位置不变 )
如:visibility、color、background-color
-
回流:元素的大小、位置发生了改变 ( 引起了页面布局和几何信息发生变化时 ),触发了重排重新布局,导致渲染树进行重新计算布局和渲染
如:添加或删除可见的DOM元素;元素位置改变;元素尺寸宽高改变;页面一开始渲染的时候( 这个无法避免 );浏览器窗口尺寸发生变化引发回流,因为回流是根据视口的大小来计算元素的位置和大小的
注:当页面第一次渲染完,我们后期基于某些操作改变修改页面中元素的样式:可能会引发元素大小、位置的改变,这样浏览器就需要重新进行layout计算,重排完成后,在进行重绘。所以重排一定会触发重绘,重绘不一定会触发重排。
一般我们都说操作DOM消耗性能,因为操作DOM可能会改变元素大小、位置,触发layout重新计算元素的位置
box.onclick = function(){
// 浏览器的渲染队列
// 浏览器的渲染队列机制:遇到修改样式的代码,浏览器没有立即渲染,而是把它放入到渲染队列中,继续向下看是否还是修改样式的,是的话继续放进去...(直到遇到获取元素样式的代码或者没有修改样式的代码了,则立即把队列中样式进行统一渲染,最后只引发一次回流重排)
box.style.width = '100px';
box.style.height = '200px';
};
// 如果想让他引发两次回流
box.onclick = function(){
box.style.width = '100px'
console.log(box.offsetWidth);
box.style.height = '100px';
// 这样的话就引发了两次回流
}
// 所以我们有一个专业的名词叫:分离"读写"
// 或者:样式集中改变
DOM性能优化:
-
减少重排和重绘
-
分离"读写"
-
批量修改样式
-
使用fragment
-
例如我要床架十个span加入到body中
-
for(let i=0; i<10; i++){ let span = document.createElement('span') span.innerHTML = `我是span${i}` document.body.appendChild(span) } // 这样就照成了十次重排,而使用createDocumentFragment创建文档碎片 let fragment = document.createDocumentFragment() for (let i = 0; i < 10; i++) { let span = document.createElement('span') span.innerHTML = i fragment.appendChild(span) // 先将创建的span放入内存中的文档碎片中 } console.log(fragment) document.body.appendChild(fragment)
-
-
浏览器的渲染队列
box.onclick = function () {
box.style.transitionDuration = '0s'
box.style.left = 0
box.style.left; // 中间截断
box.style.transitionDuration = '1s'
box.style.left = '400px'
}