JS-关于内存管理-闭包-this等知识点记录

141 阅读12分钟

全局代码执行过程

代码被解析,v8引擎内部会帮助我们创建一个对象(GlobalObject => go

变量环境和环境记录

  • 早期ECMA版本规范

    • 每一个执行上下文会被关联到一个变量对象VO,在源码中的变量函数声明会被作为属性添加到VO中,函数的参数也会被添加到VO
  • 最新ECMA规范

    • 每一个执行上下文会被关联到一个变量环境VE,在执行代码中的变量函数声明会被作为绑定这个VE的环境记录添加到VO中,函数的参数也会被添加到VO

内存管理

  • 生命周期

    • 分配内存
    • 使用
    • 释放
  • 手动管理:C C++ 早期OC (malloc函数和free函数)

  • 自动管理:Java JavaScript Python Swift Dart...

内存分配

  • 对于基本数据类型的分配会在执行时,直接在栈空间分配
  • 对于复杂数据类型会在堆内存中开辟一块空间,并将这块空间的指针返回值变量引用

垃圾回收

  • 垃圾回收机制 GCGarbage Collection
  • 回收不再使用的对象
  • 语言运行环境内置垃圾回收器 GC (java的JVM,js的引擎...), 大多数GC都是垃圾回收器

常用的GC算法

  • 引用计数

    • 当一个对象被引用时,引用计数+1,当引用计数为0时,这个对象就可以被销毁
    • 弊端:循环引用,互相引用,引用计数无法为0,需要手动置为null
  • 标记清除 (js引擎广泛采用)

    • 设置一个根对象root object),垃圾回收器定期从根对象开始寻找有引用的对象,没有找到的对象即未被引用的对象就被认为是不可用的对象,将会被回收
    • 解决循环引用

高阶函数

  • 函数作为参数或者作为返回值的函数

  • 数组常见高阶函数

    • filter (item,index,arr) => {return boolean}
    • map (item,index,arr) => {return item}
    • forEach (item,index,arr) => {item} 没有返回值
    • find (item,index,arr) => {return boolean}
    • findIndex (item,index,arr) => {return boolean}
    • reduce ((preValue, item) => { return 下一次的preValue}, initialValue)

参数作用域

  • 函数参数默认值时,会形成一个新的作用域,这个作用域用于保存参数的值

  • var x = 0;
    function foo(
      x,
      y = function () {
        x = 3;
        console.log(x); // undefined
      }
    ) {
      console.log(x); // 3
      var x = 2
      y()
      console.log(x); // 2
    }
    
    foo()
    console.log(x); // 0
    

闭包

  • 一个普通的函数,如果可以访问外层作用域自由变量,那这个函数就是一个闭包
  • 闭包函数+可以访问的自由变量
  • 广义上看,JS的函数都是闭包
  • 狭义上看,JS中的函数如果访问外层作用域的变量,那这个函数就是闭包

内存泄漏

  • 引用链中的对象无法释放
  • 手动释放:可以将其设置为null

this

  • 全局作用域
    • 浏览器 this => window
    • node this => { }; 因为执行函数时,默认call到一个空对象
  • this的绑定和定义的位置没有关系
  • this的绑定和调用方式以及调用的位置有关系
  • this是在运行时绑定的

绑定规则

  • 默认绑定 独立函数调用,指向window

  • 隐式绑定 通过某个对象发起的函数调用,谁调用指向谁

  • 显式绑定 call/apply/bind

    • 指定this的绑定对象

    • apply传参是数组

    • bind传参的三种方式

      • // 1.
        var newSum = sum.bind('系统bind', 10, 20, 30, 40)
        newSum()
        // 2.
        var newSum = sum.bind("系统bind");
        newSum(10, 20, 30, 40);
        // 3.
        var newSum = sum.bind("系统bind", 10, 20);
        newSum(30, 40);
        
    • 默认绑定和显式绑定bind冲突,显式绑定优先级大于默认绑定

  • new绑定 this指向创建出来的对象

    • 创建一个全新的对象
    • 这个新对象会被执行prototype连接
    • 这个新对象会绑定到函数调用的this上(this绑定在这个步骤完成)
    • 如果函数没有返回其他对象,表达式会返回这个新对象

系统API中this的指向

  • setTimeout this指向window
  • 监听点击 this指向被绑定元素
  • 数组 forEach/map/filter/find/findIndex
    • this指向window,
    • 如果传第二个参数,this指向该对象,
    • 箭头函数会指向window

绑定规则优先级

  • 默认绑定优先级最低
  • 显式绑定优先级隐式绑定
  • bind显式绑定优先级call/apply显式绑定
  • new绑定优先级隐式绑定
  • new绑定优先级bind显式绑定
  • new关键字不能和apply/call一起使用

特殊绑定

忽略显式绑定

  • apply / call / bind :当传入null/undefined时,自动将this绑定为全局对象

间接函数引用

  • 新代码时加小括号的话,前面的代码要加分号

    var obj = {
        foo: function() {
            console.log(this);
        }
    }
    
    var obb = {};
    
    // obb.bar = obj.foo
    // obb.bar() // obb
    
    (obb.bar = obj.foo)() // this指向window 加上小括号并且将函数赋值被视为独立函数调用
    

箭头函数 ()=> { }

规则

  • 不会绑定thisarguments属性

  • 根据外层作用域来决定this

    var name = 'zzy'
    var foo = () => {
      console.log(this);
    }
    var obj = {
      foo: foo
    }
    foo() // window
    obj.foo() // window
    foo.call('call') // window
    
  • 箭头函数没有显式原型,不能作为构造函数来使用(不能和new一起来使用,会抛出错误)

  • 高阶函数使用时,可以传入箭头函数

    • filter,map,reduce联合使用
    var arr = [112, 312, 31, 41];
    var result = arr.filter((e) => e % 2 === 0)
                     .map((e) => e * 2)
                     .reduce((preValue, e) => preValue + e);
    console.log(result); //848
    

常见简写

  • 只有一个参数时,**( )**可以省略

  • 只有一行代码时,{ }可以省略,会默认返回执行结果

  • 只有一行代码,返回一个对象,应该给对象加上**()**

    var foo = () => ({name: 'zzy', age: 22})
    

常用场景

  • var obj = {
        data1: [],
        data2: [],
        getData: function () {
            // 没有箭头函数之前
            var _this = this; // 将obj赋值给_this
            setTimeout(function () {
                var res = ["a", "b", "c"];
                _this.data1 = res; // 将res保存到obj.data1
            }, 2000);
            // 有了箭头函数之后
            setTimeout(() => {
                var res = ["a", "b", "c"];
                this.data2 = res; // 将res保存到this.data2,向外层作用域寻找this
            }, 2000);
        },
    };
    obj.getData();
    

面试题

  • 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 等价于person.sayName() 
        (b = person.sayName)(); // window 间接函数引用
    }
    sayName();
    
  • 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  preson1只隐式绑定了foo3(),后面执行是独立函数调用
    person1.foo3.call(person2)(); // window  person2只显式绑定到了foo3(),后面执行是独立函数调用
    person1.foo3().call(person2); // person2  后面执行时显式绑定到了person2上
    
    person1.foo4()(); // person1 上层作用域是person1
    person1.foo4.call(person2)(); // person2 将foo4的作用域显式绑定到了person2,所以执行时的上层作用域是person2
    person1.foo4().call(person2); // person1 上层作用域是person1
    
  • 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() // person1 向上层作用域寻找,函数有作用域,所以是person1
    person1.foo2.call(person2) // person1 同上
    
    person1.foo3()() // window 在全局进行调用的
    person1.foo3.call(person2)() // window 在全局进行调用的
    person1.foo3().call(person2) // person2 显式绑定到person2
    
    person1.foo4()() // person1 向上层作用域寻找,是被person1隐式绑定的foo4
    person1.foo4.call(person2)() // person2 向上层作用域寻找,是被person2显式绑定的foo4
    person1.foo4().call(person2) // person1 向上层作用域寻找,是被person1隐式绑定的foo4,call调返回的箭头函数不绑定this,所以是person1
    
  • 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 被person2绑定后调用
    
    person1.obj.foo2()() // obj 上层作用域被obj隐式绑定
    person1.obj.foo2.call(person2)() // person2 上层作用域被person2显式绑定
    person1.obj.foo2().call(person2) // obj 箭头函数不被person2绑定,依然是obj
    

实现apply,call,bind

mycall

  • Function.prototype.mycall = function(thisArg, ...args) {
        // 1.获取需要被执行的函数
        var fn = this
        // 2.对thisArg转成对象类型(防止传入的是非对象类型)
      thisArg = (thisArg !== undefined && thisArg !== null) ? Object(thisArg) : window
        // 3.调用需要被执行的函数
        thisArg.fn = fn
        var result = thisArg.fn(...args)
    
        delete thisArg.fn
        return result
    }
    

myapply

  • Function.prototype.myapply  = function(thisArg, argArray) {
        // 1.获取需要被执行的函数
        var fn = this
        // 2.对thisArg转成对象类型(防止传入的是非对象类型)
      thisArg = (thisArg !== undefined && thisArg !== null) ? Object(thisArg) : window
        // 3.调用需要被执行的函数
        thisArg.fn = fn
        var result
        // if (!argArray) {
        //   // 无参数
        //   result = thisArg.fn()
        // } else {
        //   // 有参数
        //   result = thisArg.fn(...argArray)
        // } // 1.if-else
        // argArray = argArray ? argArray : [] // 2,三元运算符
        argArray = argArray || [] // 3.逻辑或
        result = thisArg.fn(...argArray)
        delete thisArg.fn
    
        return result
    }
    

mybind

  • Function.prototype.mybind = function(thisArg, ...argArray) {
        // 1.获取需要调用的函数
        var fn = this
    
        // 2.绑定this
        thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window
    
        function proxyFn(...args) {
            // 3.将函数放入thisArg中调用
            thisArg.fn = fn
            // 对传入的两个参数进行合并
            var finalArgs = [...argArray, ...args]
            var result = thisArg.fn(...finalArgs)
            delete thisArg.fn
            // 4.返回结果
            return result
        }
    
        return proxyFn
    }
    

arguments

类数组(array-like)

  • 是对象类型(传递给函数的参数

拥有数组的一些特性

arguments.length

  • 获取参数长度

arguments[index]

  • 根据索引获取某一个参数

但没有数组的一些方法forEachmap...

arguments.callee

  • 获取当前arguments所在的函数
  • 禁止使用arguments.callee(),会造成递归

arguments转数组

for循环

  • var arr = [];
    for (let i = 0; i < arguments.length; i++) {
        arr.push(arguments[i]);
    }
    

Array.prototype.slice.call(arguments)

  • var arr = Array.prototype.slice.call(arguments)
    

[ ].slice.call(arguments)

  • var arr = [].slice.call(arguments)
    

ES6语法 Array.from(arguments)

  • var arr = Array.from(arguments)
    

展开运算符 ...[arguments]

  • var arr = [...arguments]
    

箭头函数中的arguments

  • 会向上层作用域中去寻找arguments

  • 上层作用域是全局

    • var foo = () => {
          console.log(arguments);
      }
      foo()
      
    • 浏览器全局中没有arguments

      image.png

    • node全局中有arguments

      image.png

JS函数式编程

JavaScript纯函数

维基百科定义纯函数

  • Pure Function
  • 此函数在相同的输入值时,需产生相同的输出
  • 函数的输出输入值以外的其它隐藏信息状态无关,也和I/O设备产生的外部输出无关
  • 该函数不能有语义上可观察函数副作用:触发事件,使输出设备输出,更改输出值以外物件的内容...

总结纯函数定义

  • 确定的输入,一定会产生确定的输出
  • 函数在执行过程中,不能产生副作用

副作用 side-effect

  • 在执行一个函数时,除了返回函数值外,还对调用函数产生了附加的影响比如:修改了全局变量,修改了参数,改变了外部存储
  • 非纯函数容易产生bug

数组函数对比

  • splice修改原数组,不是纯函数

  • slice未修改原数组,纯函数

    • slice的实现

      Array.prototype.myslice = function(start,end) {
          var arr = this
          start = start || 0
          end = end || arr.length
          var newArr = []
          for (var i = start; i < end; i++) {
              newArr.push(arr[i])
          }
          return newArr
      }
      

纯函数/非纯函数例子

  • // 1.splice-------------------------------------------------------
    var names = ['a', 'b', 'c', 'd']
    var newNames = names.splice(0,2)
    console.log(newNames); // [ 'a', 'b' ]
    console.log(names); // [ 'c', 'd' ] splice修改了原数组,不是纯函数
    
    // 2.slice--------------------------------------------------------
    var chars = ['a', 'b', 'c', 'd']
    var newChars = chars.slice(0,2)
    console.log(newNames); // [ 'a', 'b' ]
    console.log(chars); // [ 'a', 'b', 'c', 'd' ] slice没有修改原数组,是纯函数
    
    // 3.foo是纯函数-------------------------------------------------------
    function foo(num1, num2) {
        return num1 * 2 + num2 * num2
    }
    
    // 4.bar不是纯函数,修改了外界变量---------------------------------------
    var flag = 'aaa'
    function bar() {
        flag = "bbb"
    }
    bar()
    console.log(flag); // bbb
    
    // 5.baz不是纯函数,修改了传入的参数---------------------------------------
    function baz(info) {
        info.age = 33
    }
    var obj = {
        age: 22
    }
    baz(obj)
    console.log(obj.age); // 33
    
    // 6.test是纯函数,确定的输入产生确定的输出--------------------------
    function test(info) {
        return {
            ...info,
            age: 22
        }
    }
    test(obj)
    

纯函数的优势

  • 安心编写,安心使用
  • 只用单纯实现自己的业务逻辑
  • 不需要关心传入的内容是如何获得
  • 不需要关心依赖其他的外部变量是否发生了修改
  • 确定的输入,一定有确定的输出

React的规则

  • 所有React组件都必须像纯函数一样保护他们的props不被更改

JavaScript柯里化

维基百科定义柯里化

  • Currying 柯里化卡瑞化加里化
  • 把接收多个参数的函数,变成接收一个单一参数的函数,并且返回接收余下的参数,而且返回结果新函数
  • 柯里化声称:如果你固定某些参数,你将得到接收余下参数的一个函数

总结柯里化定义

  • 传递给函数一部分参数来调用他,让它返回一个函数去处理剩余的参数,该过程即为柯里化

柯里化过程

  • // 普通函数
    function add(x,y,z) {
        return x + y + z
    }
    var result = add(1,2,3)
    console.log(result); // 6
    
    // 柯里化
    function sum(x) {
        return function(y) {
            return function(z) {
                return x + y + z
            }
        }
    }
    var res = sum(1)(2)(3)
    console.log(res); // 6
    
    // 简化柯里化
    var simplifySum = x => y => z => x + y + z
    console.log(simplifySum(1)(2)(3)); // 6
    

柯里化优势

使函数职责单一

  • 单一职责原则 SRP(Single Responsibility principle)
  • 将每次传入的参数在单一的函数中进行处理,处理完之后在下一个函数中再使用处理后的结果

逻辑的复用

  • 可以将功能一步一步实现,后续需求不会影响之前的逻辑,就可以实现复用

柯里化的实现

  • // 编写柯里化函数
    function myCurring(fn) {
        function curried(...args) {
            // 判断当前已经接收的参数的个数,和函数本身需要接收的参数是否已经一致
            if (args.length >= fn.length) {
                // fn.call(this, ...args)
                return fn.apply(this, args);
            } else {
                // 没有达到需要的参数时,返回一个新的函数,继续接收剩余的参数
                function curried2(...args2) {
                    // 接收到参数后,需要递归去调用curried,来检查参数个数是否满足要求
                    // return curried.apply(this, [...args, ...args2]);
                    return curried.apply(this, args.concat(args2))
                }
                return curried2;
            }
        }
        return curried;
    }
    

组合函数

  • 组合函数(compose)是在JavaScript开发过程中一种对函数使用技巧模式

通用组合函数的实现

  • function myCompose(...fns) {
        var length = fns.length;
        for (var i = 0; i < length; i++) {
            if (typeof fns[i] !== "function") {
                throw new TypeError("Expected functions");
            }
        }
        function compose(...args) {
            var index = 0;
            var result = length ? fns[index].apply(this, args) : args;
            // 先加加,在判断
            while (++index < length) {
                // result = fns[index].apply(this, [result])
                result = fns[index].call(this, result);
            }
            return result;
        }
        js
        return compose;
    }
    

with语句(不推荐使用)

  • 可以形成自己的作用域

    • var message = "hello world";
      var obj = {
          message: "obj message",
      };
      function foo() {
          // 不推荐使用
          with (obj) {
              console.log(message); // obj message 严格模式下没有with语句
          }
      }
      foo();
      
  • 不推荐使用

    • 容易混淆错误
    • 造成兼容性问题

eval函数 (不推荐使用)

  • 可以将传入的字符串当作JS代码执行

    • var jsString = 'var message = "hello world"; console.log(message);'
      eval(jsString) // hello world
      
    • 不推荐使用

      • 可读性差(代码的可读性是高质量代码重要原则)
      • eval是一个字符串,有可能在执行过程中被刻意更改,易被攻击
      • eval的执行必须经过JS解释器,不能被JS引擎优化

严格模式

  • 严格模式Strict Mode

    • 对JS代码具有限制性,使代码隐式脱离懒散模式Sloppy Mode
    • 支持严格模式浏览器在检测到代码中有严格模式时,会更严格检测执行代码
  • 严格模式 对正常的JavaScript语义进行了一些限制

    • 通过抛出错误消除原有的静默silent错误
    • JS引擎执行代码时可以进行更多的优化不需要对一些特殊的语法进行处理)
    • 禁用了在ECMAScript未来版本可能会定义的一些语法

开启严格模式

  • 严格模式支持粒度化迁移
    • 对某个js文件开启
      • "use strict"; 加到文件顶部
    • 对某个函数开启
      • "use strict";加到函数内顶部

严格模式限制

严格语法限制

  • 新手开发者的不规范失误严格模式下会被当作错误,以便可以快速改正
    • 不允许意外的创建全局变量
    • 严格模式会引起静默失败(silent fail,注:不报错也不会生效)的赋值操作抛出异常
    • 不允许删除不可删除属性
    • 不允许函数参数名称相同
    • 不允许0八进制语法
    • 不允许使用with
    • eval不再为上层引用变量
    • this绑定不会默认转为对象
    • 自执行函数默认绑定)的this会指向undefined
    • setTimeout普通函数this依然指向window箭头函数this上层作用域寻找

补充

  • _开头表示私有变量