zoukankan      html  css  js  c++  java
  • JS核心知识梳理

    前言

    本文目标

    从JS的运行设计数据应用四个角度来梳理JS核心的知识点

    主题大纲

    1. JS运行

      • 变量提升
      • 执行上下文
      • 作用域
      • let
      • 作用域链
      • 闭包
      • 事件循环
    2. JS设计

      • 原型
      • 原型链
      • this
      • call
      • apply
      • bind
      • new
      • 继承
    3. JS数据

      • 数据类型
      • 数据的存储(深浅拷贝)
      • 数据类型判断(隐式转换,相等和全等,两个对象相等)
      • 数据的操作(数组遍历,对象遍历)
      • 数据的计算(计算误差)
    4. JS应用

      • 防抖,节流,柯里化

    一. JS运行

    大概分为四个阶段

    1. 词法分析:将js代码中的字符串分割为有意义的代码块,称为词法单元

      • 浏览器刚拿到一个JS文件或者一个script代码段的时候,它会认为里面是一个长长的字符串
      • 这是无法理解的,所以要分割成有意义的代码块,比如: var a = 1
    2. 语法分析:将词法单元流转换成一颗 抽象语法树(AST),并对生成的AST树节点进行处理,

      • 比如使用了ES6语法,用到了let,const,就要转换成var。

    为什么需要抽象语法树呢?

    • 抽象语法树是不依赖于具体的文法,不依赖于语言的细节,方便做很多的操作
    • 另一方面说,现在有许多语言,C,C++,Java,Javascript等等,他们有不同的语言规范
    • 但是转化成抽象语法树后就都是一致的了,方便编译器对其进行进一步的增删改查等操作,
    1. 预解析阶段:

      • 会确定作用域规则
      • 变量和函数提升
    2. 执行阶段:

      • 创建执行上下文,生成执行上下文栈
      • 执行可执行代码,依据事件循环

    1.作用域

    指定了函数和变量的作用范围

    • 分为全局作用域函数作用域
    • JS不像C,JAVA语言一样,没有块级作用域,简单说就是花括号的范围

    2.变量和函数提升

    全局变量和函数声明会提升

    • 函数声明方式有三种,
      • function foo() {}
      • var foo = function () {}
      • var foo = new Function()
      • 可归为两类,直接创建变量赋值
    • 变量赋值函数赋值普通变量的优先级按位置来,变量名相同前者被覆盖
    • 函数直接创建优先级高于变量赋值,同名取前者,与位置无关,也就是说函数直接创建即使再变量声明后面,也是优先级最高

    3. 执行上下文

    有不同的作用域,就有不同的执行环境,我们需要来管理这些上下文的变量

    • 执行环境分为三种,执行上下文对应执行环境
      • 全局执行环境
      • 函数执行环境
      • eval执行环境(性能问题不提)
    1. 全局执行上下文
      • 先找变量声明,
      • 再找函数声明
    2. 函数执行上下文
      • 先找函数形参,和变量声明
      • 把实参赋值给形参
      • 找函数声明
    • 多个函数嵌套,就会有多个执行上下文,这需要执行上下文栈来维护,后进先出
    • 执行上下文里包含着变量环境词法环境
    • 变量环境里就包含着当前环境里可使用的变量
    • 当前环境没有用哪的, 这就说到了作用域链

    4. 作用域链

    • 引用JS高程的定义:作用域链来保证对执行环境有权访问的变量和函数的有序访问
    • 变量的查找顺序不是按执行上下文栈的顺序,而是由词法作用域决定的
    • 词法作用域也就是静态作用域,由函数声明的位置决定,和函数在哪调用无关,也就js这么特殊

    5. 静态作用域和动态作用域

    • 词法作用域是在写代码或者定义时确定的
    • 而动态作用域是在运行时确定的(this也是!
        var a = 2;
        function foo() {
            console.log(a); // 静态2 动态3
        }
        function bar() {
            var a = 3;
            foo();
        }
        bar();
    复制代码

    闭包

    • 由于作用域的限制,我们无法在函数作用域外部访问到函数内部定义的变量,而实际需求需要,这里就用到了闭包
    • 引用JS权威指南定义:闭包是指有权访问另一个函数作用域中的变量的函数

    1. 闭包作用

    • for循环遍历进行事件绑定 输出i值时为for循环的长度+1
    • 这结果显示不是我们想要的, 因为JS没有块级作用域,var定义的i值,没有销毁,存储与全局变量环境中
    • 在事件具体执行的时候取的i值,就是全局变量中经过多次计算后的i值
        for(var i = 0;i < 3;i++){
            document.getElementById(`item${i+1}`).onclick = function() {
                console.log(i);//3,3,3
            }
        }    
    复制代码
    • 闭包特性:外部函数已经执行结束,内部函数引用外部函数的变量依然保存在内存中,变量的集合可称为闭包
    • 在编译过程中,对于内部函数,JS引擎会做一次此法扫描,如果引用了外部函数的变量,堆空间创建换一个Closure的对象,用来存储闭包变量
    • 利用此特性给方法增加一层闭包存储当时的i值, 将事件绑定在新增的匿名函数返回的函数上
    for(var i = 0;i < 3;i++){
        document.getElementById(`item${i+1}`).onclick = make(i);
    }
    function make(e) {
        return function() {
            console.log(e)//0,1,2
    };
    复制代码

    闭包注意

    • 我们在不经意间就写成了闭包,内部函数引用外部函数的变量依然保存在内存中,
    • 该销毁的没有销毁,由于疏忽或错误造成程序未能释放已经不再使用的内存,就造成了内存泄漏
    • 同时注意闭包不会造成内存泄漏,我们错误的使用闭包才是内存泄漏

    事件循环

    • JS代码执行依据 事件循环
    • JS是单线程,通过异步保证执行不被阻塞
    1. 执行机制
      • 简单说就是,一个执行栈,两个任务队列
      • 发现宏任务就放入宏任务队列,发现微任务就放入微任务队列,
      • 执行栈为空时,执行微任务队列所有微任务,再取宏任务队列一个宏任务执行
      • 如此循环
    2. 宏&微任务 macroTask: setTimeout, setInterval, I/O, UI rendering microTask: Promise.then

    二. JS设计

    1. 原型

    1. JS的设计
    • 有new操作符,构造函数,却没有类的概念,而是使用原型来模拟类来实现继承
    1. JS设计心路历程
    • JS在设计之初,给的时间较短,并且定义为简单的网页脚本语言,不用太复杂,且想要模仿Java的理念,(这也是为什么JS叫JavaScript的原因)
    • 因此就借鉴了Java的对象构造函数new操作符理念,而抛弃掉了了复杂的class(类)概念
    1. 继承机制
    • 需要有一种继承的机制,来把所有对象联系起来,就可以使用构造函数
    • 但是构造函数生成实例对象的缺点就是无法共享属性和方法
    1. prototype属性
    • 为解决上面问题,就引入了prototype属性,就是我们常说的原型
    • 为构造函数设置一个prototype属性,实例对象需要共享的方法,都放在此对象上,

    整个核心设计完成后,后面的API也就顺理成章

    原型

    • 每一个js对象在创建的时候就会与之关联另一个对象
    • 这个对象就是原型,每个对象都会从原型继承属性

    proto

    • 每个对象都有一个属性叫proto,该属性指向对象的原型
    • 构造函数的prototype属性等于实例化对象的proto属性
    • 此属性并不是ES5 中的规范属性,只是为了在浏览器中方便获取原型而做的一个语法糖,
    • 我们可以使用Object.getPrototype()方法获取原型

    constructor 原型没有指向实例,因为一个构造函数可以有多个对象实例 但是原型指向构造函数是有的,每个原型都有一个constructor属性指向关联的构造函数

    function Per() {} // 构造函数
    const chi = new Per() // 实例对象
    
    chi.__proto__ ===  Per.prototype // 获取对象的原型 也是就构造函数的prototype属性
    Per.prototype.constructor === Per // constructor属性 获取当前原型关联的构造函数
    
    复制代码

    实例与原型

    • 读取实例属性找不到时,就会查找与对象关联的原型的属性,一直向上查找,
    • 这种实例与原型之间的链条关系,这就形成了原型链
        function Foo() {}
        Foo.prototype.name = 'tom'
        const foo = new Foo()
        foo.name = 'Jerry'
        console.log(foo.name); // Jerry
        delete foo.name
        console.log(foo.name); // tom
    复制代码

    2.原型链

    首先亮出大家熟悉的网图

    原型链关系图

    就是实例与构造函数,原型之间的链条关系

    • 实例的 proto 指向 原型

    • 构造函数的 prototype 属性 指向 原型

    • 原型的 constructor 属性 指向 构造函数

    • 所有构造函数的 proto 指向 Function.prototype

    • Function.prototype proto 指向 Object.prototype

    • Object.prototype proto 指向 null

    函数对象原型(Function.prototype) 是负责造构造函数的机器,包含Object、String、Number、Boolean、Array,Function。 再由构造函数去制造具体的实例对象

    function Foo() {}
    // 1. 所有构造函数的 __proto__ 指向 Function.prototype
    Foo.__proto__ // ƒ () { [native code] }
    Function.__proto__ // ƒ () { [native code] }
    Object.__proto__ // ƒ () { [native code] }
    
    // 2. 所有构造函数原型和new Object创造出的实例  __proto__ 指向 Object.prototype
    var o = new Object()
    o.__proto__ // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ ...}
    Function.prototype.__proto__  // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ ...}
    
    // 3. Object.prototype 指向null
    Object.prototype.__proto__ // null
    
    复制代码

    2. this

    1. 作为对象方法调用时,指向该对象 obj.b(); // 指向obj`
    2. 作为函数方法调用, var b = obj.b; b(); // 指向全局window(函数方法其实就是window对象的方法,所以与上同理,谁调用就指向谁)
    3. 作为构造函数调用 var b = new Per(); // this指向当前实例对象
    4. 作为call与apply调用 obj.b.apply(object, []); // this指向当前指定的值

    谁调用就指向谁

    
    const obj = {a: 1, f: function() {console.log(this, this.a)}}
    obj.f(); // 1
    
    const a = 2;
    const win = obj.f;
    win(); // 2
    
    function Person() {
        this.a = 3;
        this.f = obj.f;
    }
    const per = new Person()
    per.f() // 3
    
    const app = { a: 4 }
    obj.f.apply(app); // 4
    复制代码

    this 指向是动态作用域,谁调用指向谁,

    3. call

    1. 定义:使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法
    2. 举例:
    var obj = {
        value: 1,
    }
    function foo (name, old) {
        return {
            value:this.value, 
            name, 
            old
        }
    } 
    foo.call(obj, 'tom', 12); // {value: 1, name: "tom", old: 12}
    复制代码
    1. 要求:
    • call改变了this的指向,
    • 执行了foo函数
    • 支持传参,参数个数不固定
    • this参数可以传null,传null时 指向window
    • 函数可以有返回值
    • 非函数调用判断处理
    1. 实现及思路
    Function.prototype.call1 = function (context, ...args) {
        if(typeof this !== 'function') {throw new Error('Error')} // 非函数调用判断处理
        context = context || window; // 非运算符判定传入参数 null则指向window
        const key = Symbol(); // 利用symbol创建唯一的属性名,防止覆盖原有属性
        context[key] = this; // 把函数作为对象的属性,函数的this就指向了对象
        const result = context[key](...args) // 临时变量赋值为执行对象方法
        delete context[key]; // 删除方法,防止污染对象
        return result // return 临时变量
    }
    复制代码
    1. 应用场景 改变this指向,大部分是为了借用方法或属性
      1. 判断数据类型,借用ObjecttoString方法 Object.prorotype.toString.call()
      1. 子类继承父类属性,function Chi() {Par.call(this)}

    4. apply

    1. 定义:使用一个指定的 this 值和一个数组(数组包含若干个指定的参数值)的前提下调用某个函数或方法
    2. 举例:
    var obj = {
        value: 1,
    }
    function foo (name, old) {
        return {
            value:this.value, 
            name, 
            old
        }
    } 
    foo.apply(obj, ['tom', 12], 24); // {value: 1, name: "tom", old: 12}
    复制代码
    1. 要求:
    • call改变了this的指向,
    • 执行了foo函数
    • 支持传参,第二个参数为数组,之后的参数无效,数组内参数个数不固定,
    • this参数可以传null,传null时 指向window
    • 函数可以有返回值
    • 非函数调用判断处理
    1. 实现及思路
    Function.prototype.apply1 = function (context, args) { // 与call实现的唯一区别,此处不用解构
        if(typeof this !== 'function') {throw new Error('Error')} // 非函数调用判断处理
        context = context || window; // 非运算符判定传入参数 null则指向window
        const key = Symbol(); // 利用symbol创建唯一的属性名,防止覆盖原有属性
        context[key] = this; // 把函数作为对象的属性,函数的this就指向了对象
        const result = context[key](...args) // 临时变量赋值为执行对象方法
        delete context[key]; // 删除方法,防止污染对象
        return result // return 临时变量
    }
    复制代码
    1. 应用场景
    • 同 call

    5. bind

    1. 定义
      • bind方法创建一个新函数,
      • 新函数被调用时,第一个参数为运行时的this,
      • 之后的参数会在传递实参前传入作为它的参数
    2. 举例
    const obj = {
        value: 1
    };
    function foo(name, old) {
        return {
            value: this.value,
            name,
            old
        }
    }
    const bindFoo = foo.bind(obj, 'tom'); 
    bindFoo(12); // {value: 1, name: "tom", old: 12}
    复制代码
    1. 要求
      • 返回一个函数,第一个参数作为执行时this
      • 可以在bind时传一部分参,执行函数时再传一部分参
      • bind函数作为构造函数,this失效,但参数有效,
      • 并且作为构造函数时,原型应指向绑定函数的原型,以便实例来继承原型中的值
      • 非函数调用bind判断处理
    2. 实现及思路
    Function.prototype.bind1 = function (context, ...args) {
        if(typeof this !== 'function') {throw new Error('Error')} // 非函数调用判断处理
        const self = this; // 保存当前执行环境的this
        const Foo = function() {} // 保存原函数原型
        const res = function (...args2) { // 创建一个函数并返回
            return self.call( // 函数内返回
                this instanceof res ? this : context, // 作为构造函数时,this指向实例,:作为普通函数this正常指向传入的context
                 ...args, ...args2) // 两次传入的参数都作为参数返回
        }
        Foo.prototype = this.prototype; // 利用空函数中转,保证在新函数原型改变时bind函数原型不被污染
        res.prorotype = new Foo();
        return res;
    }
    复制代码

    6. new

    看看new出的实例能做什么

    • 可访问构造函数的属性
    • 访问prototype的属性
    • 构造函数如有返回值,返回对象,实例则只能访问返回的对象中的属性,this无效
    • 返回基本类型值,正常处理,this有效
    function Persion(name, age) {
        this.name = name;
        this.age = age;
    }
    Person.prototype.sayName = function () {
        console.log(this.name)
    }
    var per = new Person('tom', 10)
    per.name // 10 可访问构造函数的属性
    per.sayName // tom 访问prototype的属性
    复制代码

    new实现

    var person = factory(Foo)
    
    function factory() { // new是关键字,无法覆盖,函数替代
        var obj = {}; // 新建对象obj
        var con = [].shift.call(arguments); // 取出构造函数
        obj._proto_ = con.prototype; // obj原型指向构造函数原型
        var res = con.apply(obj, arguments); // 构造函数this指向obj
        return typeof(res) === 'object' ? ret : obj;
    }
    复制代码

    7. 继承

    1. 原型链继承和原型式继承
    • 无法向父类传参数,只能共用属性
    1. 借用构造函数和寄生继承
    • 方法不在原型上,每次都要重新创建
    1. 组合继承
    • 虽然解决了以上俩个问题,但是调用了两次父亲,
    • 实例和原型上会用相同属性
    1. 寄生组合继承
    • 目前最优方案

    寄生组合继承实现

    function P (name) { this.name = name; } // 父类上绑定属性动态传参
    P.prototype.sayName = function() { // 父类原型上绑定方法
        console.log(111)
    }
    function F(name) { // 子类函数里,父类函数利用`call`函数this指向子类,传参并执行
        P.call(this, name)
    }
    const p = Object.create(P.prototype) // Object.create 不会继承构造函数多余的属性和方法
    p.constructor = F; // constructor属性丢失,重新指向
    F.prototype = p; // 子类原型 指向 中转对象
    
    const c = new F('tom'); // 子类实例 化
    c.name // tom
    c.sayName() // 111 
    复制代码

    三. JS数据

    1.数据类型

    JS分为基本类型和引用类型

    • 基本类型:Boolean Undefined String Number Null Symbol
    • 引用类型:Object Funciton Array 等

    2.数据存储 (深浅拷贝)

    js数据类型中分为基本类型,和引用类型

    1. 基本类型 保存在栈内存中
    • 赋值时 编译系统重新创建一块内存来存储新的变量 所以基本类型变量赋值后就断绝了关系
    1. 引用类型 保存在堆内存中
    • 赋值时 只是对对象地址的拷贝 没有开辟新的内存,堆内存地址的拷贝,两者指向了同一地址
    • 修改其中一个,另外一个就会收到影响

    两个对象 指向了同一地址 修改其中一个就会影响另一个

    特殊的数组对象方法(深拷贝一层)

    1. obj2 = Object.assign({}, obj1)

    2. arr2 = [].concat(arr1)

    3. arr2 = arr1.slice(0)

    4. arr2 = Array.form(arr1)

    5. arr2 = [...arr1];

    以上方法都只能深拷贝一层

    JSON.parse(JSON.stringify(obj))(多层) 不拷贝一个对象,而是拷贝一个字符串,会开辟一个新的内存地址,切断了引用对象的指针联系

    缺点

    1. 时间对象 => 字符串的形式
    2. RegExp、Error => 只得到空对象
    3. function,undefined => 丢失
    4. NaN、Infinity和-Infinity => 序列化成null
    5. 对象是由构造函数生成 => 会丢弃对象的 constructor
    6. 存在循环引用的情况也无法实现深拷贝

    手动实现深拷贝(多层)

        function Judgetype(e) {
            return Object.prototype.toString.call(e).slice(8, -1).toLowerCase();
        }
        function Loop(param) {
            let target = null;
            if(Judgetype(param) === 'array') {
                target = [];
                for(let key of param.keys()){
                    target[key] = Deep(param[key]);
                }
            } else {
                target = {};
                Object.keys(obj).forEach((val) => {
                    target[key] = Deep(param[key]);
                })
            }
            return target;
        }
        function Deep(param) {
            //基本数据类型
            if(param === null || (typeof param !== 'object' && typeof param !== 'function')) {
                return param;
            }
            //函数
            if(typeof param === 'function') {   
                return new Function('return ' + param.toString())();
            }
            return Loop(param);
        }
    复制代码

    3.数据类型判断(类型判断,相等和全等,隐式转换,两个对象相等)

    1. 类型判断 typeof
    • 无法区分 object, null 和 array
    • 对于基本类型,除 null 以外,均可以返回正确的结果。
    • 对于引用类型,除 function 以外,一律返回 object 类型
    typeof(1) // number
    typeof('tom') // string
    typeof(undefined) // undefined
    typeof(null) // object
    typeof(true) // boolean
    typeof(Symbol(1)) // symbol
    
    typeof({a: 1}) // object
    typeof(function () {}) // function
    typeof([]) // object
    复制代码

    instanceof

    • 判断一个实例是否属于某种类型
    [] instanceof Array; // true
    {} instanceof Object;// true
    
    var a  = function (){}
    a instanceof Function // true
    
    
    复制代码

    Object.prototype.toString.call

    • 目前最优方案

    • toString() 是 Object 的原型方法,调用该方法,默认返回当前对象的 [[Class]] 。这是一个内部属性,其格式为 [object Xxx] ,其中 Xxx 就是对象的类型。

    • 对于 Object 对象,直接调用 toString() 就能返回 [object Object] 。而对于其他对象,则需要通过 call / apply 来调用才能返回正确的类型信息

    • 这里就是call的应用场景,巧妙利用call借用Object的方法实现类型的判断

    function T(e) {
        return Object.prototype.toString.call(e).slice(8, -1).toLowerCase();
    }
    T(1) // number
    T('a') // string
    T(null) // null
    T(undefined) // undefined
    T(true) // boolean
    T(Symbol(1)) // symbol
    
    T({a: 1}) // object
    T(function() {}) // function
    T([]) // array
    T(new Date()) // date
    T(/at/) // RegExp
    复制代码
    1. 相等和全等
    • == 非严格比较 允许 类型转换
    • === 严格比较 不允许 类型转换

    引用数据类型栈中存放地址,堆中存放内容,即使内容相同 ,但地址不同,所以两者还是不等的

    const a = {c: 1}
    const b = {c: 1}
    a === b // false
    复制代码
    1. 隐式转换
    • == 可能会有类型转换,不仅在相等比较上,在做运算的时候也会产生类型转换,这就是我们说的隐式转换

    布尔比较,先转数字

    true == 2 // false
    ||
    1 == 2
    // if(X)
    var X = 10
    if(X) // true
    10 ==> true
    
    // if(X == true)
    if(X == true) // false
    10 == true
    || 
    10 == 1
    复制代码

    数字和字符串做比较,字符串数字

    0 == '' // true
    ||
    0 == 0
    
    1 == '1' // true
    ||
    1 == 1
    
    
    复制代码

    对象类型和原始类型的相等比较

    
    [2] == 2 // true
    || valueOf() // 调用valueOf() 取自身值
    [2] == 2
    || toString() // 调用toString() 转字符串
    "2" == 2
    || Number() // // 数字和字符串做比较,`字符串`转`数字`
    2 == 2
    
    复制代码

    小结 js使用某些操作符会导致类型变换, 常见是+,==

    1. 运算时
    • 加法 存在一个字符串都转字符串
    • 乘 - 除 - 减法 字符串都转数字
    1. 相等比较时
    • 布尔比较,先转数字
    • 数字和字符串做比较,字符串数字
    • 对象类型比较先转原始类型

    课外小题 实现 a == 1 && a == 2 && a == 3

    const a = {
      i: 1,
      toString: function () {
        return a.i++;
      }
    }
    a == 1 && a == 2 && a == 3
    复制代码
    1. 判断两个对象值相等
    • 引用数据类型栈中存放地址,堆中存放内容,即使内容相同 ,但地址不同,所以===判断时两者还是不等的
    • 但引用数据内容相同时我们如何判断他们相等呢?
    // 判断两者非对象返回
    // 判断长度是否一致
    // 判断key值是否相同
    // 判断相应的key值里的对应的值是否相同
    
    这里仅仅考虑 对象的值为object,array,number,undefined,null,string,boolean
    
    关于一些特殊类型 `function date RegExp` 暂不考虑
    function Judgetype(e) {
        return Object.prototype.toString.call(e).slice(8, -1).toLowerCase();
    }
    function Diff(s1, s2) {
        const j1 = Judgetype(s1);
        const j2 = Judgetype(s2);
        if(j1 !== j2){
            return false;
        }
        if(j1 === 'object') {
            if(Object.keys(s1).length !== Object.keys(s2).length){
                return false;
            }
            s1[Symbol.iterator] = function* (){
                let keys = Object.keys( this )
                for(let i = 0, l = keys.length; i < l; i++){
                    yield {
                        key: keys[i],
                        value: this[keys[i]]
                    };
                }
            }
            for(let {key, value} of s1){
                if(!Diff(s1[key], s2[key])) {
                    return false
                }
            }
            return true
        } else if(j1 === 'array') {
            if(s1.length !== s2.length) {
                return false
            }
            for(let key of s1.keys()){
                if(!Diff(s1[key], s2[key])) {
                    return false
                }
            }
            return true
        } else return s1 === s2
    }
    
    Diff( {a: 1, b: 2}, {a: 1, b: 3}) // false
    Diff( {a: 1, b: [1,2]}, {a: 1, b: [1,3]}) // false
    复制代码

    其实对象遍历 return 也可以用for in, 关于遍历原型的副作用可以用hasOwnproperty判断去弥补

    • 对象for of 遍历还得自己加迭代器,比较麻烦
        for(var key in s1) {
            if(!s1.hasOwnProperty(key)) {
                if(!Diff(s1[key], s2[key])) {
                    return false
                }
            }
        }
    复制代码

    4.数据操作(数组遍历,对象遍历)

    1.数组遍历

    `最普通 for循环` // 较为麻烦
    for(let i = 0,len = arr.length; i < len; i++) {
        console.log(i, arr[i]);
    }
    
    `forEach` 无法 break return
    
    `for in` 不适合遍历数组,
    
    `for...in` 语句在w3c定义用于遍历数组或者对象的属性
    1. index索引为字符串型数字,不能直接进行几何运算
    2. 遍历顺序有可能不是按照实际数组的内部顺序
    3. 使用for in会遍历数组所有的可枚举属性,包括原型
    
    `for of` 无法获取下标
    
    // for of 兼容1
    for(let [index,elem] of new Map( arr.map( ( item, i ) => [ i, item ] ) )){
      console.log(index);
      console.log(elem);
    }
    
    // for of 兼容2
    let arr = [1,2,3,4,5];
    for(let key of arr.keys()){
        console.log(key, arr[key]);
    }
    for(let val of arr.values()){
        console.log(val);
    }
    for(let [key, val] of arr.entries()){
        console.log(key, val);
    }
    
    2. 
    复制代码

    2.对象遍历

    1. for in

    缺点:会遍历出对象的所有可枚举的属性, 比如prototype上的

    var obj = {a:1, b: 2}
    obj.__proto__.c = 3;
    Object.prototype.d = 4
    for(let val in obj) {
        console.log(val) // a,b,c,d
    }
    // 优化
    for(let val in obj) {
        if(obj.hasOwnProperty(val)) { // 判断属性是存在于当前对象实例本身,而非原型上
            console.log(val) // a,b
        }   
    }
    复制代码
    1. object.keys
    var obj = { a:1, b: 2 }
    Object.keys(obj).forEach((val) => {
        console.log(val, obj[val]); 
        // a 1
        // b 2
    })
    复制代码
    1. for of
    • 只有提供了 Iterator 接口的数据类型才可以使用 for-of
    • Array 等类型是默认提供了的
    • 我们可以给 对象 加一个 Symbol.iterator 属性
    var obj = { a:1, b: 2 }
    obj[Symbol.iterator] = function* (){
        let keys = Object.keys( this )
        for(let i = 0, l = keys.length; i < l; i++){
            yield {
                key: keys[i],
                value: this[keys[i]]
            };
        }
    }
    
    for(let {key, value} of obj){
        console.log( key, value );
        // a 1
        // b 2
    }
    复制代码

    5.数据计算(计算误差)

    1. 0.1 + 0.2 = 0.30000000000000004
    • 所有的数都会转换成二进制,逐位去计算,
    • 小数 二进制不能二等分的 会无限循环
    • js数据存储 64 位双精度浮点数,这里不做赘述,超出会被截取(大数计算误差与限制也是因为这个)
    • 相加后再转换回来就会出现误差
    1. 那么如何做出精确的计算呢
    • 对于数字十进制本身不超过js存储位数的小数,可以同时变为整数,计算后再化为小数
    function getLen(n) {
        const str = n + '';
        const s1 = str.indexOf('.')
        if(s1) {
            return str.length - s1 - 1
        } else {
            return 0
        }  
    }
    function add(n1, n2) {
        const s1 = getLen(n1)
        const s2 = getLen(n2)
        const max = Math.max(s1, s2)
        return (n1 * Math.pow(10, max) + n2 * Math.pow(10, max)) / Math.pow(10, max)
    }
    add(11.2, 2.11) // 13.31
    复制代码
    • 对于超出存储位数的可以,转换成数组,倒序逐位相加,大于10进位,字符串拼接得到值
    function add(a, b) {
        let i = a.length - 1;
        let j = b.length - 1;
        let carry = 0;
        let ret = '';
        while(i>=0|| j>=0) {
            let x = 0;
            let y = 0;
            let sum;
            if(i >= 0) {
                x = a[i] - '0';
                i--
            }
            if(j >=0) {
                y = b[j] - '0';
                j--;
            }
            sum = x + y + carry;
            if(sum >= 10) {
                carry = 1;
                sum -= 10;
            } else {
                carry = 0
            }
            ret = sum + ret;
        }
        if(carry) {
            ret = carry + ret;
        }
        return ret;
    }
    add('999999999999999999999999999999999999999999999999999999999999999', '1') 
    //  1000000000000000000000000000000000000000000000000000000000000000
    复制代码

    四. JS应用

    1.防抖

    场景:

    • 搜索框输入下拉联想,请求后台接口,为了避免频繁请求,给服务器造成压力 定义:
    • 在事件触发n秒后执行,在一个事件触发n秒内又触发了该事件,就以新的事件为准

    实现思想:

    1. 定时器的执行与清除
    2. apply 改变this指向
    3. apply传参继承
    function debounce(func, wait) {
        var timeout;
        return function () {
            var context = this;
            var args = arguments;
            clearTimeout(timeout)
            timeout = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
    }
    复制代码

    2.节流

    场景:

    • 可以将一些事件降低触发频率。
    • 比如懒加载时要监听计算滚动条的位置,但不必每次滑动都触发,可以降低计算的频率,而不必去浪费资源;

    定义:

    • 持续触发事件,规定时间内,只执行一次
    1. 方法一:时间戳
    • 实现思想:
      • 触发时间取当前时间戳now, 减去flag时间戳(初始值为0)
      • 如果大于规定时间,则执行,且flag更新为当前时间,
      • 如果小于规定时间,则不执行
    function foo(func, wait) {
        var context, args;
        var flag = 0;
        return function () {
            var now = +new Date();
            context = this;
            args = arguments;
            if(now - flag > 0) {
                func.apply(context, args);
                flag = now; 
            }
        }
    }
    复制代码
    1. 方法二:定时器
    • 实现思想:
      • 判断当前是否有定时器,
      • 没有 就定义定时器,到规定时间执行,且清空定时器
      • 有则不执行
    function foo(func,   wait) {
        var context, args;
        var timeout;
        return function() {
            if(!timeout){
                setTimeout(()=>{
                    timeout = null;
                    func.apply(context, args);
                }, wait) 
            }
        }
    }
    复制代码

    3.柯里化

    1. 定义:将能够接收多个参数的函数转化为接收单一参数的函数,并且返回接收余下参数且返回结果的新函数
    2. 特点:参数复用,提前返回,延迟执行
    3. 实现过程
    • 创建一个函数,利用 apply,给柯里化函数重新传入合并后的参数
    • 利用reduce迭代数组所有项,构建一个最终返回值
    function add(...args) {
        var fn = function(...args1) {
            return add.apply(this, [...args, ...args1]);
        }
        fn.toString = function() {
            return args.reduce(function(a, b) {
                return a + b;
            })
        }
        return fn;
    }
    add(1)(2)(3).toString(); // 6
    

     

  • 相关阅读:
    mysql 常用方法
    jquery input click取消默认事件
    Sprint Boot 集成mongodb
    MSSQL复习笔记
    H5(3)基础知识点补充
    H5(2) 前端基础知识点补充
    H5(1) h5的新特性
    用队列的方式实现日志
    Spring.Net的快速应用
    总结2016/展望2017
  • 原文地址:https://www.cnblogs.com/onesea/p/13615078.html
Copyright © 2011-2022 走看看