js-基础总结2

对象

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 执行环境栈 也是栈内存

栈内存:提供代码的执行环境

堆内存:存放属性和方法

== 在进行比较的时候,如果左右两边数据类型不相等,先转化为相同的类型,在进行比较

对象 == 字符串 对象转化为字符串

  1. null == undefined true (三个等号下不相等),但是和其他任何不相等
  2. 0 == null false
  3. 剩下的情况都是转化为数字在进行比较
[] == 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 不允许重复声明

总结

  1. 在相同的作用域中(或执行上下文中)如果用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,这种就是作用域链查找的机制
// 函数执行形成的私有栈内存,会把内存中所有的私有变量保护起来,和外面没有任何关系 => 函数执行的这种 保护机制 就是“闭包”
截屏2020-04-28 下午1.45.45

在来一道题

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 的区别

  1. var能重复声明,而let不能重复声明

  2. var有变量提升,而let没有

  3. let能解决typeof检测时出现的暂时性死区的问题(更加严谨)

GO

GO 全局对象window 堆内存 浏览器内置的API
!==

VO(G) 全局变量对象 上下文中的空间 全局上下文中创建的变量

基于VAR/FUNCTION在全局上下文中声明的全局变量也会给GO赋值一份(映射机制)

但是就LET/CONST等ES6方式在全局上下文中创建的全局变量和GO没有关系

浏览器的垃圾回收机制

浏览器的垃圾回收机制(自己内部处理):
[谷歌等浏览器是“基于引用查找“来进行垃圾回收的]

  1. 开辟的堆内存,浏览器自己默认会在空闲的时候,查找所有内存的引用,把那些不被引用的内存释放掉
  2. 开辟的栈内存(上下文)一般在代码执行完都会出栈释放,如果遇到上下文中的东西被外部占用,则不会释放
    [IE等浏览器是“基于计数器”机制来进行内存管理的]
  3. 创建的内存被引用一次,则计数1,在被引用一次,计数2... 移除引用减去1... 当减为零的时候,浏览器会把内存释放掉
    =>真实项目中,某些情况导致计数规则会出现一些问题,造成很多内存不能被释放掉,产生“内存泄漏”;查找引用的方式如果形成相互引用,也会导致“内存泄漏“

闭包

函数执行会形成全新的私有上下文,这个上下文可能被释放,也可能不被释放,不论是否被释放,它的作用是:

  1. 保护:划分一个独立的代码执行区域,在这个区域中有自己私有变量存储的空间,而用到的私有变量和其它区域中的变量不会有任何的冲突(防止全局变量污染)

  2. 保存:(防止全局变量污染)如果上下文不被销毁,那么存储的私有变量的值也不会被销毁,可以被其下级上下文中调取使用

    我们把函数执行,形成私有上下文,来保存和保护私有变量的机制,称之为“闭包” =>它是一种机制

原型及原型链模式

  1. 每一个函数都有一个叫prototype的属性,这个属性是一个对象,保存了类的公共属性和方法
    • 普通的函数
    • 类(自定义类和内置累)
  2. 在prototype这个对象中,也又一个天生自带的属性教constructor,这个属性存储的是当前函数本身
  3. 每一个类的实例,都有一个_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这种普通函数混合使用

  1. 箭头函数简化的函数创建的代码

    // 箭头函数的创建方式都是函数表达式的方式,这种方式不存在变量提升,也就是函数只能在函数创建之后在执行
    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;

图片加载

图片延时加载

  1. 结构中,用一个盒子包裹图片(在图片不展示的时候,可以占据这个位置,并设置默认的加载图)
  2. 最开始,img的src中不设置任何图片地址,把图片的真实地址设置给自定义属性,data-src/true-img
  3. 当浏览器窗口完全展示到图片位置的时候,再去加载真实的图片,并且让图片显示出来(第一屏的图片一般都会延迟加载,等待其他资源加载完)

经典面试题:

window.onload 和 document.ready($(document.read()) 的区别?
  1. window.onload 是等待所有资源都加载完成才会触发执行,而我之前研究过部分jQuery源码,发现$(document.read())是用的DOMContentLoaded事件,事件本身是DOM结构加载完成之后才会触发执行,所以$(document.ready)要优先于window.onload触发
  2. window.onload是基于DOM0事件绑定,只能帮定一个方法,所以页面中只能使用一次,而jquery中是使用DOM2完成的,所以可以在相同页面中帮定多个不同的方法,也就是能使用很多次$(document.ready),我之前在jQuery的开发中,经常把编写的模块放在$(document.ready)中,既能形成必报,也能保证DOM结构加载完成
  3. 不论是哪一种方法,都是为了保证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都是导入外部样式 ( 从服务器获取样式文件 ),他们的区别是什么

  1. 遇到link,浏览器会新派发一个线程 ( HTTP网络请求线程 ),去加载资源文件,与此同时,GUI渲染线程会继续向下渲染代码...所以导致了一个问题,不论css是否请求回来,代码继续渲染
  2. 但是遇到@improt,是GUI渲染线程会暂时结束( 暂时停止渲染 ),去服务器加载资源文件,资源文件,没有返回之前,是不会去渲染的,所以所@import阻碍了浏览器的渲染,项目中尽量少用,但是vue、react项目中使用webpack打包后都是link了,不存在其他问题
  3. 如果是style,GUI直接渲染

正常情况下JS也会阻碍GUI的渲染,所以:

  1. JS一般放在页面的尾部,就是为了确保DOM树生成后才会加载JS
  2. 可能会基于defer和async异步去管控JS的请求。defer等待所有JS加载完,更具顺序分别渲染JS

浏览器渲染机制

页面渲染第一步,在CSS资源没有请求回来之前,先生成DOM树

页面渲染第二部:当所有的CSS请求回来的之后,浏览器按照CSS的导入顺序,依次进行渲染,最后生成CSSOM树

页面渲染第三部:把DOM树和CSSOM树结合在一起,生成有样式有结构的Render-Tree树

最后一步:浏览器按照渲染树,在页面中进行渲染和解析,分为以下连个步骤

(1) 计算元素在设备视口中的大小和位置,布局(Layout)或重排/回流( reflow )

(2) 格局渲染树以及回流得到的几何信息,得到节点的绝对像素-> 绘制/重绘( painting )

所以根据上面的浏览器渲染的机制,能做出以下优化

性能优化:

  1. 减少DOM树渲染的时间(HTML层级不要太深,标签语义化...)
  2. 减少CSSOM渲染时间(选择器树从右向左解析,所以尽可能的减少选择器的层级)
  3. 减少HTTP的请求次数和请求大小
  4. 一般把css放在页面的开始位置( 提前做资源请求,用link而不用@import,对于移动端来讲,如果css比较少,尽可能使用内嵌式即可 )
  5. 为了避免白屏,可以进来的第一件事,快速生成一套loading的渲染树( 前端骨架屏 ),服务器的SSR骨架屏所提高的渲染是避免了客户端再次单独请求数据,而不是样式和结构上的。

服务器渲染:服务器直接把数据放在页面中在将页面放回给客户端,浏览器直接渲染出来。

两个缺点:

  1. 服务器压力大
  2. 不能实现局部刷新,数据改变后只能刷新页面( 整个页面刷新 )

优点:

  1. 有利于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'
}
posted @ 2021-05-28 21:47  有风吹过的地方丨  阅读(82)  评论(0编辑  收藏  举报