• 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'
    }
    
  • 相关阅读:
    在游戏中充分利用可编程的GPU
    坐标变换
    深入理解GPU Architecture(上)
    RV870和GT300的一些猜测
    深入理解Intel Core Microarchitecture
    CGDC见闻
    hdu 1517 K(2~9)倍博弈
    hdu 2177 威佐夫博弈+输出使你胜的你第1次取石子后剩下的两堆石子的数量
    坚持住
    真正体会到一个ac的快感
  • 原文地址:https://www.cnblogs.com/ycyc123/p/14823924.html
Copyright © 2020-2023  润新知