JavaScript高级

函数的this指向

函数this的绑定规则

默认绑定

  • 函数默认调用时,函数的this都指向window
    // "use strict"

        // 1.普通的函数被独立的调用
        function foo() {
            console.log('foo', this);
        }
        foo()//window

        // 2.函数定义在对象中,但是独立调用
        var obj = {
            name: "why",
            bar: function() {
                console.log("bar", this)
            }
        }
        var baz = obj.bar
        baz()//window

        // 3.高阶函数
        function test(fn) {
            fn()
        }

        test(obj.bar)//window

        // 4.严格模式下,独立调用的函数中的this指向的是undefind
        // "use strict"

隐式绑定

  • 通过某个对象来调用函数,函数中的this指向调用它的对象
 function foo() {
            console.log("foo函数", this);
        }
        var obj = {
            name: "why",
            bar: foo
        }

        obj.bar()//obj

new绑定

  • JS中的函数可以当作一个类的构造函数来使用,也就是使用new关键字
  • 使用new创建一个对象时,其中的this会指向这个对象
    • 创建一个新对象
    • 这个新对象会被执行prototype连接
    • this会指向这个新对象
    • 函数会返回这个新对象
 function Foo() {
            console.log("foo函数", this);
            this.name = "why"
        }

        new Foo()//Foo {}

显示绑定

  • 使用call、apply显示绑定this
 function foo(name, age, height) {
            console.log("foo函数", this);
            console.log(name, age, height)
        }

        // ()调用
        foo("why", 18, 1.88)

        // apply
        // 第一个参数绑定this
        // 第二个参数:传入额外的实参,以数组的形式
        foo.apply("apply", ["why", 30, 1.98])

        // call
        // 第一个参数:绑定this
        // 参数列表:后续的参数以参数列表的形式传入实参
        foo.call("call", "james", 25, 1.80)

  • 当我们希望一个函数总是显示的绑定到一个对象上时
    • 使用bind方法,bind()方法会创建一个新的绑定函数
       function foo(name, age, height, address) {
            console.log("foo函数", this);
            console.log(name, age, height, address)
        }

        var obj = {
            name: "why"
        }

        // 需求:调用foo时,总是绑定到obj对象上(不作为obj对象方法的方式)

        // 1.bind函数的基本使用
        var bar = foo.bind(obj)
        bar()//obj
        bar()//obj
        bar()//obj

        // 2.bind函数的其他参数
        var bar2 = foo.bind(obj, "why", 18, 1.99)
        bar2("james")//why, 18,1.99, james

this绑定的规则优先级

  1. 默认规则的优先级最低
  2. 显示绑定高于隐式绑定
  3. new绑定高于隐式绑定
  4. new绑定高于bind的优先级
  5. new绑定不能与call、apply同时使用
  6. bind优先级高于call、apply

优先级:

  1. new
  2. bind
  3. apply、call
  4. 隐式绑定
  5. 默认绑定

this绑定之外的情况

  • 情况一:如果在显示绑定中,我们传入一个null或者undefind,那么这个显示绑定会被忽略,使用默认规则:window

  • 情况二:创建一个函数的间接引用,这种情况使用默认绑定规则

    • 赋值(obj2.foo = obj1.foo)的结果是foo
    • 直接调用foo函数,就是默认绑定规则
    (obj2.foo = obj1.foo)()
    
  • 情况三:箭头函数

箭头函数

  • 箭头函数式ES6新增的一种函数编写方法,比函数表达式更加简洁
    • 不会绑定this、arguments
    • 不能作为构造函数来使用,不能和new一起使用,会抛出错误(因为箭头函数没有绑定this)
    // 1.之前的方式
        function foo1() {}
        var foo2 = function(){}

        // 2.箭头函数
        var foo3 = (name, age) => {
            console.log(name, age);
        }

箭头函数的编写优化
  • 优化一:如果只有一个参数,()可以省略
nums.forEach(item => {})
  • 优化二:如果函数执行体中只有一行代码,那么可以省略大括号
    • 并且这行代码的返回值会作为整个函数的返回值
    • return也要省略,因为会自动返回值
nums.forEach(item => console.log(item))
nums.filter(item => true)
  • 优化三:如果函数执行体只有返回一个对象,那么需要给这个对象加上()
    • 在redux中经常使用
var foo = () => {
    return { name: "abc" }
}
//返回对象时,如果你想省略大括号,需要在对象外加小括号
var bar = () => ({name: "abc"})

箭头函数中的this使用
  • 箭头函数中没有this
    • 所以当我们在函数中使用this时,它会在上层作用域中查找this
      // 1.箭头函数中没有this
        // 所以当我们在函数中使用this时,它会在上层作用域中查找this

        var bar = () => {
            console.log('bar:', this);
        }
        bar()//window
        bar.apply('aaa')//window

        // 2.this的查找规则
        var obj = {
            name: "obj",
            foo: function() {
                var bar = () => {
                    console.log("bar", this)
                }
                return bar
            }
        }

        var fn = obj.foo()
        fn.apply("bbb") //obj


        var obj2 = {
            name: "obj",
            foo: () => {
                var bar = () => {
                    console.log("bar", this)
                }
                return bar
            }
        }

        var fn2 = obj2.foo()
        fn2.apply("bbb") //window

箭头函数的this应用
  // 网络请求场景
        function request(url,fn) {
            var results = ["a", "b", "c"]
            fn(results)
        }

        var obj = {
            names: [],
            network: function() {
                request("/lists", res => this.names = [].concat(res))
            }
        }

        obj.network()
        console.log(obj.names)

this面试题

      var name = "window"
        var person = {
            name: "person",
            sayName: function () {
                console.log(this.name);
            }
        }
        function sayName() {
            var sss = person.sayName;
            sss();//window
            person.sayName();//person
            (person.sayName)();//person :这个跟上面那个一样是隐式绑定
            (b = person.sayName)()//winow:间接函数引用,返回一个独立的函数,然后独立的函数调用
        }

        sayName()
 // 面试题2
        var name = "window"

        var person1 = {
            name: "person1",
            foo1: function () {
                console.log(this.name);
            },
            foo2: () => console.log(this.name),
            foo3: function () {
                return function () {
                    console.log(this.name)
                }
            },
            foo4: function() {
                return () => {
                    console.log(this.name)
                }
            }
        }

        var person2 = { name: "person2"}

        person1.foo1();//person1
        person1.foo1.call(person2);//person2

        person1.foo2();//window
        person1.foo2.call(person2); //window

        person1.foo3()();//window
        person1.foo3.call(person2)();//window
        person1.foo3().call(person2);//person2

        person1.foo4()(); //person1
        person1.foo4.call(person2)();//person2
        person1.foo4().call(person2);//person1
    // 面试题3
        var name = "window"

        function Person(name) {
            this.name = name
            this.foo1 = function () {
                console.log(this.name);
            },
            this.foo2 = () => console.log(this.name),
            this.foo3 = function () {
                return function () {
                    console.log(this.name)
                }
            },
            this.foo4 = function () {
                return () => {
                    console.log(this.name)
                }
            }
        }

        var person1 = new Person("person1")
        var person2 = new Person('person2')

        person1.foo1();//person1
        person1.foo1.call(person2);//person2

        person1.foo2();//window是错的,构造函数有自己的作用域,箭头函数会在这个作用域中找到person1
        person1.foo2.call(person2); //person1:理由同上

        person1.foo3()();//window
        person1.foo3.call(person2)();//window
        person1.foo3().call(person2);//person2

        person1.foo4()(); //person1
        person1.foo4.call(person2)();//person2
        person1.foo4().call(person2);//person1
        // 面试题4
        var name = "window"
        function Person (name) {
            this.name = name
            this.obj = {
                name: "obj",
                foo1: function () {
                    return function () {
                        console.log(this.name);
                    }
                },
                foo2: function () {
                    return () => {
                        console.log(this.name)
                    }
                }
            }
        }

        var person1 = new Person("person1")
        var person2 = new Person('person2')

        person1.obj.foo1()();//window
        person1.obj.foo1.call(person2)()//window
        person1.obj.foo1().call(person2)//person2

        person1.obj.foo2()();//obj
        person1.obj.foo2.call(person2)()//person2
        person1.obj.foo2().call(person2)//obj

浏览器渲染原理

输入URL后资源是如何加载的

JavaScript高级_第1张图片

渲染引擎如何解析页面

JavaScript高级_第2张图片

更详细的过程:

JavaScript高级_第3张图片

  • DOM树上有些元素可能是隐藏的,那么在渲染树中是没有这些元素的
  • 所以DOM树和渲染树不是一一对应的
  • 单独的layout计算位置大小信息,防止有些DOM树中隐藏的元素

解析一:HTML解析过程

  • 因为默认情况下服务器会给浏览器返回index.html文件,所以解析HTML是所有步骤的开始
  • 解析HTML会构建DOM Tree

解析二:生成CSS规则

  • 在解析HTML过程中,如果遇到CSS的link元素,那么会由浏览器负责下载对应的CSS文件
    • 注意:下载CSS文件不会影响DOM解析
  • 浏览器下载完CSS文件后,就会对CSS文件进行解析,解析出对应的规则树:
    • 我们可以称之为CSSDOM(CSS对象模型)

解析三:构建Render Tree

  • 当有了DOM Tree和CSSDOM Tree后,就可以结合来构建Render Tree了
  • CSS规则树中display为none的值不会出现在DOM树中
  • 注意:link元素不会阻塞DOM树的构建过程,但是会阻塞Render树的构建过程
    • 因为Render树在构建时,需要对应的CSSOM Tree

解析四:布局和绘制

  • 第四步是在渲染树上运行布局以计算每个节点的几何体
    • 渲染树会表示显示哪些节点以及其他样式,但是不表示每个节点的尺寸、位置等信息
    • 布局就是确定渲染树中所有节点的宽度、高度和位置信息
  • 第五步是将每个节点绘制到屏幕上
    • 在绘制阶段,浏览器将布局阶段计算的每个frame转为屏幕上的实际的像素点
    • 包括将元素的可见部分进行绘制,比如文本、颜色、边框、阴影、替换元素

回流和重绘

  • 回流reflow:也可以称之为重排
    • 第一次确定节点的大小和位置,称之为布局
    • 之后对节点的大小、位置修改重新计算,称之为回流
    • 不一定会重建DOM树,但是一定会重新布局
  • 什么情况下引起回流?
    • 比如DOM结构发生变化(添加新的节点或者移除节点 )
    • 比如改变了布局(修改了width、height、padding、font-size等值)
    • 比如窗口resize(修改了窗口的尺寸等)
    • 比如调用getComputedStyle方法获取尺寸、位置等信息

  • 重绘repaint:
    • 第一次渲染内容称之为绘制
    • 之后重新渲染称之为重绘
  • 什么情况下会引起重绘呢?
    • 比如修改背景色、文字颜色、边框颜色、样式等

  • 回流一定会引起重绘,所以回流是一件很消耗性能的事情
  • 所以在开发中要尽量避免发生回流:
  • 集中改变样式
    • 比如通过cssText修改,比如通过添加class修改
  • 尽量避免频繁的操作DOM
    * 我们可以在一个DocumentFragment或者父元素中将要操作的DOM操作完成,再一次性的操作
  • 尽量避免通过getComputedStyle获取尺寸、位置等信息
  • 某些元素使用position的absolute或者fixed
    • 并不是不会引起回流,不会对其他元素造成影响,开销相对较小
  • 读写分离
  • 离线改变DOM

特殊解析 - composite合成

  • 绘制的过程,可以将布局后的元素绘制到多个合成图层中
    • 这是浏览器的一种优化手段
  • 默认情况下,标准流中的内容都是被绘制在同一个图层中的
  • 而一些特殊的属性,会创建一个新的合成层,并且新的图层可以利用GPU来加速绘制
    • 因为每个合成层都是单独渲染的
  • 哪些属性会形成新的合成层呢?
    • 3D transforms
    • video、vanvas、iframe
    • opacity动画转换时
    • positon:fixed
    • will-change
    • animation或transition设置了opacity、transform
  • 分层是以内存为代价提升性能的,所以不要过度使用

script元素和页面解析的关系

  • 页面渲染过程中,JS在哪里?
    • 浏览器在解析HTML的过程中,遇到了script元素是不能继续构建DOM树
    • 它会停止继续构建,首先下载JS代码,并且执行JS的脚本
    • 只有等到JS脚本执行结束之后,才会继续解析HTML,构建DOM树
  • 为什么要这样做呢?
    • 因为JS的作用之一就是操作DOM,并且可以修改DOM
    • 如果我们等到DOM树构建完成并且渲染再执行JS,会造成严重的回流和重绘,影响页面的性能
    • 所以在遇到script元素时,优先下载和执行JS代码,再继续构建DOM树
  • 带来的问题:
    • 目前的开发模式中,脚本往往比HTML页面更‘重“,处理时间需要更长
    • 所以造成页面的解析阻塞,在脚本下载、执行完成之前,用户在界面上什么都看不到
  • 为了解决这个问题,script元素给我们提供了两个属性:defer和async

defer和async的使用

defer属性

  • defer属性告诉浏览器不要等待脚本下载,而继续解析HTML,构建DOM Tree
    • 脚本会由浏览器来进行下载,但是不会阻塞DOM Tree的构建过程
    • 如果脚本提前下载好了,它会等待DOM Tree构建完成,在DOMContentLoaded事件之前先执行defer中的代码
      • DOM 树已经构建完毕,但是像是 和样式表等外部资源可能并没有下载完毕。
  • 所以DOMContentLoaded总是等待defer中的代码优先执行完成
  • 另外多个带defer的脚本可以保持正确的顺序执行
  • 从某种角度来说,defer可以提高页面的性能,并且建议放到head中
  • defer只适用于外部脚本

async属性

  • 与defer有些类似,能让脚本不阻塞页面
  • async是让一个脚本完全独立的
    • 浏览器不会因为async脚本而阻塞
    • async脚本不能保证顺序,它是独立下载、独立运行、不会等待其他脚本
    • async不会保证能在DOMContentLoaded之前或者之后执行

  • defer通常用于需要在文档解析之后操作DOM的JS代码,并且对多个script文件有顺序要求

  • async通常用于独立的脚本,对其他脚本、甚至DOM没有依赖的

JavaScript的运行原理

JavaScript代码的执行

  • JavaScript代码下载好后,是如何一步步被执行的?
  • 浏览器内核由两部分组成,以webkit为例:
    • WebCore:负责HTML解析、布局、渲染等等相关的工作
    • JavaScriptCore:解析、执行JavaScript代码
  • 另外一个强大的JavaScript引擎就是V8引擎

V8引擎的执行原理

  • V8引擎
    • 是用C++编写的Goole开源高性能JavaScript和WebAssembly引擎,用于Chrome和Node.js等
    • V8可以独立运行,也可以嵌入到任何C++应用程序中

V8引擎解析执行JS代码
V8引擎就是将高级语言代码最终转成二进制代码交给CPU执行

  1. JS源代码解析成AST抽象语法树(知道代码结构)
  2. Ignition成字节码(跨平台的,可以运行在window或者Mac上)
  3. TurboFan来收集信息,生成优化的机器码,之后重复调用时,直接运行优化的机器码就可以
 function sum(num1,num2) {
            return num1 + num2
        }
        // 多次调用时,每次都需要将字节码翻译成机器指令,效率很低
        // 于是有了TurboFan来收集信息,生成优化的机器码,之后重复调用时,直接运行优化的机器码就可以
        sum(10,20)
        sum(20,30)

        // 当类型出现变化,优化的机器码会反优化成字节码,重新由字节码开始运行,翻译成机器指令在运行
        // 这样会导致性能降低,所以在开发中尽量保证类型一致
        // 所以TS在一定程度上可以提高JS性能
        sum("10","20")

JavaScript高级_第4张图片

V8引擎的架构

  • Parse模块会将JS代码转换为AST(抽象语法树)
    • 这是因为解释器并不直接认识JS代码
    • 如果函数没有被调用,那么是不会被转换为AST的
  • Ignition是一个解释器,会将AST转换为ByteCode(字节码)
    • 同时会收集TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算)
    • 如果函数只调用一次,lgnition会解释执行字节码
  • TueboFan是一个编译器,可以将字节码编译成CPU可以直接执行的机器码
    • 如果一个函数被多次调用,会被标记为热点函数,就会被TurboFan转换成优化的机器码,提高代码的执行性能。
    • 但是,机器码实际上也会被还原为ByteCode(字节码),这是因为如果在后续执行函数的过程中,类型发生了变化(比如sum函数原来执行的是number类型,后来执行变成了string类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码
    • 还有一个核心模块是垃圾回收器
 function sum(num1,num2) {
            return num1 + num2
        }
        // 多次调用时,每次都需要将字节码翻译成机器指令,效率很低
        // 于是有了TurboFan来收集信息,生成优化的机器码,之后重复调用时,直接运行优化的机器码就可以
        sum(10,20)
        sum(20,30)

        // 当类型出现变化,优化的机器码会反优化成字节码,重新由字节码开始运行,翻译成机器指令在运行
        // 这样会导致性能降低,所以在开发中尽量保证类型一致
        // 所以TS在一定程度上可以提高JS性能
        sum("10","20")

V8引擎的解析图

先了解两个概念

  • 词法分析(lexical analysis)
    • 将字符序列转换成token序列的过程
    • token是记号化(tokenization)的缩写
    • 词法分析器,也叫扫描器(scanner)
  • 语法分析(parsing)
    • 语法分析器也可以称之为parser
  1. blink解析html文件,遇到script,拿到js原码,交给v8引擎-Stream
  2. scanner词法分析,即分词记号化
    • var name = "why"
    • 将var、name、=、"why"标记成token,以区分,确定var是第一个词,并在之后的语法分析中确定是在声明变量
    • 然后在AST中声明这一行语句是声明变量的,如果前面是一个function说明在定义函数
    • 所以js中关键字很重要,js在进行词法分析之后,拿到关键字,进行第二步语法分析
  3. parsing语法分析:语法分析中,确定ver在声明变量,确定name是一个变量标识符
  4. 生成AST
  5. 生成字节码

JavaScript高级_第5张图片

JavaScript的执行过程

  • 假如有下面一段代码,它在JavaScript中是如何被执行的?
 var name = "why"
       function foo() {
        var name = 'foo'
        console.log(name);  
       }

       var num1 = 20
       var num2 = 30
       var result = num1 + num2

       console.log(result)

       foo()

一、初始化全局对象

  • js引擎会在执行代码之前,会在堆内存中创建一个全局对象:Global Object(GO)
    * 该对象在浏览器中就是window
    * 该对象 所有的作用域(scope) 都可以访问
    * 里面会包含Date、Array、String、Number、setTimeout、setInterval等等
    * 其中还有一个window属性指向自己
    JavaScript高级_第6张图片

二、全局代码的执行过程

  • js引擎内部有一个执行上下文栈(简称ECS),它是用于执行代码的调用栈
  • 那么现在它要执行谁?执行的就是全局的代码块
    • 全局的代码块为了执行会构建一个全局执行上下文Global Execution Context(GEC)
    • GEC会被放入到ECS中执行
  • 全局执行上下文放到执行上下文栈中包含两部分内容:
    • 第一部分:在代码执行前,在parser转成AST的过程中,会将全局定义的变量、函数等加入到GlobalObject中,但是并不会赋值
      • 这个过程也称之为变量的作用域提升
      • 基础数据类型的变量值保存在栈内存中,而变量对象则包含有关这些变量和函数声明的信息。
    • 第二部分:在代码执行中,对变量赋值,或者执行其他的函数

认识VO对象:

  • 每一个执行上下文会关联一个VO(Variable Object,变量对象),变量和函数声明会被添加到这个VO对象中(其中函数会被优先添加,如果有同名的变量赋值如var foo = 1,会覆盖之前的指向函数对象的指针)
  • 对于全局上下文来说,这个VO就是GO
    JavaScript高级_第7张图片

三、函数代码的执行流程

  • 在执行过程中执行到一个函数时,就会根据函数体创建一个函数执行上下文(简称FEC),并且压入到EC Stack中
  • 每一个执行上下文都会关联一个VO,那么函数执行上下文关联的VO是什么呢?
    • 当进入一个函数执行上下文时,会创建一个AO对象(Activation Object)
    • 这个AO对象会使用arguments作为初始化,并且初始值是传入的参数
    • 这个AO对象会作为执行上下文的VO来存放变量的初始化
      JavaScript高级_第8张图片

四、函数多次调用的情况

  • foo函数第一次执行
  • foo第一次执行完,会弹出第一次的函数执行上下文(关联的VO对象不一定被销毁),全局执行上下文继续执行,于是第二次调用foo函数
  • foo函数第二次执行,新建执行上下文,关联一个新的VO对象

  • JavaScript高级_第9张图片

函数代码相互调用

 var message = "Global Message"
        var obj = {
            name: "why"
        }
        function bar() {
            console.log("bar function");
            var address = "bar"
        }

        function foo(num) {
            var message = 'Foo Message'
            bar()
            var age = 18
            var height = 1.88
            console.log("foo function")
        }

        foo(123)

执行过程:

  1. 创建全局执行上下文和全局对象,在全局对象中声明变量message、obj等值为undefined和声明函数bar(关联一个函数对象)、foo(关联一个函数对象)(函数会优先声明,因为js希望你可以在函数声明之前调用函数)
  2. 执行全局代码, var message = "Global Message",会在栈中执行并将值保存在栈中,但是在VO上也会添加一个属性message= “Global Message”;obj是引用类型,会在堆中创建一个obj对象,并将地址赋值给obj;
  3. foo(123)会创建一个新的执行上下文,关联一个新的AO对象,AO对象中一样先将变量message、age、height赋值为undefined
  4. 调用bar(),又会创建一个新的执行上下文入栈,关联一个新的AO对象,执行完bar中的代码,该执行上下文出栈,关联的AO对象销毁
  5. 继续执行foo的执行上下文,age赋值为18、height赋值为1.88,打印foo function。foo执行上下文出栈,关联的AO对象销毁
  6. 继续执行全局执行上下文栈

JS执行中变量查找的作用域链

  1. 先在自己的VO对象中查找,如果有对应的变量则使用该变量
  2. 如果自己的VO对象中没有该变量,沿着作用域链上查找
    作用域链
    • 当进入一个执行上下文的时,执行上下文也会关联一个作用域链
      • 作用域链是一个对象列表,用于变量标识符的求值
      • 当进入一个执行上下文时,这个作用域链被创建,并且根据代码类型,添加一系列的对象
    • 全局执行上下文的作用域链只有GO
    • 函数在声明创建的时候就确定作用域链了
       var message = "Global Message"

        function foo() {
            console.log(message);
        }

        var obj = {
            bar: function () {
                var message = "bar message"
                //当这里foo被调用时,打印出来的message的值是Global Message
                // 因为函数的作用域链在创建时就确定了
                foo()
            }
        }

        obj.bar()

JavaScript高级_第10张图片

函数代码多层嵌套作用域链

  var message = "hello world"
        debugger
        function foo() {
            var message = "hello foo"
            function bar() {
                var message = "hello bar"
                console.log(message);
            }

            return bar
        }
        var bar = foo()
        bar()

JavaScript高级_第11张图片

作用域面试题

   // 1.面试题一:
        var n = 100
        function  foo() {
            // 这里访问的n变量就是全局的n
            n = 200
        }
        foo()

        console.log(n);//200
   // 面试题二:
        var n = 100
        function foo(){
            console.log(n);//undefined
            var n = 200 //作用域提升
            console.log(n)//200
        }

        foo()
     // 3.面试题三:
        var n = 100

        function foo1() {
            console.log(n); 
        }

        function foo2() {
            var n = 200
            console.log(n)//200
            foo1()//100
        }

        foo2()
	   // 面试题四:
        var a = 100

        function foo() {
            console.log(a); //undefined
            return //在代码执行时才return
            var a = 100 //在代码解析时就创建了a
        }

        foo()
  // 面试题五:
        function foo() {
            var a = b = 100
        }

        foo()
        console.log(b)//100 因为不使用var创建变量时,会默认放到全局window中
        console.log(a);//a is not defined

JS内存管理和闭包

认识内存管理

  • 不管是什么样的编程语言,在代码执行的过程中我们都是需要给它分配内存的
    • 有些我们需要手动管理内存,有些自动帮我们管理内存
  • 内存的管理都会有如下的生命周期
    • 分配申请你需要的内存(申请)
    • 使用分配的内存(存放一些东西,比如对象等)
    • 不需要使用时,对其进行释放
  • 对于开发者来说,JavaScript的内存管理是自动的、无形的
    • 我们创建的原始值、对象、函数…这一切都会占用内存
    • 我们不需要手动对它们进行管理,JS引擎会帮助我们处理好它

JS的内存管理

  • JS在定义数据时为我们分配内存
  • 但是内存分配方式是一样的吗?
    • JS对于原始数据类型的内存分配会在执行时,直接在栈空间进行分配
    • JS对于复杂数据类型内存的分配会在堆内存中开辟一块空间,并且将这块空间的指针返回值变量引用

JS的垃圾回收

  • 当内存不再需要的时候,我们需要对其进行释放,以便腾出更多的内存空间
  • 大部分现代的编程原因都有自己的垃圾回收机制:
    • 垃圾回收(GC)
    • 对于那些不再使用的对象,我们称之为垃圾,需要被回收,以释放更多的内存空间
  • GC怎么知道哪些对象是不再使用的?
    • GC的实现以及对应的算法

常见的GC算法-引用计数

  • 引用计数:
    • 一个对象有一个引用指向它时,那么这个对象的引用就+1
    • 一个对象的引用为0时,这个对象就可以被销毁掉
  • 弊端是会产生循环引用

常见的GC算法-标记清除

  • 标记清除:
    • 核心思路是可达性
    • 设置一个根对象垃圾回收器会定期从这个根开始,找所有从根开始有引用到的对象,对于那些没有引用到的对象,就认为是不可用的对象
  • 这个算法可以很好的解决循环引用的问题
  • 在JS中这个根对象就是window对象

常见的GC算法-其他算法优化补充

  • JS引擎比较广泛的采用的就是可达性中的标记清除算法,V8引擎为了进行更好的优化,也结合了一些其他的算法
  • 标记整理 和“标记-清除” 相似
    • 不同的是,回收期间同时会将保留的存储对象搬运汇集到连续的内存空间,从而整合内存空闲,避免内存碎片化
  • 分代收集 – 对象被分成两组:"新的"和“旧的”
    • 许多对象出现,完成它们的工作并很快死去,它们可以很快被清理
    • 那些长期存活的对象会变得**“老旧”,而且被检查的频次也会减少**
  • 增量收集
    • 如果有许多对象,并且我们试图一次遍历并标记整个对象集,则可能需要一些时间,并在执行过程中带来明显的延迟
    • 所以引擎试图将垃圾收集工作分为几部分来做,然后将这几部分逐一处理
  • 闲时收集
    • 垃圾收集器只会在CPU空闲时尝试运行,以减少对代码执行的影响

JS闭包

闭包的定义

  • 闭包就是嵌套函数,使用了外层作用域中的变量

  • 如果嵌套函数被返回了出去被调用,一直存在,那么可能造成内存泄漏

  • 本质上就是上级作用域内变量的生命周期,因为被下级作用域内引用,没有被释放

// 写一个闭包函数
function foo(){
    const a = 2;
    
    function bar () {
        console.log(a)
    }

    return bar
}

let baz = foo()

baz()

闭包的使用过程和内存图

   function createAdder(count) {
            function adder(num) {
                return count + num
            }
            return adder
        }

        var adder5 = createAdder(5)
        adder5(100)//105

        var adder8 = createAdder(8)
        adder8(30)//38

JavaScript高级_第12张图片

闭包内存泄漏和释放内存

  • 假设之后不会再使用adder8
    • 内存泄漏:对于那些我们永远不会再使用的对象,但是对于GC来说,它不知道要进行释放的对应内存依然保留
  • 手动释放内存
adder8 = null

浏览器的优化操作

    function foo() {
            var name = 'foo'
            var age = 18
            var height = 1.88

            function bar() {
                // 浏览器做的优化
                // 因为这里没有用到age
                // 所以浏览器会将age从内存中删除,以释放内存
                console.log(name, height);  
            }
        }

        var fn = foo()
        fn()

JS函数的增强知识

函数对象的属性

  • JS中函数也是一个对象,那么对象中就可以有属性和方法
    • 属性name:访问函数的名词
    • 属性length:用于返回函数形参的个数
      • 剩余参数是不参与参数个数的
        // 定义函数
        function foo(a, b, c) {

        }
        var bar = function() {

        }
        // 自定义属性
        foo.message = "hello foo"
        console.log(foo.message);

        // 默认函数对象中已经有自己的属性
        // 1.name属性
        console.log(foo.name)

        // 2.length属性:参数(形参)个数
        // 不算剩余参数
        console.log(foo.length)

函数arguments的使用

  • arguments 是一个对应于 传递给函数的参数 的 类数组(array-like)对象
  // 定义函数
        function foo(m, n) {
            // 类数组对象
            console.log(arguments[3]);

            for (var arg of arguments) {
                console.log(arg)
            }
        }
        var bar = function () {

        }

        foo(10, 20, 30, 40)
  • array-like意味这它不是一个数组类型,而是一个对象类型
    • 但是它拥有数组的一些特性,比如说length,index索引
    • 但是没有数组的一些方法,比如filter、map等

arguments转Array

  • 在开发中,我们经常需要将arguments转成Array,以便使用数组的一些特性

    • 常见传化方式如下
    			// 定义函数
            function foo(m, n) {
                // 类数组对象
                console.log(arguments[3]);
    
                // 将arguments转成数组方式一:
                var newArguments = []
                for (var arg of arguments) {
                    newArguments.push(arg)
                }
                console.log(newArguments)
    
                // 将arguments转成数组方式二:ES6方法
                // 传入可迭代对象
                var newArgs1 = Array.from(arguments)
                console.log(newArgs1)
                // 使用扩展运算符
                var newArgs2 = [...arguments]
                console.log(newArgs2)
    
                // 将arguments转成数组方式三:slice
                var newArgs = [].slice.apply(arguments)
                var newArgs3 = Array.prototype.slice.apply(arguments)
                console.log(newArgs)
            }
    

箭函不绑定arguments

  • 箭头函数是不绑定arguments的,所以箭头函数中使用arguments会去上层作用域查找

函数的剩余参数

  • ES6引进了剩余参数,可以将不定量的参数放入到一个数组中
   // 剩余参数:rest parameters
        // 剩余参数需要写到参数列表最后
        function foo(num1, num2, ...args) {
            console.log(args);
        }

        foo(20, 30, 111, 222, 343)
  • 剩余参数和arguments的区别?
    • 剩余参数只包含那些没有对应形参的实参,而arguments对象包含了传给函数的所有实参
    • rest参数是一个真正的数组,而arguments不是
    • arguments是早期ES中为了方便去获取所有的参数提供的一个数据结构,而rest参数是ES6来替代arguments的
  • 剩余参数必须放到最后一个位置,否则会报错

纯函数的理解和应用场景

理解JS纯函数

  • 函数式编程有一个非常重要的概念叫纯函数
    • 在react开发中纯函数是被多次提及的
    • 比如react组件被要求是一个纯函数,redux中的reducer也被要求是一个纯函数
  • 纯函数的维基百科定义:
    • 若一个函数符合以下条件,那么称之为纯函数
      • 相同的输入值时,需产生相同的输出
      • 函数的输出与输入值以外的其他隐藏信息或状态无关
      • 能有语义上可观察的函数副作用,诸如“触发事件”等
  • 总结:
    • 确定的输入,一定会产生确定的输出
    • 函数在执行过程中,不能产生副作用

  • 什么是副作用?
    • 表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量,修改参数或者改变外部的存储

  • 纯函数的作用和优势
    • 不需要关心外层作用域的值,目前是什么状态
    • 调用函数时,确定的输入一定产生确定的输出

柯里化和柯里化函数

  • 维基百科对柯里化的解释
    • 是把接收多个参数的函数,变成接受一个单一参数的函数,并且返回接受余下的参数,而且返回结果的新函数的技术
  • 总结:
    • 传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数
    • 这个过程就称之为柯里化
  • 柯里化是一种函数的转化,将一个函数从可调用的f(a, b, c)转化为可调用的f(a)(b)©
    • 柯里化不会调用函数,它只是对函数进行转换
      // 普通的函数
        function foo(x, y, z) {
            console.log(x + y + z);
        }

        foo(10, 20, 30)

        //柯里化函数
        function foo1(x) {
            return function(y) {
                return function(z) {
                    console.log(x, y, z)
                }
            }
        }

        foo1(10)(20)(30)

  • 箭头函数的写法
      // 箭头函数的写法
    var foo3 = x => y => z => console.log(x, y, z)
    foo3(10)(20)(30)

自动柯里化函数的封装

  // 普通的函数
        function foo(x, y, z) {
            console.log(x + y + z);
        }

        function sum(num1, num2) {
            return num1 + num2
        }

        function logInfo(data, type, message) {
            console.log(`时间:${data} 类型:${type} 内容:${message}`)
        }

        // 自动转化柯里化函数
        function myCurrying(fn) {
            function curryFn(...args) {
                // 两类操作
                // 第一类操作:参数不够,操作返回一个新的函数,继续接受参数
                // 第二类操作:参数够,直接执行fn的函数
                if (args.length >= fn.length) {
                    // 执行第二类
                    return fn(...args)
                    // 考虑到this时的写法
                    // return fn.apply(this, args)
                } else {
                    // 执行第一类
                    // 继续接受参数
                    return function (...newArgs) {
                        // 重新进行判断
                        return curryFn(...args.concat(newArgs))
                        //return curryFn.apply(this, args.concat(newArgs))
                    }
                }
            }
            return curryFn
        }
        // 对其他函数进行柯里化
        var fooCurry = myCurrying(foo)
        fooCurry(10)(20)(30)
        fooCurry(10, 20, 30)

        var sumCurry = myCurrying(sum)
        console.log(sumCurry(10)(20))
        var sum5 = sumCurry(5)
        console.log(sum5(10))
        console.log(sum5(15))
        console.log(sum5(18))

        var logInfoCurry = myCurrying(logInfo)
        logInfoCurry("2022", "9", '29')
        logInfoCurry("2022")("9")("29")

组合函数的概念

  • 基本使用
     var num = 100

        // 第一步对数字*2
        function double(num) {
            return num * 2
        }

        // 第二步对数字**2
        function pow(num) {
            return num ** 2
        }

        console.log(pow(double(num)))
        
        // 将上面两个函数组合在一起,生成一个新的函数
        function composeFn(num) {
            return pow(double(num))
        }
        console.log(composeFn(num))

  • 组合函数的工具封装
   var num = 100
   
        // 第一步对数字*2
        function double(num) {
            return num * 2
        }

        // 第二步对数字**2
        function pow(num) {
            return num ** 2
        }

        console.log(pow(double(num)))

        // 封装的函数:传入多个函数,自动将多个函数组合,挨个调用
        function composeFn(...fns) {
            // 1.边界判断
            var length = fns.length
            if(length <= 0) return
            for (var i = 0; i < length; i++) {
                var fn = fns[i]
                if (typeof fn != "function") {
                    throw new Error(`fn ${i} must be function`)
                }
            }
            // 2.返回的新函数
            return function(...args) {
                // 传递this和参数数组
                var result = fns[0].apply(this, args)
                for (var i = 1; i < length; i++) {
                    var fn = fns[i]
                    result = fn.apply(this, [result])
                }
                return result
            }
        }

        var newFn = composeFn(double, pow)
        console.log(newFn(100))

with语句的使用

  • width语句:扩展一个语句的作用域链
 var obj = {
            message: "hello world"
        }

        with (obj) {
            console.log(message);//hello world
        }
  • 不建议使用

eval函数的使用

  • 内建函数eval允许执行一个代码字符串
    • eval是一个特殊的函数,它可以将传入的字符串当作JS代码来运行
    • eval会将最后一句执行语句的结果,作为返回值
var codeString = `var message = 'hello world'; console.log(message);`
eval(codeString)//hello world
  • 不建议使用
    • 可读性差
    • 作为字符串,可能在执行过程中被恶意纂改,被攻击
    • eval的执行必须经过JS解释器,不能被JS引擎优化

严格模式

  • 再ES5中,JS提出了严格模式的概念
    • 严格模式是一种具有限制性的JS模式,从而使代码隐式的脱离了“懒散模式”
    • 支持严格模式的浏览器在检测到代码中有严格模式时,会以更加严格的方式对代码进行检测和执行
  • 严格模式对正常的JS语义进行了一些限制
    • 严格模式通过抛出错误来消除一些原有的静默错误
    • 严格模式让JS引擎在执行代码时可以进行更多的优化(不需要对一些特殊的语法进行处理)
    • 严格模式禁用了在ES未来版本中可能会定义的一些语法

开启严格模式

  • 严格模式支持粒度话的迁移:
    • 可以支持在js文件中开启严格模式
    • 也支持对某一个函数开启严格模式
  • 严格模式通过在文件或者函数开头使用 use strict 来开启
 <script>
        // 给整个script开启严格模式
        "use strict"

        // 给一个函数开启严格模式
        function foo() {
            "use strict"
            
        }
    script>
  • 现代JS支持class和moudle,默认开启严格模式

严格模式限制

  1. 无法意外的创建全局变量
  2. 严格模式会引起静默失败的赋值操作抛出异常
  3. 严格模式下试图删除不可删除的属性
  4. 严格模式不允许函数参数有相同的名称
  5. 不允许0的八进制语法
  6. 在严格模式下,不允许使用with
  7. 在严格模式下,eval不再为上层引用变量
  8. 严格模式下,this绑定不会转成对象类型
    "use strict"

        // 1.无法意外的创建全局变量
        // function foo() {
        //     message = "hello world"
        // }
        // foo()
        // console.log(message);

        // 2.严格模式会引起静默失败(不报错也没有任何效果)的赋值操作抛出异常
        var obj = {
            name: "why"
        }
        Object.defineProperty(obj, "name", {
            writable:false
        })

        obj.name = "kobe"
        console.log(obj.name)

        // 3. 参数名称不能相同
        function foo(num, num) {

        }
        // 4. 不能以0开头
        console.log(0o123)

        // 5.eval函数不能为上层创建变量
        eval(`var message = "hello world"`)
        console.log(message)

        // 6.严格模式下,this绑定不会转成对象类型
        function foo() {
            console.log(this)
        }
        foo.apply("abc")
        foo.apply(123)

        // 函数的独立调用在默认模式下this绑定widnow,严格模式下this绑定undefined

对象的增强知识

对属性操作的控制

  • 如果我们想要对一个属性进行比较精准的操作控制,那么我们就可以使用属性描述符
    • 通过属性描述符可以精准的添加或修改对象的属性
    • 属性描述符需要使用Object.defineProperty来对属性进行添加或者修改

  • Object.defineProperty
    • 该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
  • 参数:
    • obj:要定义属性的对象
    • prop:要定义或修改的属性的名称或者Symbol
    • descriptor:要定义或修改的属性描述符
  • 返回值:
    • 被传递给函数的对象

  • 属性描述符分类:
    • 数据属性描述符
    • 存取属性描述符

数据描述符

  • configurable: 表示属性是否可以通过delete删除属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符
    • 当我们直接在一个对象上定义某个属性时,这个属性的configurable为true
    • 当我们通过属性描述符定义一个属性时,这个属性的configurable默认为false
  • enumerable: 表示属性是否可以通过for-in或者Object.keys()返回该属性
    • 当我们直接在一个对象上定义某个属性时,这个属性的enumerable为true
    • 当我们通过属性描述符定义一个属性时,这个属性的enumerable默认为false
  • writable:表示是否可以修改属性的值
    • 当我们直接在一个对象上定义某个属性时,这个属性的writable为true
    • 当我们通过属性描述符定义一个属性时,这个属性的writable默认为false
  • value:属性的value值,读取属性时会返回该值,修改属性时,会对其进行修改
    • 默认情况下这个值是undefined
		var obj = {
            name: "why",
            age: 18
        }

        Object.defineProperty(obj, "name", {
            configurable: false ,//告诉js引擎,obj对象的name属性不可以被删除
            enumerable:false,//不可枚举 (for-in/Object.keys)
            writable: false,//不可以写入(只读属性)
            value: "hello world"//返回这个value
        })
        delete obj.name
        console.log(obj.name);//hello world

        // 通过Object.defineProperty添加一个新的属性
        Object.defineProperty(obj, "address", {})
        delete obj.address
        console.log(obj.address)//undefined, 没有被删掉

        console.log(Object.keys(obj))//age

存取属性描述符

  • configurable:与数据属性描述符一致
  • enumerable:与数据属性描述符一致
  • get:获取属性时会执行的函数,默认为undefined
  • set:设置属性时会执行的函数,默认为undefined
        var obj = {
            name: "why",
        }

        // 对obj对象中的name添加描述符(存取属性描述符)
        var _name = ""
        Object.defineProperty(obj, "name", {
            configurable: true,
            enumerable: true,
            set: function(value) {
                console.log('hello world', value);
                _name = value
            },
            get: function() {
                console.log('get方法')
                return _name
            }
        })

        obj.name = "kobe"
        // 获取值
        console.log(obj.name)

多个属性描述符

  • Object.defineProperties()方法直接在一个对象上定义多个属性或者修改现有的属性,并且返回该对象
       var obj = {
            name: 'why',
            age: 18,
            height:1.88
        }
        // Object.defineProperty(obj, "name", {})
        // Object.defineProperty(obj, "name", {})
        // Object.defineProperty(obj, "name", {})

        // 新增的方法
        Object.defineProperties(obj, {
            name:{
                configurable:true,
                enumerable:false,
            },
            age: {

            },
            height: {
                
            }
        })

对象额外方法的补充

		var obj = {
            name: 'why',
            age: 18,
            height: 1.88
        }
        // 获取对象的属性描述符
        console.log(Object.getOwnPropertyDescriptor(obj, "name"));
        console.log(Object.getOwnPropertyDescriptors(obj));

        // 禁止对象扩展新属性:prevenExtensions
        // 给一个对象添加新的属性会失败(在严格模式下会报错)
        Object.preventExtensions(obj)
        obj.address = "china"
        console.log(obj.address)//undefined

        // 密封对象,不允许配置和删除属性:seal
        // 实际是调用preventExtendsions
        // 并且将现有属性对的configurable:false
        Object.seal(obj)
        delete obj.name
        console.log(obj.name)//why

        // 冻结对象,不允许修改现有属性:freeze
        // 实际上是调用seal
        // 并且将现有属性的writable:false
        Object.freeze(obj)
        obj.name = "kobe"
        console.log(obj.name)//why

ES5实现继承

认识对象的原型

  • JS中每个对象都有一个特殊的内置属性[[prototype]],这个特殊的对象可以指向另外一个对象
  • 那么这个对象有什么用呢?
    • 当我们通过引用对象的属性key来获取一个value时,它会触发[Get]的操作
    • 这个操作会首先检查该对象是否有对应的属性,如果有的话就使用它
    • 如果对象中没有属性,那么会访问对象[[prototype]]内置属性指向的对象上的属性
  • 只要是对象都会有这个内置属性
  • 获取方式:
 var obj = {
            name: "why",
            age: 18
        }
        console.log(obj);

        // 获取对象的原型
        console.log(obj.__proto__)
        console.log(Object.getPrototypeOf(obj))
        console.log(Object.getPrototypeOf(obj) === obj.__proto__) //true

        // 这个原型有什么用?
        // 当我们通过[[get]]方式获取一个属性对应的value时
        // 它会优先在自己的对象中查找,如果找到直接返回
        // 如果没有找到,那么会在原型对象中查找
        console.log(obj.name)

        obj.__proto__.message = "hello world"
        console.log(obj.message)//hello world

函数对象的原型

  • 将函数看作是一个函数时,它是具备prototype(显式原型)
    • 作用:用来构建对象时,赋值给对象的隐式原型
 		var obj = {}
        function foo() {}

        // 1.将函数看成是一个普通的对象时,它时具备__proto__(隐式原型)
        // 为什么叫隐式原型,因为我们一般不会直接去获取它
        // 作用:查找key对应的value时,会找到原型身上
        console.log(obj.__proto__);
        console.log(foo.__proto__);

        // 2.将函数看作是一个函数时,它是具备prototype(显式原型)
        // 作用,用来构建对象时,赋值给对象的隐式原型
        // 对象没有prototype
         console.log(foo.prototype)
         console.log(obj.prototype)//undefined

  • 回忆一下new操作符:
    1. 在内存中创建一个新的对象(空对象)
    2. 这个对象内部的[[prototype]]属性即隐式原型会被赋值为该构造函数的prototype原型即显式原型
  • 那就意味这我们通过Person构造函数创建出来的所有对象的[[prototype]]属性都指向Person.prototype
  function Foo() {
            // 1.创建空的对象
            // 将Foo的显式原型赋值给空对象的隐式原型
        }

        console.log(Foo.prototype);

        var f1 = new Foo()
        var f2 = new Foo()
        console.log(f1.__proto__)
        console.log(f2.__proto__)
        console.log(f1.__proto__ === Foo.prototype)//true

  • 将方法放在原型上
        function Student(name, age, sno) {
            this.name = name
            this.age = age
            this.sno = sno

            // 方式一:导致每次创建一个新对象时,都会创建这三个方法,这是没必要的
            // this.running = function() {
            //     console.log(this.name + "running");
            // }
            // this.eating = function() {
            //     console.log(this.name + "eating");
            // }
            // this.studying = function() {
            //     console.log(this.name + "studying");
            // }
        }
        // 方式二:
        // 当我们多个对象拥有共同的值,我们可以将它放到构造函数的显式原型上
        // 由构造函数创建出来的所有对象,都会分享这些属性
        Student.prototype.running = function () {
            console.log(this.name + "running");
        }

        // 1.创建三个学生
        var stu1 = new Student('why1', 18, 111)
        var stu2 = new Student('why2', 28, 112)
        var stu3 = new Student('why3', 38, 113)


        stu1.running()

constructor属性

  • 显式原型对象上有一个constructor属性,会指向当前的函数对象
        // 函数对象中显式原型中重要的属性:constructor,指向Person函数对象
        function Person() {
            
        }

        var PersonPrototype = Person.prototype
        console.log(PersonPrototype);
        console.log(PersonPrototype.constructor);
        console.log(PersonPrototype.constructor === Person);//true

        console.log(Person.name)
        console.log(PersonPrototype.constructor.name)

        // 2.实例对象
        var p = new Person()
        console.log(p.__proto__.constructor)
        console.log(p.__proto__.constructor.name)

构造函数创建对象的内存表现

JavaScript高级_第13张图片

重写原型对象

      function Person() {

        }

        console.log(Person.prototype);

        // 在原有的原型对象上添加新的属性
        Person.prototype.message = "hello world"
        Person.prototype.info = { name: 'hhh', age: 30 }
        Person.prototype.running = function() {}
        Person.prototype.eating = function() {}

        // 直接赋值一个新的原型对象
        // 弊端:没有指向Person的constructor属性
        Person.prototype = {
            message: "hello person",
            info: { name: "hhh", age: 30},
            running: function() {},
            eating: function() {},
            // 自定义指向Person的constructor属性
            // constructor: Person
        }
        Object.defineProperty(Person.prototype, "construcotor", {
            configurable: true,
            writable: true,
            enumerable:false,
            value: Person
        })
        console.log(Object.keys(Person.prototype))
        var p1 = new Person()
        console.log(p1.message)//hello person

面向对象的特性-继承

  • 面向对象有三大特性: 封装、 继承、 多态
    • 封装: 我们前面将属性和方法封装到一个对象或者类中,可以称之为封装的过程
    • 继承:继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态前提
    • 多态:不同的对象在执行时表现出不同的形态
  • 继承
    • 继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要继承过来使用即可
    • 在很多编程语言中,继承也是多态的前提

默认原型链和自定义原型链

  • 原型链:
    • 我们从一个对象上获取属性,如果在当前对象中没有获取到就会沿着原型链去它的原型上面获取
   // 1.{}的本质
        //var info = {}
         // 相当于  var info = new Object()
        //console.log(info.__proto__);
        //console.log(info.__proto__ === Object.prototype);//true

        //2. 原型链
        var obj = {
            name: "why",
            age: 18
        }
        // 查找顺序
        // 1.obj上面找到
        // 2.obj.__proto__上找到
        // 3.obj.__proto__.___proto__ => null 上查找
       // console.log(obj.message)//undefined

        //3.对现有的代码进行改造
        obj.__proto__ = {
            // message: "hello world"
        }
        obj.__proto__.__proto__ = {
            message:'hello world'
        }
        console.log(obj.message)

通过原型链实现继承

Stu类实现继承Person类的属性和方法

  • 方式一:父类的原型直接赋值给子类的原型(错误做法)

    • 缺点:父类和子类共享一个原型对象,修改了任意一个,另外一个也被修改
  • 方式二:创建一个父类的实例对象(new Person()),用这个实例对象作为子类的原型

  • 原型链继承的弊端:某些属性(Person类中的name和age属性)其实是保存在p对象上的

    • 第一:我们通过**直接打印对象是看不到这个属性(Person类中的name和age属性)**的
    • 第二:这个属性被多个对象共享,如果这个对象是一个引用类型,那么就会造成问题
    • 第三:不能给Person传递参数(让每个stu有自己的属性),因为这个对象是是一次性创建的(没办法定制化)
    // 定义Person构造函数(类)
        function Person(name, age) {
            this.name = name
            this.age = age
        }

        Person.prototype.running = function() {
            console.log("running");
        }
        Person.prototype.eating = function() {
            console.log('eating')
        }

        // 定义学生类
        function Student(sno, score){
            //this.name = name
           // this.age = age
            this.sno = sno
            this.score = score
        }

        // 实现继承方式一:父类的原型直接赋值给子类的原型(错误做法)
        // 缺点:父类和子类共享一个原型对象,修改了任意一个,另外一个也被修改
        // Student.prototype = Person.prototype

        // 方式二:创建一个父类的实例对象(new Person()),用这个实例对象作为子类的原型
        var p = new Person("why", 18)
        Student.prototype = p

        // Student.prototype.running = function() {
        //     console.log("running")
        // }
        // Student.prototype.eating = function() {
        //     console.log('eating')
        // }
        Student.prototype.studying = function() {
            console.log("studying")
        }

        // 创建学生
        var stu1 = new Student("kobe", 18, 111, 110)
        stu1.running()
        stu1.studying()

        console.log(stu1.name, stu1.age)

借用构造函数继承

  • 为了解决原型链继承中属性继承存在的问题,开发人员提供了:借用构造函数继承
  • 借用继承的做法:在子类型构造函数的内部调用父类型构造函数
    • 可以通过apply()和call()方法在新创建的对象上执行构造函数,从而实现定制化父类上的一些属性
    function Student(name, age, sno, score){
            
            // 重点:借用构造函数
            Person.call(this, name, age)

            this.sno = sno
            this.score = score
        }

组合借用继承的问题

  • 组合继承(原型链继承和构造函数继承的结合)是JavaScript最常用的继承模式之一
  1. 无论什么情况下,都会调用两次父类构造函数
    1. 一次在new p对象(创建子类原型)的时候
    2. 另一次在子类构造函数内部(借用父类构造函数)的时候
  2. 所有的子类实例会拥有两份父类的属性
    1. 一份是实例的原型中
    2. 一份是实例自己本身的

寄生组合式继承

  • 继承的目的:重复利用另外一个对象的属性和方法
  • 寄生组合式继承
    • 原型链
    • 借用
    • 原型式对象(新建一个对象)
    • 寄生式函数

        // 满足什么条件
        //  1. 必须创建一个对象
        //  2. 这个对象的隐式原型指向父类的显式原型
        //  3. 将这个对象赋值给了类的显式原型

        function Person() { }

        function Student() { }

        // 1.之前的做法
        var p = new Person()
        Student.prototype = p

        // 2. 方案一
        var obj = {}
        // _proto_属性在有些浏览器可能不兼容
        // obj._proto_ = Person.prototype
        Object.setPrototypeOf(obj, Person.prototype)
        Student.prototype = obj

        // 3. 方案二:和之前的做法相比更具通用性,也更具兼容性
        function F() { }
        F.prototype = Person.prototype
        Student.prototype = new F()

        // 4. 方案三
        // Object.create实现创建一个新对象,并且赋值一个原型对象
        var obj3 = Object.create(Person.prototype)
        Student.prototype = obj3



        // 封装方案三:寄生组合式继承
        // 寄生式函数,新建了一个对象,而不是直接使用父类的实例
        function inherit(Subtype, Supertype) {
            var obj = Object.create(Supertype.prototype)
            Subtype.prototype = obj

            //使用数据属性描述符添加construct属性
            Object.defineProperties(Subtype.prototype, "contructor", {
                enumerable: false,
                configurable: true,
                writable: true,
                value: Subtype
            })
        }

        // 如果担心Object.create有兼容性问题时,自定义createObject函数
        function createObject(o) {
            function F() { }
            F.prototype = o
            return new F()
        }
       
//inde.html

  <script src="./index.js">script>
    <script>
        function Person(name, age, height) {
            this.name = name
            this.age = age
            this.height = height
        }
        Person.prototype.running = function() {
            console.log("running");
        }
        Person.prototype.eating = function() {
            console.log("eating")
        }

        function Student(name, age, height, sno) {
            Person.call(this, name, age, height)
            this.sno = sno
        }

        inherit(Student, Person)

        Student.prototype.studying = function() {
            console.log("studying")
        }

        var stu1 = new Student("why", 18, 1.88, 111)
        console.log(stu1)
        stu1.running()
        stu1.eating()
        stu1.studying()
    script>

Object是所有类的父类

        function Person() {}
        function Student() {}
        function Teacher() {}

        inherit(Student, Person)

        console.log(Person.prototype.__proto__ === Object.prototype);

        // 在Object的原型上添加属性
        Object.prototype.message = "hello world"

        var stu = new Student()
        console.log(stu.message)//hello world

        // Object原型上已存的一些方法
        console.log(Object.prototype)
        console.log(stu.toString())

对象的方法补充

  • hasOwnProperty
    • 对象是否有某一个属于自己的属性(不是原型上的属性)
  • in/for in操作符
    • 判断某个属性是否在某个对象或者对象的原型上
  • instanceof:(实例对象和类之间的关系)
    • 用于检测构造函数的prototype,是否出现在某个实例对象的原型链上
  • isPrototypeOf:(对象之间的关系)
    • 用于检测某个对象,是否出现在某个实例对象的原型链上

原型继承关系式图解

  1. p1是Person的实例对象
  2. obj是Object的实例对象
  3. Function/Object/Foo都是Function的实例对象
  4. 原型对象默认创建时,隐式原型都是指向Object的显式原型的(Object的显式原型的显式原型指向null)
  5. Object是Person/Function的父类

JavaScript高级_第14张图片

ES6实现继承

认识class定义类

  • 按照前面的构造函数的形式来创建类,不仅仅与编写普通的函数过于相似,而且代码并不容易理解
  • 在ES6中使用了class关键字来直接定义类
  • 但是类本质上依然是之前所讲的构造函数、原型链的语法糖而已
      // ES6定义类
            // {} :对象/代码块/类的结构
            class Person {

            }

            // 创建实例对象
            var p1 = new Person()
            var p2 = new Person()
            console.log(p1, p2);

            // 表达式定义方法
            var Student = class {

            }
            var stu1 = new Student()
            console.log(stu1)

class类中实例方法和构造方法

 class Person {
            // 1.类中的构造函数
            // 当我们通过new关键字调用一个Person类时,默认调用class中的constructor方法
            constructor(name, age) {
                // 在constrotor中跟之前的写法一样
                this.name = name
                this.age = age
            }

            // 2.实例方法 (定义在类的显式原型上,只有实例对象可以使用)
            running() {
                console.log(this.name + "running")
            }
        }

        var p1 = new Person("why", 18)
        console.log(p1);
        //Person类的显式原型也会赋值给实例对象p1的隐式原型
        console.log(Person.prototype === p1.__proto__)//true
        p1.running()

class定义访问器方法

  • 对象中访问器的编写
  // 针对对象
        // 方式一:描述符
        var obj = {
            _name: "why"
        }
        Object.defineProperty(obj, "name", {
            configurable: true,
            enumerable: true,
            set: function () {

            },
            get: function () {

            }
        })
 // 方式二:直接在对象中定义访问器(不推荐)
        var obj = {
            _name: "why",
            set name(value) {
                console.log("setter方法调用")
                this._name = value
            },
            get name() {
                console.log("getter方法调用")
                return this._name
            }
        }
        obj.name = "join"
        console.log(obj.name)

  • 类的访问器方法的编写
   class Person{
            // 程序员约定俗成:以下划线开头的属性和方法,不在外界访问
            constructor(name, age) {
                this._name = name
                this._age = age
            }

            set name(value) {
                this._name = value
            }

            get name() {
                return this._name
            }
        }

        var p1 = new Person("why", 18)
        p1.name = "kobe"
        console.log(p1.name);
  • 类的访问器的应用场景
	// 2.访问器应用场景
        class Rectangle {
            constructor(x, y, width, height) {
                this.x = x
                this.y = y
                this.width = width
                this.height = height
            }

            get position() {
                return { x: this.x, y: this.y}
            }
        }

        var react1 = new Rectangle(10, 20, 100, 200)
        console.log(react1.position)

类的静态方法(类方法)

  • 静态方法通常用于定义直接使用类来执行的方法,不需要有类的实例,使用static关键字来定义
	 class Person {

            // 实例方法
            running() { }

            // 类方法(静态方法):直接通过类来调用
            static randomPerson() {
                console.log('类方法被调用');
            }
        }

        var p1 = new Person()
        Person.randomPerson()

ES6通过extends实现继承

  // 定义父类
        class Person {
            constructor(name, age) {
                this.name = name
                this.age = age
            }
            running() {
                console.log("running")
            }
            eating() {
                console.log("eating")
            }
        }

        class Student extends Person {
            constructor(name, age, sno, score) {
                // this.name = name
                // this.age = age
                super(name, age)
                this.sno = sno
                this.score = score
            }
            studying() {
                console.log('studying')
            }
        }

        var stu1 = new Student("why", 18, 111, 12)
        console.log(stu1)
        stu1.running()

        class Teacher extends Person {
            constructor(name, age, title) {
                this.name = name
                this.age = age
                this.sno = title
            }
            teaching() {
                console.log("teaching")
            }
        }

super关键字

  • super关键字:

    • 执行super.method来调用父类方法
    • 执行super()来调用一个父类constructor(只能在我们的construtor中)
  • super可以用在:子类的构造方法、实例方法、静态方法

继承内置类

  • 我们也可以让我们的类继承自内置类,比如Array:
// 继承内置类的应用场景
        class MyArray extends Array {
            get lastItem() {
                return this[this.length - 1]
            }
            get firstItem() {
                return this[0]
            }
        }

        var arr = new MyArray(10, 20, 30)
        console.log(arr.lastItem);
        console.log(arr.firstItem);

类的混入mixin

  • JS中的继承只支持单继承,即只能有一个父类
    • 开发中如果想实现多继承,可以使用混入
    function mixinAnimal(BaseClass) {
            return class extends BaseClass {
                running() {
                    console.log("running");
                }
            }
        }

        function mixinRunner(BaseClass) {
            return class extends BaseClass {
                flying() {
                    console.log('flying')
                }
            }
        }

        class Bird {
            eating() {
                console.log("eating")
            }
        }

        var NewBird = mixinRunner(mixinAnimal(Bird))
        var bird = new NewBird()
        bird.flying()
        bird.running()
        bird.eating()

babel工具

babel在线转化工具

babel对类class的转化

//es6
  class Person {
            constructor(name, age) {
                this.name = name
                this.age = age
            }

            running() { }
            static randomPerson() { }
        }

        let p1 = new Person()

//es5
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }

        function _classCallCheck(instance, Constructor) {
            if (!(instance instanceof Constructor)) {
                // 如果不是Person的实例则报错,不允许构造函数直接调用
                throw new TypeError("Cannot call a class as a function");
            }
        }
        function _defineProperties(target, props) {
            for (var i = 0; i < props.length; i++) {
                var descriptor = props[i];
                descriptor.enumerable = descriptor.enumerable || false; 
                descriptor.configurable = true; 
                if ("value" in descriptor) descriptor.writable = true; 
                Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor);
            }
        }
        function _createClass(Constructor, protoProps, staticProps) {
            if (protoProps) _defineProperties(Constructor.prototype, protoProps);
            if (staticProps) _defineProperties(Constructor, staticProps); 
            Object.defineProperty(Constructor, "prototype", { writable: false }); 
            return Constructor;
        }
        function _toPropertyKey(t) {
            var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : String(i);
        }
        function _toPrimitive(t, r) {
            if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) {
                var i = e.call(t, r || "default");
                if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value.");
            } return ("string" === r ? String : Number)(t);
        }

        // 纯函数:相同的输入一定产生相同的输出,并且不会产生副作用
        var Person = /*#__PURE__*/function () {
            function Person(name, age) {
                _classCallCheck(this, Person);
                this.name = name;
                this.age = age;
            }
            _createClass(Person, [{
                key: "running",
                value: function running() { }
            }], [{
                key: "randomPerson",
                value: function randomPerson() { }
            }]);
            return Person;
        }();
        var p1 = new Person();

JS中的多态

  • 面向对象的三大特性:封装、继承、多态
  • 多态:
    • 不同的数据类型进行同一操作,表现出不同的行为,就是多态的体现

##Java中面向对象的多态表现

  • 严格意义的面向对象语言中,多态是存在如下条件的
    • 必须有继承(实现接口)
    • 必须有父类引用指向子类对象
//继承之后操作统一的接口   
class Shap {
            getArea(){}
        }

        class Circle extends Shap {
            
            constructor( radius ){
                super()
                this.radius = radius
            }

            getArea() {
                return this.radius * this.radius
            }
        }

        class Rectangle extends Shap{

            constructor( width, height ) {
                super()
                this.width = width
                this.height = height
            }
            getArea() {
                return this.width * this.height 
            }
        }

        var react1 = new Rectangle(100, 200)
        var c1 = new Circle(20, 30)

        function getShapeArea(shape) {
            console.log(shape.getArea())
        }

        getShapeArea(react1)
        getShapeArea(c1)

JS面向对象的多态理解

 // 多态的表现:JS到处都是多态
        function sum(a1, a2) {
            return a1 + a2
        }

        sum(20, 30)
        sum("abc", "cba")

        // 多态的表现形式二:
        var foo = 123
        foo = "hello world"
        foo = {

        }
        foo = []

对象字面量的增强写法

  • ES6中对字面量的增强主要包括以下几部分
    • 属性的简写
    • 方法的简写
    • 计算属性名
		/*
            1.属性的增强 
            2.方法的增强
            3.计算属性的写法
        */
        var name = "why"
        var age = 18

        var key = "address"

        var obj = {
            // 1.属性的增强
            name,
            age,
            // 2.方法的增强
            running: function() {
                console.log(this)
            },
            swimming() {
                console.log(this)
            },
            // 3.计算属性名
            [key]: "安吉"
        }
        obj.swimming()

ES6-解构

  • ES6中新增了一个从数组或者对象中方便获取数据的方法,称之为解构
    • 解构赋值是一种特殊的语法,它使我们可以将数组或对象"拆包"至一系列变量中
  • 我们可以划分为数组的解构和对象的解构
   	    var names = ["a", "b", "c", "d"]
        var obj = { name: "why", age: 18, height: 1.88 }

        // 1.数组的解构

        // 1.1基本使用
        var [name1, name2, name3] = names
        console.log(name1, name2, name3);

        // 1.2顺序问题:严格的顺序
        var [name1, , name3] = names
        console.log(name1, name3)

        // 1.3 解构出数组
        var [name1, name2, ...newArray] = names
        console.log(name1, name2, newArray)

        // 1.4 解构的默认值
        // 当name3没有赋值时,使用默认值w
        var [name1, name2, name3 = "w"] = names

        // 2.对象的解构

        // 2.1. 基本使用
        var { name, age, height } = obj
        console.log(name, age, height)

        // 2.2. 顺序问题:对象的解构是没有顺序的,根据key解构
        var { height, name, age } = obj
        console.log(name, age, height)

        // 2.3.对变量进行重命名
        var { height: height2, name: name2, age: age2 } = obj
        console.log(name2, height2, age2)

        // 2.4 默认值
        var { height, name, age, address = "china" } = obj
        console.log(name, age, height, address) 

        // 2.5 对象的剩余内容
        var { name, age, ...newObj} = obj
        console.log(newObj)

  • 解构的应用场景
    • 比如开发中拿到一个变量时,自动对其进行解构使用
    • 比如对函数的参数进行结构
    // 应用:在函数中(其他类似的地方)
        function getPosition( {x, y}) {
            console.log(x,y)
        }
        getPosition({ x: 10, y: 20})

补充手写apply-call-bind

  • 手写实现apply,利用隐式绑定,使this绑定我们传入的对象
        function foo(name, age) {
            console.log(this, name, age);
        }

        // 1.给函数对象添加方法:
        Function.prototype.Myapply = function (thisArg, otherArgs) {
            // this -> 调用的函数对象
            // thisArg -> 传入的第一个参数,要绑定的this

            // 1.获取thisArg,并且确保是一个对象类型
            thisArg = (thisArg === null || thisArg === undefined) ? window : Object(thisArg)


            // thisArg.fn = this
            Object.defineProperty(thisArg, "fn", {
                configurable: true,
                value: this
            })

            // 检查 otherArgs 是否存在,如果不存在,设置为一个空数组
            // if (!otherArgs) {
            //     otherArgs = [];
            // }

            thisArg.fn(...otherArgs || [])

            delete thisArg.fn
        }

        foo.Myapply({ name: "why" }, ["james", 25])
        foo.Myapply(123)
        foo.Myapply(null)

  • 手写实现Call方法
  Function.prototype.Mycall = function (thisArg, ...otherArgs) {
            // 1.获取thisArg,并且确保是一个对象类型
            thisArg = (thisArg === null || thisArg === undefined) ? window : Object(thisArg)

            // thisArg.fn = this
            Object.defineProperty(thisArg, "fn", {
                configurable: true,
                value: this
            })

            thisArg.fn(...otherArgs)

            delete thisArg.fn
        }

        foo.Mycall({ name: "why" }, "james", 25)
        foo.Mycall(123)
        foo.Mycall(null)

  • 手写apply-call-bind封装
     function foo(name, age) {
            console.log(this, name, age);
        }
        // 1.封装思想
        // 1.1 封装到独立的函数中
        function execFn(thisArg, otherArgs, fn) {
            thisArg = (thisArg === null || thisArg === undefined) ? window : Object(thisArg)

            Object.defineProperty(thisArg, "fn", {
                configurable: true,
                value: fn
            })

            thisArg.fn(...otherArgs || [])

            delete thisArg.fn
        }

        // 1.2 封装到原型中
        Function.prototype.Myexec = function (thisArg, otherArgs) {
            thisArg = (thisArg === null || thisArg === undefined) ? window : Object(thisArg)

            Object.defineProperty(thisArg, "fn", {
                configurable: true,
                value: this
            })

            thisArg.fn(...otherArgs || [])

            delete thisArg.fn
        }

        // 1.给函数对象添加方法:
        Function.prototype.Myapply = function (thisArg, otherArgs) {

            // execFn(thisArg, otherArgs, this)

            this.Myexec(thisArg, otherArgs)
        }

        foo.Myapply({ name: "why" }, ["james", 25])
        foo.Myapply(123)
        foo.Myapply(null)

        Function.prototype.Mycall = function (thisArg, ...otherArgs) {

            // execFn(thisArg, otherArgs, this)
            this.Myexec(thisArg, otherArgs)
        }

        foo.Mycall({ name: "why" }, "james", 25)
        foo.Mycall(123)
        foo.Mycall(null)

ES6~ES13新特性(一)

let/const基本使用

  • let关键字:
    • 声明变量
  • const关键字:
    • 保存的数据一旦被赋值,就不能被修改
    • 如果赋值的引用类型,则可以通过引用修改对象的内容
  • let、const不允许重复声明变量

let/const没有作用域提升

  • let/const和var的区别:
    • var声明的变量会进行作用域提升
    • 使用let声明的变量,在声明之前访问会报错
 	 console.log(message);//可以访问,但是值是undefined
        // 1.var声明的变量会进行作用域的提升
        var message = "hello world"

        // 2.let/const声明的变量
        console.log(address)//会报错,不能访问
        let address = "china"
  • 作用域提升:在声明变量的作用域中,如果这个变量可以在声明之前被访问,就称之为作用域提升
  • 总结:let/const没有进行作用域提升,但是会在解析阶段被创建出来

let/const的暂时性死区

  • 在let、const定义的标识符真正执行到声明的代码以前,是不能被访问的
    • 从块作用域的顶部一直到变量声明之前,这个变量处于暂时性死区
    • 暂时性死区和定义的位置没有关系,和代码执行顺序有关系
    • 暂时性死区形成之后,在该区域中的这个标识符不能被访问
 // 暂时性死区
        function foo() {
            // 暂时性死区begin
            console.log(bar)
            // 暂时性死区end
            
            let bar = "bar"
            let baz = "baz"
        }
        foo()
   // 暂时性死区和定义的位置没有关系,和代码执行顺序有关系
        function foo() {
            console.log(message);
        }
        let message = "hello world"
        foo()
        console.log(message)
     // 3.暂时性死区形成之后,在该区域中的这个标识符不能被访问
        let message = "hello world"
        function foo() {
            console.log(message);//会报错,不会访问外部的message

            let message = "hello let"
        }
        foo()

let/const不添加window

  • var定义的变量是会默认添加到window上的
  • let/const定义的变量不会添加到window上
 // 1.var定义的变量是会默认添加到window上的
        var message = "hello world"
        var address = "china"

        console.log(window.message);
        console.log(window.address)
 // 2.let/const定义的变量不会添加到window上
        let message = "hello world"
        let address = "china"

        console.log(window.message);
        console.log(window.address)

let/const的块级作用域

  • 在ES5以及之前,只有全局作用域和函数作用域
  • 在ES6中新增了块级作用域,并且通过let、const、function、class声明的标识符是具有块级作用域的限制
    • 但是我们发现函数拥有块级作用域,但是外面依然是可以访问的
      • 这是因为引擎会对函数的声明进行特殊的处理,允许像var一样在块级作用域外进行访问

块级作用域的应用

  • 通过词法环境保存了每次的变量i

JavaScript高级_第15张图片

ES6~ES13新特性(二)

模版字符串基本使用

  • ES6之前,将字符串和一些动态变量结合在一起的方式非常丑陋

模版字符串基本使用

  		const name = "why"
        const age = 18
        // 1.ES6之前
        // const info = "my name is" + name + ", age is" + age

        // 2.ES6之后
        const info = `my name is ${name}, age is ${age}`
        console.log(info);

标签模版字符串的使用

  • 在React的styled-components库中使用
 function foo(...args) {
            console.log("参数", args);
        }
        foo("why", 18, 1.88)
        // 标签模版字符串
        foo`my name is ${name}, age is ${age}`

函数的默认参数

默认值的写法:

  • 两种不严谨的写法

    • 默认值写法一:
    arg1 = arg1 ? arg1p: "默认值"
    
    • 默认值写法二:
    arg1 = arg1 || "默认值"
    
  • 严谨的写法

    • 三元运算符
    arg1 = (arg1 === undefined ||  arg1 === null) ? "默认值": arg1
    
    • ES6之后新增的语法:??
    arg1 = arg1 ?? "默认值" 
    
  • 简便的写法:默认参数

    • 可以传入null,因为其中只对undefined进行了判断
    function foo(arg1 = "默认值" ,arg2 = '默认值'){}
    

默认参数的写法注意:

  • 有默认参数的形参尽量写到后面
  • 有默认参数的形参(和之后的形参),都不会计算在函数的length之内
  • 默认参数放到剩余参数前面

箭头函数的额外补充

  • 箭头函数是没有显式原型prototype的,所以不能作为构造函数,使用new来创建对象
  • 箭头函数也不绑定this、argument、super参数
  • 在ES6之后,定义一个类要使用class

展开语法

  • 展开语法:
    • 可以在函数调用/数组构造时,将数组表达式或者string在语法层面展开
    • 还可以在构造字面量对象时,将对象表达式按key-value的方式展开
  • 展开语法的场景
    • 函数调用时使用
    • 数组构造时使用
    • 构建对象字面量时,也可以使用展开运算符,这个是在ES9中添加的新特性
  • 展开运算符其实是一种浅拷贝

基本使用:


        // 基本使用
        // ES6
        const names = ["a", "b", "c", "d"]
        const str = "hello"

        function foo(name1, name2, ...args) {
            console.log(name1, name2, args);
        }

        foo(...names)
        foo(...str)

        // ES9
        const obj = {
            name: "why",
            age: 18
        }
        // 错误使用:在函数调用时,使用展开运算符,将对应的展开数据,进行迭代
        // 可迭代对象:数组/string/arguments
        // foo(...obj)

        // 在构建对象字面量时使用展开运算符
        const info = {
            ...obj,
            height:1.88,
            address: "china"
        }
        console.log(info)

引用赋值-浅拷贝-深拷贝

  • 引用赋值
const obj = {
    name: "why",
    age: 18,
    height: 1.88
}
//1.引用赋值
const info1 = obj

//2.浅拷贝
const info2 = {
    ...obj
}
info2.name = "kobe"
  • 浅拷贝内存图
  • 如果对象中有一个对象属性,浅拷贝只拷贝对象的引用地址

JavaScript高级_第16张图片

  • 深拷贝
    • 第三方库
    • 自己实现
    • 利用js机制,实现深拷贝JSON
  // 深拷贝
  const info3 = JSON.parse(JSON.stringify(obj))

JavaScript高级_第17张图片

数值的表示

  • ES6中规范了二进制和八进制的写法:
const num1 = 100
const num2 = 0b100
const num3 = 0o100
const num4 = 0x100
  • 在ES2021新增特性:数字过长时,可以使用_连接
const num5 = 100_000_000

Symbol的基本使用

  • Symbol是ES6中新增的一个基本数据类型
  • 为什么需要Symbol?
    • 在ES6之前,对象的属性名都是字符串形式,很容易造成属性名的冲突
    • 比如原来有一个对象,我们希望在其中添加一个新的属性和值,但是我们不确定其中的内容的情况下,很容易造成冲突,从而覆盖掉它内部的某个属性
  • Symbol就是为了解决上面的问题,用来生成一个独一无二的值
    • Symbol值就是通过Symbol函数来生成的,生成后可以作为属性名
    • 所以在ES6中,对象的属性名可以使用字符串,也可以使用Symbol值
const s1 = Symbol()
//加入对象中
const obj = {
    [s1]: "aaa"
}

const s2 = Symbol()
obj[s2] = "bbb"

const s3 = Symbol()
Object.defineProperty(obj, s3, {
    value: "aaa"
})

//获取Symbol对应的key
const keys = Object.getOwnPropertySymbols(obj)
for(const key of keys){
    
}
  • ES10的新特性中,我们可以在创建Symbol值的时候传入一个描述description
  • 相同值的Symbol
  • 如果我们就想创建相同的Symbol
    • 我们可以使用Symbol.for方法传入相同的key
    • 并且可以通过Symbol.keyFor方法来获取对应的key
//如果相同的key,通过Symbol.for可以生成相同的Symbol值
const s5 = Symbol.for("ddd")
const s6 = Symbol.for("ddd")
console.log(s5 === s6) //true

//获取传入的key
Symbol.keyFor(s5)

Set的基本使用

  • 在ES6之前,我们存储数据的结构主要有两种:数组、对象
    • 在ES6中新增了另外两种数据结构:Set、Map,以及它们的另外形式WeakSet、WeakMap
  • Set是一个新增的数据结构,可以用来保存数据,类似于数组,但是和数组的区别是元素不能重复
    • 创建Set我们需要通过Set构造函数(暂时没有字面量创建的方式)
  // 创建Set
        const set = new Set()

        // 添加元素
        set.add(10)
        set.add(22)
        set.add(35)
        set.add(22)
        console.log(set);

        const info = {}
        const obj = {}
        set.add(info)
        set.add(obj)
        console.log(set);

        // 3.应用场景:数组去重
        const names = [ "a", "b", "a", "c", "d", "b"]

        // 去重方式一:
        // const newNames = []
        // for( const item of names) {
        //     if(!newNames.includes(item)) {
        //         newNames.push(item)
        //     }
        // }
        // console.log(newNames)

        // 去重方式二:
        const newNamesSet = new Set(names)
        const newNames  = Array.from(newNamesSet)
        console.log(newNames)


  • Set常见的属性:
    • size:返回Set中元素的个数
  • Set常用的方法:
    • add(value): 添加某个元素,返回Set对象本身
    • delete(value):从set中删除和这个值相等的元素,返回boolean类型
    • has(value):判断set中是否存在某个元素,返回boolean类型
    • clear():清空set中所有的元素,没有返回值
    • forEach(callback, [thisArg]):通过forEach遍历set
  • set是一个可迭代对象,也可以使用for…of
  set.forEach(item => console.log(item))

WeakSet使用

  • WeakSet和Set有什么区别呢?
    • weakSet只能存放对象类型,不能存放基本数据类型
    • WeakSet对元素的引用是弱引用,如果没有其他引用对某个对象进行引用,那么GC可以对该对象进行回收
  • 注意:WeakSet不能遍历
    • 因为WeakSet只是对对象的弱引用,如果我们遍历获取到其中的元素,那么可能造成元素不能被正常的销毁
    • 所以存储到WeakSet中的对象是没办法获取的
  • 应用场景
const pwset = new WeakSet()
class Person {
    constructor() {
        pwset.add(this)
    }
    running() {
        if(!pwset.has(this)) throw new Error("只能通过实例对象来调用running方法")
        console.log("runnning",this)
    }
}

Map的基本使用

  • ES6新增数据结构Map,用于存储映射关系
  • 在之前我们可以使用对象来存储映射关系,他们有什么区别呢?
    • 对象存储映射关系**只能使用字符串(ES6新增了Symbol)**作为属性名
    • 使用其他类型作为属性名会自动转为字符串类型
    • 但是某些时候我们可能希望通过其他类型作为key,比如对象

Map基本使用

       const info = { name: "why"}
        const info2 = { age: 18}
        // Map映射类型
        const map = new Map()
        map.set(info, "aaa")
        map.set(info2, "bbb")
        console.log(map);
  • Map常见的属性
    • size:返回Map中元素的个数
  • Map常见的方法:
    • set(key, value):在Map中添加key、value,并且返回整个Map对象
    • get(key):根据key获取Map中的value
    • has(key):判断是否包括某一个key,返回Boolean类型
    • delete(key):根据key删除一个键值对,返回Boolean类型
    • clear():清空所有的元素
    • forEach()
  • Map也可以通过for of进行遍历

WeakMap的使用

  • WeakMap和Map的区别?
    • WeakMap的key只能使用对象,不接受其他的类型作为key
    • WeakMap的key对对象的引用是弱引用,如果没有其他引用对某个对象进行引用,那么GC可以对该对象进行回收
  • 也是不能遍历的

ES6其他知识点

  • Promise:用于处理异步的解决方案
  • ES Module模块化开发
  • 等等

ES7~ES13新特性(三)

ES7-Array Includes

  • 在ES7之前,我们通过index Of判断返回值是否是-1来判断一个数组中是否包含某个元素
  • 在ES7中,我们可以通过includes来判断

ES7-指数运算符

  • 在ES7之前,使用Math.pow
  • 在ES7中,使用**运算符

ES8 Object values

  • 之前我们通过Object.keys获取一个对象所有的key
  • 在ES8中提供了Object.values来获取所有的value值

ES8 Object entries

  • 通过Object.entries可以获取一个数组,数组中会存放可枚举属性的键值对数组
    • 可以针对对象、数组、字符串进行操作

ES8 - String Padding

  • 某些字符串我们需要对其进行前后的填充,来实现某种格式化效果
  • ES8中增加了padStart 和padEnd方法,分别对字符串的首尾进行填充
 		// 1.应用场景一:对时间进行格式化
        const minute = "2".padStart(2, "0")
        const second = "6".padEnd(2, "0")
        console.log(`${minute}:${second}`);

        // 2.应用场景二:对一些敏感数据格式化
        const cardNumber = "1222334455555"
        cardNumber2 = cardNumber.slice(-4).padStart(cardNumber.length, "*")
        console.log(cardNumber2)

ES8 - Trailing Commas

  • 在ES8中,我们允许在函数定义和调用时在参数尾部多加一个逗号

ES8 - Object Descriptors

  • Object.getOwnPropertyDescriptors:获取对象的属性描述符
  • Async Function: async、await

ES9新增特性

  • Async iterators
  • 展开运算符
  • Promise finally

ES10 - flat flatMap

  • flat()的使用:将一个数组按照指定的深度遍历,将遍历到的元素和子数组中的元素组成一个新的数组,进行返回
 // 1.flat()的使用:将一个数组按照指定的深度遍历,将遍历到的元素和子数组中的元素组成一个新的数组,进行返回
        const nums = [10, 20, [111, 222], [333, 444], [[123, 321]], [231, 321]]
        const newNum1 = nums.flat(1)
        console.log(newNum1);
        const newNum2 = nums.flat(2)
        console.log(newNum2)
  • flatMap()方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组
    • flatMap是先进行map操作,再做flat的操作
    • flatMap中的flat相当于深度为1
  const messages = [
            "hello world",
            "hello flat",
            "hello flatmap"
        ]
        const finalMessage = messages.flatMap(item => item.split(" "))
        console.log(finalMessage)

ES10 - Object fromEntries

  • 将entries转换为对象
 // 应用场景
        const searchString = "?name=why&&age=18"
        const params = new URLSearchParams(searchString)
        console.log(params.get(name))
        console.log(params.entries())

        const paramObj =  Object.fromEntries(params.entries())
        console.log(paramObj)

ES10 - trimStart trimEnd

  • 去除一个字符串首尾的空格,我们可以使用trim方法
  • 如果单独去除前面或者后面呢?
    • ES10给我们提供了trimStart和trimEnd

ES10 其他知识点

  • Symbol description
  • Option catch binding

ES11 - BigInt

  • 大于Number.MAX_SAFE_INTEGER 的数值,表示的可能是不正确的
  • BigInt用于表示大的整数
    • BigInt的表示方法是在数值的后面加上n

ES11 - 空值合并运算符

  • ES11添加了空值合并运算符
    • foo为undefined或者null时使用默认值
const result = foo ?? "默认值"

ES11 - 可选链

  • 主要作用是让我们的代码在进行undefined和null判断时更加清晰
     const obj = {
            name: "why",
            frient: {
                name: "kobe",
                running: function() {
                    console.log("running");
                }
            }
        }
        // 这样使用不安全,不确定friend是否是undefined
        obj.frient.running()
        // 可选链的写法
        // 想判断有没有再获取
        obj?.frient?.running?.()

ES11 - Global This

  • 在ES11中对获取全局对象进行了统一的规范:globalThis

ES11 - for… in标准化

  • 对其进行了标准化,for…in是用于遍历对象中的key的

ES12 - FinalizationRegistry

  • FinalizationRegistry 对象可以让你在对象被垃圾回收时请求一个回调
  • 可以调用register方法,注册任何你想要清理回调的对象,传入该对象和所含的值
		let obj = { name: "why" }

        // 先创建对象
        const registry = new FinalizationRegistry(value => {
            console.log("对象被销毁了", value);
        })

        registry.register(obj, "obj")

        obj = null

ES12 - WeakRefs

  • 如果我们默认将一个对象赋值给另外一个引用,那么这个引用是一个强引用
    • 如果有多个这样的强引用存在,对象可能不会被真正清除
    • 如果我们希望是一个弱引用的话,可以使用WeakRef
let obj = { name:"why"}
let info = new WeakRef(obj)

ES12 - 逻辑赋值运算符

function foo(message) {
    //message = message || "默认值"
    message ||= "默认值"
    // message = message ?? "默认值"
    message ??= "默认值"
    //message = message && message.name
    message &&= message.name
}

ES12 - 其他知识点

  • 数字分隔符
  • 字符串replaceAll方法
const message = "my name is why"
//replace只替换第一个
const newMessage = message.replace("why", "kobe")
//replaceAll替换所有
const newMessage = message.replaceAll("why", "kobe")

ES13 - method .at()

  • 用来获取字符串、数组的元素,可以使用负值
name.at(-1)
name.at(1)

ES13 - Object.hasOwn(obj, porpKey)

  • 该方法是一个类方法
    • 用于判断对象中是否有某个自己的属性
  • 和之前的Object.prototype.hasOwnProperty有什么区别
    • 防止对象中重写hasOwnProperty方法
    • 原型为null时没有hasOwnProperty方法

ES13 - new members of classes

 class Person {
            //对象属性:public 公共的属性
            height = 1.99

            // 对象属性:private 私有:约定俗成
            _intro = 'name'

            // ES13 私有属性
            #intro = "name"

            //ES13  私有类属性
            static #male = "70"

            constructor(name, age) {
                // 对象中的属性
                this.name = name
                this.age = age
            }

            // ES13 静态代码块
            // 会自动执行一次,可以进行初始化操作
            static {
                console.log("hello world");
            }
        }

Proxy - Reflect使用详解

监听对象的操作

  • 使用Object.defineProperty
   const keys = Objects.keys(obj)
        for(const key of keys) {
            let value = obj[key]
            Object.defineProperty(obj, key, {
                set: function(newValue) {
                    value = newValue
                },
                get: function() {
                    return value
                }
            })
        }

Proxy基本使用

  • ES6新增,帮助我们创建一个代理
    • 之后对对象的所有操作,都通过代理对象来完成
 const obj = {
            name: "why",
            age: 18,
            height: 1.88
        }

        // 1. 创建一个Proxy对象
        const objProxy = new Proxy(obj, {
            // 编写捕获器
            get: function(target,key) {
                return target[key]
            },
            set: function(target, key, value) {
                target[key] = value
            },
            deleteProperty: function(target, key) {
                console.log("监听删除")
                delete target[key]
            },
            has:function(target, key) {
                console.log("监听判断")
                return key in target
            }
        })

        // 2.对obj的所有操作,去操作objProxy
        console.log(objProxy.name);
        objProxy.name = "lobe"
        console.log(objProxy.name);

Reflect的作用

  • 也是ES6中新增的API,它是一个对象,字面的意思是反射
  • 主要提供了很多操作JS对象的方法,有点像Object中操作对象的方法
    • 比如Relect.getPrototypeOf(target)类似于Object.getPrototypeOf()
  • 为什么还需要Reflect这样的新增对象呢?
    • 早期没有考虑到这种对对象本身的操作如何设计更加规范,所以将这些API放到了Object中
    • 但是Object作为一个构造函数,这些操作实际上并不合适
    • 另外还包含类似于in、delete操作符,让JS看起来很奇怪
    • ES6中新增了Reflect,让我们把这些操作集中到Reflect中
    • 另外在使用Proxy时,可以做到不操作原对象

Reflect和Proxy共同完成代理

 const obj = {
            name: "why",
            age: 18,
            height: 1.88
        }

        // 1. 创建一个Proxy对象
        const objProxy = new Proxy(obj, {
            // 编写捕获器
            get: function(target,key) {
                return target[key]
            },
            set: function(target, key, value, receiver) {
                // 不用直接操作原对象,而且返回布尔值,可以判断是否修改成功
                Reflect.set(target, key, value)

                // receiver是proxy对象
                
            },
            deleteProperty: function(target, key) {
                console.log("监听删除")
                delete target[key]
            },
            has:function(target, key) {
                console.log("监听判断")
                return key in target
            }
        })

        objProxy.name = "lobe"
        console.log(objProxy.name);

Promise使用详解

异步任务的处理

  • ES5之前
  function execCode(counter, successCallback, failureCallback) {
            // 异步任务
            setTimeout(() => {
                if (counter > 0) {
                    // ...
                    successCallback(counter)
                } else {
                    failureCallback("error")
                }
            })
        }
        execCode(100, (value) => {
            console.log(value);
        }, (err) => {
            console.log(err)
        })

promise解决异步问题

  • 在通过new创建Promise对象时,我们需要传入一个回调函数,我们称之为executor
    • 这个回调函数会被立即执行,并且传入另外两个回调函数resolve、reject
    • 当我们调用resolve回调函数时,会执行Promise对象的then方法传入的回调函数
    • 当我们调用reject回调函数时,会执行Promise对象的catch方法传入的回调函数
function execCode(counter) {
            const promise = new Promise((resolve, reject) => {
                // 异步任务
                setTimeout(() => {
                    if (counter > 0) {
                        // ...
                        //对应then
                        resolve(counter)
                    } else {
                        //对应catch
                        reject("error")
                    }
                })
            })
            return promise
        }
        execCode(100).then((value) => {
            console.log(value);
        }).catch((err) => {
            console.log(err)
        })

promise的三种状态

  • pending:待定状态,既没有兑现也没有拒绝
  • fulfilled:成功状态,执行了resolve
  • rejected:失败状态,执行了reject
  • Promise的状态一旦确定,就不能更改,也不能再次执行回调函数

then的返回值

  • then方法本身是有返回值的,它的返回值是一个Promise,所以我们可以进行如下的链式调用:

catch返回值

  • catch方法也是会返回一个Promise对象的,所以catch方法后面我们可以继续调用then方法或者catch方法

finally方法

  • finally是ES9中新增的特性,表示无论Promise对象无论变成fulfilled还是rejected状态,最终都会被执行的代码
  • finally方法是不接受参数的

Promise类方法 - resolve/reject

  • 有时候我们已经有一个现成的内容了,希望将其转成Promise来使用,这个时候我们可以使用Promise.resolve来完成
    • Promise.resolve的用法相当于new Promise, 并且执行resolve操作

all方法

  • Promise.all
    • 它的作用是将多个Promise包裹在一起形成一个新的Promise
    • 当所有的Promise变成fulfilled时,新的Promise变成fulfilled,并且将所有Promise返回值组成一个数组
    • 当有一个Promise变成reject时,新的Promise状态为reject,并且会将第一个reject的返回值作为参数
 	const p1 = new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve("p1")
            }, 3000)
        })
        const p2 = new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve("p2")
            }, 2000)
        })
        const p3 = new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve("p3")
            }, 5000)
        })

        Promise.all([p1, p2, p3]).then(res => {
            console.log(res)
        })

迭代器和生成器

async、await、时间循环

异步函数 async function

  • async关键字用于声明一个异步函数:
    • async:异步、非同步
    • sync:同步、同时
async function foo() {
    console.log('1')
    
    return 123
}

foo().then(res => {
    console.log("res:", res)
})

await关键字

  • 只能在async函数中使用await关键字
  • await关键字
    • 通常在await后面跟上一个表达式,这个表达式会返回一个Promise
    • await会等到Promise的状态变成fulfiled状态,之后继续执行异步函数
async function fetchData() {
  try {
    const response = await fetch('https://example.com/api/data');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Error:', error);
  }
}

fetchData().then(result => {
  console.log('Data:', result);
});

浏览器事件循环机制

进程和线程

操作系统中:

  • 进程:计算机已经运行的程序,是操作系统管理程序的一种方式
    • 打开任务管理器可以查看进程,一个打开的应用程序至少有一个进程
  • 线程:操作系统能够运行运算调度的最小单位,通常情况下它被包含在进程中
    • 每个应用程序都是由代码开发的,而运行这些代码的就是线程,每个进程中至少有一个线程
  • 总结:
    • 进程:启动一个应用程序,就会默认启动一个进程(也可能是多个进程)
    • 线程:每个进程中,都会启动至少一个线程用来执行程序中的代码,这个线程被称之为主线程
    • 所以我们可以说进程是线程的容器
  • 形象解释
    • 操作系统类似于一个大工厂
    • 工厂中有很多车间,这个车间就是进程
    • 每个车间可能有一个以上的工人在工厂,这个工人就是线程
      JavaScript高级_第18张图片

操作系统的工作方式

  • 操作系统是如何做到同时让多个进程同时工作的?
    • 因为CPU的运算速度非常快,可以快速在多个进程之间切换
    • 当进程中的线程获取到时间片时,就可以快速执行我们编写的代码
    • 对于用户来说是感受不到这种快速的切换的

浏览器中的JS线程

  • JS是单线程的,但是JS的线程应该有自己的容器进程:浏览器或者Node

  • 浏览器是一个进程吗?

    • 目前多数的浏览器其实都是多进程的,当我们打开一个tab页面时就会开启一个新的进程,这是为了防止一个页面卡死而导致所有页面无法响应
    • 每个进程中又有很多的线程,其中包括执行JS的线程
  • JS的代码执行是在一个单独的线程中执行的

    • 这就是意味着JS的代码,在同一时刻只能做一件事
    • 如果这件事是非常耗时的,就意味当前的线程会被阻塞
  • 但是耗时的操作,并不会由JS线程来执行

    • 浏览器的每个进程都是多线程的,那么其他线程可以来完成这个耗时的操作
    • 比如网路请求、定时器等,我们只需要在特定的时候执行应该有的回调即可
    • setTimeout的计时操作由浏览器来执行
  • JS线程执行代码碰到setTimeout函数,会将计时操作交给浏览器线程执行,然后继续执行之后的js代码

  • 当js代码执行完毕之后,浏览器的计时操作也结束,会将计时器结束的回调函数的js代码放到一个事件队列中

  • 然后依次取出事件队列中的事件执行

浏览器的事件循环

  • 如果在执行JS代码的过程中,有异步操作
    • 比如说中间我们插入了一个setTimeout的函数调用
    • 这个函数会被放到调用栈中,不会阻塞后续代码的执行

JavaScript高级_第19张图片

单线程-微任务和宏任务

  • 浏览器事件循环中有两个队列:
    • 宏任务队列:ajax、setTimeout、setInterval、DOM监听、UI Rendering
    • 微任务队列:Promise的then回调、Mutation ObServe API、queueMicrotask()等
  • 时间循环对于两个队列的优先级是怎么样的?
    • 主线程的先执行
    • 在执行任何一个宏任务之前,都会先查看微任务队列中是否有任务需要执行
      • 也就是宏任务执行之前,必须保证微任务队列是空的
      • 如果不为空,那么就优先执行微任务队列中的任务

Promise面试题

JavaScript高级_第20张图片

  1. script start
  2. promise1
  3. 2
  4. script end
  5. then1
  6. queueMicrotask1
  7. then3
  8. setTimeout1
  9. then2
  10. then4
  11. setTimeout2

JavaScript高级_第21张图片

  • await之后的代码相当于调用then()之后执行的,所以会加入到微任务中
  1. script start
  2. async1 start
  3. async2
  4. promise1
  5. script end
  6. async1 end
  7. promise2
  8. setTimeout

错误处理方案

  • 开发中我们会封装一些工具函数,封装之后给别人使用:
    • 在其他人使用的过程中,可能会传递一些参数
    • 对于函数来说,需要对这些参数进行验证,否则可能得到的是我们不想要的结果
  • 当我们验证到不是希望得到的参数时,就会直接return
    • 弊端是调用者不知道是因为函数内部没有正常执行,还是执行结果就是一个undefined
    • 正确的做法应该是如果没有通过某些验证,让外界知道函数内部报错了
  • 可以通过throw关键字,抛出一个异常
  • throw语句:
    • throw语句用于抛出一个用户自定义的异常
    • 当遇到throw语句时,当前的函数执行会被停止(throw后面的语句不会执行)

throw关键字

  • throw表达式就是在throw后面可以跟上一个表达式来表示具体的异常信息
  • Error类型:JS已经给我们提供了一个Error类,我们可以直接创建这个类的对象
    • Error包含三个属性
      • message
      • name
      • stack
    • Error的子类
      • RangeError:下标值越界时使用的错误类型
      • SyntaxError:解析语法错误时使用的错误类型
      • TypeError:出现类型错误我时,使用的错误类型

异常的捕获方式

  • 很多情况下出现异常时,我们并不需要程序直接推出,而是希望可以正确的处理异常
    • 可以使用try catch
try {
    foo()
} catch (err) {
    console.log(err)
} finally {
    
}

Storage

认识Storage

  • WebStorage提供了一种机制,可以让浏览器提供一种比cookie更直观的key、value存储方式:
    • localStorage:本地存储,提供的是一种永久性的存储方式,在关闭网页重新打开时,存储的内容依然保留
    • sessionStorage:会话存储,提供的是本次会话的存储,在关闭掉会话时,存储的内容会被清除

storage的基本使用:

 let token = localStorage.getItem("token")
        if (!token) {
            console.log("从服务器获取");
            token = "coderwhy"
            localStorage.setItem("token", token)
        }

        let username = localStorage.getItem("username")
        let password = localStorage.getItem("password")
        if (!username || !password) {
            console.log("用户输入账号密码")
            username = "coderwhy"
            password = "123456"
            localStorage.setItem("username", username)
            localStorage.setItem("password", password)
        }

JavaScript高级_第22张图片

Storage常见的方法和属性

  • 属性:
    • Storage.length:只读属性
      • 返回一个整数,表示存储在Storage对象中的数据项数量
  • 方法:
    • Storage.key(index): 该方法接受一个数值n作为参数,返回存储中的第n个key名称
    • Storage.getItem()
    • Storage.setItem()
    • Storage.removeItem()
    • Storage.clear()

Storage工具封装

class Cache {

    constructor(isLocal = true) {
        this.storage = isLocal ? localStorage: sessionStorage
    }

    setCache(key, value) {
        if(value) {
            this.storage.setItem(key, JSON.stringify(value))
        }
    }

    getCache(key) {
        const result = this.storage.getItem(key)
        if(result) {
            return JSON.parse(result)
        }
    }
    
    removeCache(key) {
        this.storage.removeItem(key)
    }

    clear() {
        this.storage.clear()
    }
}

const localStorage = new  Cache()
const sessionCache = new Cache(false)

防抖

  • 防抖:通过防抖来延迟某一操作,来防止某一操作频繁执行
  • 防抖的应用场景:
    • 输入框频繁的输入内容,搜索或者提交信息
    • 频繁的点击按钮,触发某个事件
    • 监听浏览器滚动事件,完成某些特定操作
    • 用户缩放浏览器的resize事件

  • 手写防抖代码的基本功能实现

        // 手写防抖函数基本功能实现
        function Mydebounce(fn, delay) {
            // 用于记录上一次事件触发的timer
            let timer = null
            // 触发事件时执行的函数
            const _debounce = () => {
                // 取消上一次的事件
                if (timer) clearTimeout(timer)

                timer = setTimeout(() => {
                    fn()
                }, delay)
            }

            return _debounce
        }

  • this和参数绑定
  // 手写防抖函数基本功能实现
        function Mydebounce(fn, delay) {
            // 用于记录上一次事件触发的timer
            let timer = null
            // 触发事件时执行的函数
            const _debounce = function (...args) {
                // 取消上一次的事件
                if (timer) clearTimeout(timer)

                timer = setTimeout(() => {
                    // 这里的this可以绑定输入框input,然后使用value值
                    fn().apply(this, args)
                }, delay)
            }

            return _debounce
        }

节流

  • 在固定的时间内只触发一次,固定频率
  • 应用场景
    • 监听页面的滚动事件
    • 鼠标移动事件
    • 用户频繁点击按钮操作
    • 游戏中的一些设计

  • 节流的基本实现
  	// 手写节流函数的时间差基本功能实现
        function Mythrottle(fn, interval) {
            let startTime = 0

            const _throttle = function() {
                const nowTime = new Date().getTime()
                const waitTime = interval - (nowTime - startTime)
                if(waitTime <= 0) {
                    fn()
                    startTime = nowTime
                }
            }

            return _throttle
        }

	// 利用定时器实现节流
function throttle (fn, delay) {
    let timer = null

    return function (...arg) {
        if(timer) return 
        timer = setTimeout(()=> {
            fn.apply(this, arg)
            timer = null
        },delay)
    }
}

深拷贝

  • 浅拷贝:第一层数据的拷贝,如果第一层数据中有对象,那么会引用同一个对象
// 手写deepClone深拷贝算法
function deepCopy(obj) {
    if (typeof obj !== "object" || obj == null) {
        return  obj
    }

    let res = obj instanceof Array? [] : {}

    // 通过for...in循环对象的所有枚举属性,然后再使用hasOwnProperty()方法来忽略继承属性
    for (let key in obj) {
        if(obj.hasOwnProperty(key)) {
            res[key] = deepCopy(obj[key])
        }
    }

    return res
}
// 判断一个标识符是否是对象类型
        function isObject(value) {

            const valueType = typeof value
            return (value !== null) && ( valueType === "object" || valueType === "function")
        }
        function deepCopy(obj) {
            // 1.如果是原始类型,直接返回
            if (!isObject(obj)) {
                return obj
            }

            // 2.如果是对象类型,才需要创建对象
            const newObj = {}
            for (const key in obj) {
                newObj[key] = deepCopy(obj[key])
            }
            return newObj
        }

        const info = {
            name: "coder",
            age: 18,
            friend:{
                name:"why",
                address: {
                    name: "洛杉矶",
                    detail: "斯坦普斯"
                }
            }
        }
        // 深拷贝
        // 1.JSON方法
        //当拷贝的数据为undefined,function(){}等时拷贝会为空
        const obj4 = JSON.parse(JSON.stringify(info))

        // 2.自己实现
        const obj5 = deepCopy(info)
        obj5.friend.name = "hello world"
        console.log(info.friend.name);
        console.log(obj5.friend.name);

事件总线

 		// 手写实现事件总线
        // 事件总线:我在这里点击触发,在那里执行
        class MyEventBus {
            constructor() {
                this.eventMap = {}
            }

            on(eventName, eventFn) {
                let eventFns = this.eventMap[eventName]
                if (!eventFns) {
                    eventFns = []
                    this.eventMap[eventName] = eventFns
                }
                eventFns.push(eventFn)
            }

            emit(eventName) {
                let eventFns = this.eventMap[eventName]
                if (!eventFns) return
                eventFns.forEach(fn => {
                    fn(...args)
                })
            }

            off(eventName, eventFn) {
                let eventFns = this.eventMap[eventName]
                if (!eventFns) return
                for (let i = 0; i < eventFns.length; i++) {
                    const fn = eventFns[i]
                    if (fn === eventFn) {
                        eventFns.splice(i, 1)
                        break
                    }
                }
            }
        }

JS网络编程

前后端分离的优势

  • 早期的网页都是通过后端渲染来完成的:服务器端渲染(SSR)

    • 客户端发出请求 -> 服务端接受请求并返回HTML文档 -> 页面刷新,客户端加载新的HTML

      文档

  • 服务器端渲染的缺点:

    • 当用户点击页面中的某个按钮向服务器发送请求时,页面本质上只是一些数据发生了变化,而此时服务器却要将重绘的整个页面返回给浏览器加载。
    • 这给网络带宽带来了不必要的开销
  • 在页面数据变动时,只向服务器请求新的数据,并且在阻止页面刷新的情况下,动态的替换页面中展示的数据?

    • 使用AJAX:是一种无页面刷新,获取服务器数据的技术

什么是HTTP?

  • HTTP是一个客户端(用户)和服务端之间请求和响应的标准
    • 网页中的资源通常是被放在Web服务器中的,由浏览器自动发送HTTP请求来获取、解析、展示

HTTP的组成

  • 一次HTTP请求主要包括:请求和响应
  • HTTP的组成部分

JavaScript高级_第23张图片
JavaScript高级_第24张图片

HTTP的请求方式

  • 在RFC中定义了一组请求方式,来表示要对给定资源执行的操作
    • GET:GET方法请求一个指定资源的表示形式,使用GET的请求应该只被用于获取数据
    • HEAD:与GET的区别是响应没有响应体
      • 针对那些比较大的文件,先响应头中获取文件的大小,再决定是否进行下载
    • POST:提交数据
    • PUT:提交数据并且替换目标资源的所有当前表示
    • DELETE:删除数据
    • PATCH:修改部分数据
    • CONNECT:用在代理服务器和另一个服务器的连接,让代理服务器帮我们获取数据
    • TRACE:执行一个消息回环测试

HTTP请求头

JavaScript高级_第25张图片

JavaScript高级_第26张图片

  • accept-encoding:告知服务器,客户端支持的文件压缩格式
  • accept:告知服务器,客户端可接受的文件的格式类型
  • user-agent:客户端相关的信息

HTTP响应状态码

  • Http状态码是用来表示Http响应状态的数字代码
    • 400以上都是错误请求
    • 500以上是服务器的错误

JavaScript高级_第27张图片


JavaScript高级_第28张图片

AJAX发送请求

JavaScript高级_第29张图片

 		// 1.创建XMLHTTPRequest对象
        const xhr = new XMLHttpRequest()
        // 2.监听状态的改变(宏任务)
        xhr.onreadystatechange = function() {
            // console.log(xhr.response);
            //数据如果没有完全下载完成,直接返回
            if (xhr.readyState !== XMLHttpRequest.DONE) return
            // 将字符串转成JSON对象(JS对象)
            const resJSON = JSON.parse(xhr.response)
            console.log(resJSON)
        }

        // 3.配置请求
        // method: 请求的方式(get/post/delete/put/patch...)
        // url:请求的地址
        xhr.open("get", "http://123.207.32.32:8000/home/multidata")
        // 4.发送请求
        xhr.send()

  • XMLHttpRequest的state

JavaScript高级_第30张图片

  • 发送同步请求:
    • 将open的第三个参数设置为false

xhr的其他事件监听

JavaScript高级_第31张图片

响应数据和响应类型

JavaScript高级_第32张图片

HTTP响应的状态status

  • XMLHttpRequest的state是用于记录xhr对象本身的状态变化,并非针对于HTTP的网络请求状态
  • 如果我们希望获取HTTP响应的网络状态,可以通过status和statusText来获取
console.log(xhr.status)
console.log(xhr.statusText)

客户端传递参数的四种形式

  • 方式一:GET请求的query参数
  • 方式二:POST请求x-www-form-urlencoded格式
  • 方式三:POST请求FormData格式
  • 方式四:POST请求JSON格式
	const xhr = new XMLHttpRequest()
        xhr.onload = function () {
            console.log(xhr.response);
        }
        xhr.responseType = "json"
        // 传递方式一:数据暴露在url中
        // xhr.open("get", "http://123.207.32.32:1888/02_param/get?name=why&age=18")

        // 传递方式二:post -> urlencoded  数据放在请求体中
        // xhr.open("post", "http://123.207.32.32:1888/02_param/posturl")
        // xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded")
        // xhr.send("name=why&age=18")

        // 传递方式三:传递form表单中的数据
        // const formData = new FormData(formEl)
        // xhr.send(formData)

        // 传递方式四:json
        xhr.open("post", "http://123.207.32.32:1888/02_param/postjson")
        xhr.setRequestHeader("Content-type", "application/json")
        xhr.send(JSON.stringify({ name: "why", age: 18 }))

ajax网络请求封装

     function Myajax({
            url,
            method = "get",
            timeout = 10000,
            headers = {},
            success,
            error,
            data
        } = {}) {
            const xhr = new XMLHttpRequest()
            xhr.onload = function () {
                if (xhr.status >= 200 && xhr.status < 300) {
                    success && success(xhr.response)
                } else {
                    error && error({ status: xhr.status, message: xhr.statusText })
                }
            }
            xhr.responseType = "json"
            if (method.toUpperCase() === "GET") {
                const queryStrings = []
                for (const key in data) {
                    queryStrings.push(`${key}=${data[key]}`)
                }
                url = url + "?" + queryStrings.join('&')

                xhr.open(method, url)
                xhr.send()
            } else {
                xhr.open(method, url)
                xhr.setRequestHeader("Content-type", "application/json")
                xhr.send(JSON.stringify(data))
            }

        }
        //get请求
        Myajax({
            url: "http://123.207.32.32:8000/home/multidata",
            success: function (res) {
                console.log(res)
            },
            error: function (err) {
                console.log(err)
            }
        })
        // post请求
        Myajax({
            url: "http://123.207.32.32:1888/02_param/postjson",
            method: "post",
            data: {
                name: "jsondata",
                age: 22
            },
            success: function (res) {
                console.log(res)
            },
            error: function (error) {
                console.log(error)
            }
        })

  • 封装加上Promise
 function Myajax({
            url,
            method = "get",
            timeout = 10000,
            headers = {},
            data
        } = {}) {
            return new Promise((resolve, reject) => {
                const xhr = new XMLHttpRequest()
                xhr.onload = function () {
                    if (xhr.status >= 200 && xhr.status < 300) {
                       resolve(xhr.response)
                    } else {
                        reject({ status: xhr.status, message: xhr.statusText })
                    }
                }
                xhr.responseType = "json"
                if (method.toUpperCase() === "GET") {
                    const queryStrings = []
                    for (const key in data) {
                        queryStrings.push(`${key}=${data[key]}`)
                    }
                    url = url + "?" + queryStrings.join('&')

                    xhr.open(method, url)
                    xhr.send()
                } else {
                    xhr.open(method, url)
                    xhr.setRequestHeader("Content-type", "application/json")
                    xhr.send(JSON.stringify(data))
                }

            })
        }
        //get请求
        Myajax({
            url: "http://123.207.32.32:8000/home/multidata",
        }).then(res => {
            console.log(res)
        }).catch(err => {
            console.log(err)
        })

延迟时间timeout和取消请求

  • 过期时间的设置
//监听过期
xhr.ontimeout = function() {
    console.log("请求过期:timeout")
}
//timeout:浏览器达到过期时间还没有获取到对应的结果时,取消本次请求
xhr.timeout = 3000
  • 取消请求
xhr.abort()

Fetch方法和API

  • Fetch可以看作是早期的XMLHttpRequest的替代方案
    • 比如返回值是一个Promise
    • 不像XMLHttpRequest一样,所有的操作都在一个对象上
  • fetch函数的使用

JavaScript高级_第33张图片

  • body:
    • 字符串
    • FormData对象,以multipart/form-data形式发送数据
   // 1.fetch发送get请求
        // 优化方式一:
        fetch("http://123.207.32.32:8000/home/multidata").then(res => {
            const response = res
            return response.json()
        }).then(res => {
            console.log(res)
        }).catch(err => {
            console.log(err)
        })

        // 优化方式二:
        // async function getData() {
        //     const response = await  fetch("http://123.207.32.32:8000/home/multidata")
        //     const res = await response.json()
        //     console.log(res)
        // }
        // getData()

        // post请求并且有参数
        async function getData() {
            const response = await fetch("http://123.207.32.32:8000/home/multidata", {
                method: "post",
                headers: {
                    "Content-Type": "application/x-www-form-urlencoded"
                },
                body: {
                    name: "why",
                    age: 18
                }
            })
            const res = await response.json()
            console.log(res)
        }
        getData()

XMLHttpRequest文件上传

<body>
    <input type="file" class="file">
    <button class="upload">上传文件button>
    <script src="./index.js">script>
    <script>
        const uploadBtn = document.querySelector('.upload')
        uploadBtn.onclick = function () {
            const xhr = new XMLHttpRequest()
            xhr.onload = function () {
                console.log(xhr.response)
            }
            xhr.responseType = "json"
            xhr.open("post", "http://123.207.32.32:1888/02_param/upload")
            // 表单
            const fileEl = document.querySelector(".file")
            const file = fileEl.files[0]
            const formDate = new FormData()
            formDate.append("avatar", file)
            xhr.send(formDate)
        }
    script>
body>

Fetch文件上传

const uploadBtn = document.querySelector('.upload')
        uploadBtn.onclick = async function () {
            // 表单
            const fileEl = document.querySelector(".file")
            const file = fileEl.files[0]
            const formDate = new FormData()
            formDate.append("avatar", file)
            const response = await fetch("http://123.207.32.32:1888/02_param/upload", {
                body: formDate
            })
            const res = await response.json()
            console.log(res)
        }

你可能感兴趣的:(javascript,前端,开发语言)