js笔记

作者: Gaarahan | 来源:发表于2019-04-18 11:35 被阅读0次

    1. 关于循环

    1. for( A ) { B };
      for循环的A和B是两个不同的作用域,B作用域是A作用域的子域,如下面的代码
    for(let i = 0; i < 3;i++){
      let i = "han";
      console.log(i);
    }
    // => han
    //    han
    //    han
    

    在上面的代码中

    • 在B位置,i可以被重新声明,说明A和B是两个不同级的作用域
    • 虽然在B作用中,i被重新赋值,但仍然不影响A作用域中,i作为循环变量来使用
      这说明在B作用域中的操作对A无影响,因此B为A的子作用域

    2. 变量提升(没有块级作用域)的问题

    • 内层变量覆盖外层变量
    var tmp = new Date();
    function f(){
      console.log(tmp);
      if(false){
        var tmp = 'hello world';
      }
    }
    // => undefined
    

    如上代码,本来console.log()输出的应该是全局的变量tmp,即当前的时间,但由于if语句中的tmp变量
    提升,覆盖了全局变量tmp的值,输出为undefined

    • 循环变量泄漏为全局变量
    var s = 'hello';
    for (var i = 0; i < s.length; i++) {
      console.log(s[i]);
    }
    console.log(i); 
    // => 5
    

    上面代码中,变量i本来只属于循环,但由于变量提升,它被声明为了全局变量

    3. 变量声明

    es6 有6种变量声明的方法
    var(es5) function(es5) let const import class
    js 的七种数据类型
    string boolean number null undefined Object Symbol

    4. 一些新增的常用的字符串处理方法

      let s = "Gaarahan";
      //1.  内容查询
      s.startsWith('Gaa'); //true
      s.endsWith('han');   //true
      s.includes('aha');   //true
      
      //第二个参数表示开始搜索的位置
      {
        //这俩搜索的是从0到第二个参数n
        s.startsWith('Gaa',1); //false
        s.includes('aha',s.indexOf('aha') ); //true
        // 这个搜索的是前n个字符
        s.endsWith('han',s.length - 2); //false 
        s.endsWith('han',s.length);     //true
      }
    
      //2. 重复
      'han'.repeat(3);   // "hanhanhan"
      'han'.repeat(3.6);   // "hanhanhan"
    
      //3. 字符串补全
      s.padStart();
      s.padEnd();
    
      'Gaara'.padStart(8,'han')  //"hanGaara"
      'Gaara'.padStart(6,'han')  //"hGaara"
      'Gaara'.padStart(5,'han')  //"Gaara"
      'Gaara'.padStart(7)        //"  Gaara" 无第二参数,默认空格补全
    

    5. 正则的方法

    • RegExp.exec("string")
      返回给定字符串中正则表达式的一个匹配的结果数组,每次匹配会从上次匹配的结果处继续
    let str = "aaa_aa_aaaa";
    let r1 = /a+/g;
    
    r1.exec(str); 
    ... //多次运行的结果为 =>  ["aaa"] , ["aa"], ["aaaa"] , null, ["aaa"] , ["aa"] ...
    

    注意必须将上面的正则表达式创建为一个变量,才能每次都从上次匹配的结果处继续

    let str = "aaa_aa_aaaa";
    
    /a+/g.exec(str); 
    
    // => 多次运行结果为 ["aaa"] ["aaa"] ["aaa"] ["aaa"] ..
    //  始终指向第一处匹配,因为每一次都是用的一个新的正则表达式,其中并不会存储上次的
    //  查询位置
    

    6. 关于数字

    • 在js中,整数和浮点数采用的是相同的存储方式,因此有
    Number.isInteger(25); //true 
    Number.isInteger(25.0); //true
    Number.isInteger(25.1); //false
    
    • JS能够准确表示的数在2^53 ~ -2^53之间
    //超过范围的数字无法准确判断
    Math.pow(2,53) === Math(2,53) + 1; // true
    
    • 通常理解的向上取整ceil()和向下取整floor()都可以理解为在数轴上的上和下,注意负数
    Math.ceil(4.9)    // 5
    Math.floor(4.9)   // 4
    
    Math.ceil(- 4.9)   // -4
    Math.floor(- 4.9)  // -5
    

    7. 尾调用优化与递归

    • 尾调用优化特性是es6提供的一个优化方式,当一个函数的最后一步操作是调用另一个函数,而且
      该调用无需再使用外层函数中的内部变量时,就被称为尾调用
      let x = ()=>{
        let i = 1;
        return g(i); //尾调用
      }
      let y = ()=>{
        let i = 1;
        let j = 2;
        let g = (b) = >{
          return b + j;
        }
        return g(i); 
    //不是尾调用,除了传参之外,还使用了外部函数的内部变量,此时函数y的调用帧不能被删除
      }
    

    提出尾调用的目的是为了优化代码,函数中的嵌套调用的过程中,我们将当前的主函数的状态
    (子函数的调用位置,以及主函数中的内部变量)保留
    ,为子函数以及其参数重新创建一个调用帧
    (call frame),当子函数调用完成后,我们才删除这个调用帧,并将结果带回主函数,继续我们主函
    数的调用帧。
    但是,对于尾调用来说,我们已经不再需要主函数中的调用位置以及各种内部变量了,返回到主函
    数后,我们唯一需要做的就是将子函数的返回值再做一次返回,如下:

      //两个定义好的函数,y尾调用了z
      function y(){
        let i = 3;
        return z(3);
      }
      function z(i){
        return i*2;
      }
    
      //函数x
      function x(){
        ...
        y();
        ...
      }
      // 优化后相当于:
      function x(){
        z(3);
      }
    

    从上面的函数,我们可以看出,尾调用的优化是很有必要的,而优化的过程可以这样简单理解:
    对于第一个x函数,我们调用了函数y,为函数y创建了一个调用帧,而函数y又尾调用了函数z

    我们知道,尾调用的函数z已经不需要再使用函数y中的调用位置了(因为z是最后一步操作,
    它在y中的下一步操作就是返回到函数x中去),同时函数y中的内部变量也已经通过传值的方式
    保存了下来,那么函数y的调用帧已经没有存在的必要了(我们可以直接把函数z返回到主函数x
    中去,而不需要返回到y,再由y返回到x)
    
    此时,我们可以删除函数y的调用帧,用内部函数z的调用帧取代函数y的调用帧,就可以节省
    一部分调用栈(call stack)的空间
    
    • 在简单理解了尾调用优化的原理后,看看尾调用优化的用处:
      我们已经知道,尾调用优化后,我们可以使用一个尾调用函数的调用帧来替代原来的两层调用帧
      那么,当我们有一堆嵌套的尾调用函数时,我们就可以保证始终只存在一个调用帧,例:
      function a(){
        let i = 1;
        return b(i);
      }
      function b(i){
        let j = i + 2;
        return c(j);
      }
      function c(j){
        return j;
      }
    

    如上,函数a,b都使用了尾调用,当我们调用函数a时,为函数a创建了一个调用帧,接着当我们尾调用
    函数b时,我们使用函数b的调用帧替代了函数a的调用帧,此时调用栈中仍然只存在一个调用帧b,而
    当我们继续执行,调用函数c时,我们又使用函数c的调用帧来替代调用栈中唯一的调用帧b

    • 常见的应用:
      我们在单个的函数中可以使用尾调用来减小函数的内存占用,但这些一个个写出来的尾调用函数
      优化的效果并不显著,我们平时使用的最消耗内存的做法便是递归调用,而尾调用优化能大大的
      优化递归调用占用的内存,但我们需要将递归函数改写成尾调用的形式
      //原始的递归求阶乘代码
      function func1(i){
        if(i === 1) return 1;
        return func1(i - 1) * i;
      }
      // 改写为尾调用函数
      function func2(i,temp){
        'use strict'
        if(i === 1){
          return temp;
        }
        temp *= i;
        return func2(i - 1,temp);
      }
      //在浏览器控制台运行
      func1(100000); // 栈溢出
      func2(100000,1); // 栈溢出 ???
    
    
      // 2. 原始的菲波纳切函数
      function fe1(i){
        if (i <= 1)
          return 1;
        return f(i - 1) + f(i - 2);
      }
      function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
        if( n <= 1 ) {return ac2};
        return Fibonacci2 (n - 1, ac2, ac1 + ac2);
      }
    
      Fibonacci2(100) // 573147844013817200000
      Fibonacci2(1000) // 7.0330367711422765e+208
      Fibonacci2(10000) // Infinity
    

    8. call apply bind

    1. 注意,箭头函数的this指向是固定的,始终指向函数定义时的对象,而不是使用时的对象
      无法使用这三个函数来为其指定this的指向
      let a = {x:3};
      let func = ()=>{
        console.log(this); 
        //箭头函数没有自己的this,它的this是该函数外层代码块的this
        // 在此处是window对象
      }
      func(); //=> window
    
      //call()
      func.call(a); // =>window
      //apply
      func.apply(a); // => window
      // bind()
      let func2 = func.bind(a);
      console.log(func2);// => ()=>{console.log(this);}
      func2(); // window
    
    

    1.function.call(thisArg,arg1,arg2, ...)
    每个函数都有自己的this指向,使用call方法可以为我们调用的函数指定其this指向

      function con(x){
        console.log(this.a);
      }
      con(1);  //  => undefined
      
      //使用call函数为其指出this指向
      con.call({a:2},1); // => 2
    
    • 当处于非严格模式时,指定的thisArg为 null undefined时,this会自动指向
      全局对象(浏览器为window,node为global)
      function con(){
        console.log(this); // => Window对象
        return 1;
      }
      let a = con.call(null); 
      console.log(a); // 1
    
    • 我们可以使用call方法来实现继承
      在JAVA中,我们给出一个Product类,声明类Food继承该Product类
    class Product{
      protected String name;
      protected String price;
      public Product(String name,String price){
        this.name = name;
        this.price = price;
      }
    }
    class Food extends Product{
      private String kind;
      public Food(String name,String price,String kind){
        super(name,price);
        this.kind = kind;
      }
    }
    Food apple = new Food("apple","15","friut");
    

    在js中,我们可以借助call这样来写

      function Product(name,price){
        this.name = name;
        this.price = price;
      }
      function Food(name,price,kind){
        Product.call(this,name,price);
        this.kind = kind;
      }
      let apple = new Food('apple','15','friut');
      console.log(apple); // => Food{name:'apple',price:'15',kind:'friut'};
    

    对比上面的写法,我们在js中使用了call来调用函数Product,类似与在java中使用super()
    函数来调用父类的构造方法

    • call的一个比较常见的用法是在处理类数组对象时,我们可以通过call函数,让其调用
      数组的方法,完成我们需要的操作详细讲解
      例如将类数组对象arguments转化为数组:
      let argsArr = Array.prototype.slice.call(arguments);
    

    使用forEach方法来遍历HTMl collection

      [].forEach.call(htmlCol,(val,index,arra=>{
        console.log(val);
      }))
    
    1. function.apply(thisArg,[args])
      apply的用法和call差不多,区别在与apply所调用函数的参数是以数组或者类数组方式提供的
      也就是说,call中能使用的地方,都可以使用apply来替代,有时apply写的还更简单
    • 上面的继承使用apply改写
      function Product(name,price){
        this.name = name;
        this.price = price;
      }
      function Food(name,price,kind){
        Product.apply(this,arguments); 
        //这里直接将Food函数的arguments对象传过去,虽然多传了一个参数kind,但在写法上更简单
        this.kind = kind;
      }
      let apple = new Food('apple','15','friut');
      console.log(apple); // => Food{name:'apple',price:'15',kind:'friut'};
    
    • 将一个数组push到另一个数组中去(使用concat会创建新数组,push不会)
      let a = [1,2,3];
      let b = [4,5,6];
      [].push.apply(a,b);
      console.log(a); // => [1,2,3,4,5,6]
    
    • 使用max函数找出一个数组中的最大值
      let a = [1,2,3,45,6,7];
    
      let res = Math.max.apply(null,a);
      console.log(res); // => 45
    
      //另一种简化写法 : es6
      Math.max(...a); // => 45
    
    1. function.bind(thisArg,arg1,arg2 ...)
      bind函数不是一次原函数的调用,它会返回一个新的函数,具有指定的this指向,并且在
      调用新函数时,在bind时指定的参数会作为新函数的前几项参数值
      let a = {x:3};
      function func(){
        console.log(this.x);
      }
      func(); //=> undefined
    
      let func2 = func.bind(a);
      console.log(func2);// => ()=>{console.log(this.x);}
      func2();  // =>3
    

    关于继承的常用方法

    a instanceof A // 判断a是否为A的实例
    Object.getPrototypeOf(a) === A   // 获取实例a的原型
    Object.getPrototypeOf(Gaara) === Person   // 获取子类Gaara的父类,判断继承
    
    

    相关文章

      网友评论

          本文标题:js笔记

          本文链接:https://www.haomeiwen.com/subject/tbuzwqtx.html