欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JavaScript高级

程序员文章站 2022-05-08 16:02:02
...
  • 基本包装类型: 为了便于操作基本类型值, ECMAScript 还提供了 3 个特殊的引用类型:BooleanNumberString
    • 引用类型与基本包装类型的主要区别就是对象的生存期。使用 new 操作符创建的引用类型的实例,在执行流离开当前作用域之前都一直保存在内存当中。而自动创建的基本包装类型的对象,则只存在于一行代码的执行瞬间,然后立即销毁。这意味着我们不能在运行时给基本类型值添加属性和方法

       var s1 = 'some text';
       s1.color = 'red';
       s1.color // undefined
      
       // 但是这样是可以的
      
       var s1 = new String('some text');
       s1.color = 'red';
       s1.color // red
      
    • 不建议显式的创建基本包装类型的对象
      因为在很多时候会造成一些让人很迷惑的东西

         var b = new Boolean(false)
         var c = b && true
         c // true
      

      b 这个变量就是 Boolean 对象,所以无论在什么情况下,它都是 true

    • 这里的结果为 false 的原因就是 str 本质上是一个原始值,并不存在 prototype 属性;而Str是一个包装类型,存在prototype属性,为true。

         var s1 = new String('some text');
         console.log(s1);	//String { "some text" }
         console.log(typeof(s1));	//object
         var b = new Boolean(false);
         var c = b && true;
         console.log(b);	//Boolean { false }
         console.log(typeof(b));	//object 
         
         var str = 'hello'
         console.log(str);	//hello 
         console.log(typeof str);	//string 
         console.log(str instanceof String);	//false
      
         var Str = new String("hi");	
         console.log(Str);	//String { "hi" }
         console.log(typeof(Str));	//object 
         console.log(Str instanceof String);	//true
      

具体请参考:https://segmentfault.com/a/1190000018764693

  • 重写对象原型

    function F() {};
    var f = new F;
    f.constructor == F; // true
    
    F.prototype = {a: 1};
    var f = new F;
    console.log(f.constructor == F); // false 
    F.prototype = {a:1,constructor:F};
    var f1 = new F;
    console.log(f1.constructor == F);//	true
    

    在构造函数 F.prototype 没有被重写之前,构造函数 F 就是新创建的对象 f 的数据类型。当 F.prototype 被重写之后,原有的 constructor 引用丢失, 默认为 Object

    因此,为了规范开发,在重写对象原型时一般都需要重新给 constructor 赋值,以保证对象实例的类型不被篡改

  • 原型链与继承

    • __proto__prototypeconstructor
      JavaScript高级

      • __proto__constructor属性是对象所独有的;prototype属性是函数所独有的,因为函数也是一种对象,所以函数也拥有__proto__constructor属性。
      • __proto__属性的作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__属性所指向的那个对象(父对象)里找,一直找,直到__proto__属性的终点null,然后返回undefined,再往上找就相当于在null上取值,会报错。通过__proto__属性将对象连接起来的这条链路即我们所谓的原型链。
      • prototype属性的作用就是让该函数所实例化的对象们都可以找到公用的属性和方法,即f1.__proto__ === Foo.prototype
      • constructor属性的含义就是指向该对象的构造函数,所有函数(此时看成对象了)最终的构造函数都指向Function

    具体请参考:proto、prototype和constructor详解

    • 原型链
      JavaScript中所有的对象都是由它的原型对象继承而来。而原型对象自身也是一个对象,它也有自己的原型对象,这样层层上溯,就形成了一个类似链表的结构,这就是原型链
      所有原型链的终点都是Object函数的prototype属性。Object.prototype指向的原型对象同样拥有原型,不过它的原型是null,而null则没有原型
    • 确定原型和实例的关系
    1. 使用 instanceof 操作符, 只要用这个操作符来测试实例(instance)与原型链中出现过的构造函数,结果就会返回true
    2. 使用 isPrototypeOf() 方法, 同样只要是原型链中出现过的原型,isPrototypeOf() 方法就会返回true
    • 原型链存在的问题
    1. 当原型链中包含引用类型值的原型时,该引用类型值会被所有实例共享;

    2. 在创建子类型(例如创建Son的实例)时,不能向超类型(例如Father)的构造函数中传递参数.

    • 继承

      1. 原型链继承
        引用类型的属性被所有实例共享
        在创建 Child 的实例时,不能向Parent传参
      2. 借用构造函数(经典继承)
        优点:避免了引用类型的属性被所有实例共享
           可以在 Child 中向 Parent 传参
        缺点:方法都在构造函数中定义,每次创建实例都会创建一遍方法。
      3. 组合继承
        优点:融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。
      4. 原型式继承
        缺点:包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样。
      5. 寄生式继承
        缺点:跟借用构造函数模式一样,每次创建对象都会创建一遍方法。
      6. 寄生组合式继承
        这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

    具体请参考原型链与继承深入继承的多种优点和缺点

  • bind

    • bind()方法会创建一个新函数。当这个新函数被调用时,bind()的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。(来自于 MDN )

    • 由此我们可以首先得出bind 函数的两个特点:

      1. 返回一个函数
      2. 可以传入参数
    • 模拟bind实现:

      Function.prototype.bind2 = function (context) {
      
      var self = this;
      var args = Array.prototype.slice.call(arguments, 1);
      
      var fNOP = function () {};
      
      var fBound = function () {
          var bindArgs = Array.prototype.slice.call(arguments);
          return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
      }
      
      fNOP.prototype = this.prototype;
      fBound.prototype = new fNOP();
      return fBound;
      }
      
    • 多次bind绑定只有第一次有效

      <script>
      "use strict";
      var a=function(){
          console.log("a")
          if(this){
              this()
          }
      }
      
      var b=function(){
          console.log("b")
          if(this){
              this()
          }
      }
      
      var c=function(){
          console.log("c")
          if(this){
              this()
          }
      }
      
      var d=a.bind(b)
      var e=d.bind(c)
      d()
      e()
      
      
      </script>
      
      结果:abab
      
    • strict严格模式下function里的this如果不设置指向为undefined,非严格模式下指向window

      "use strict";
      var a=function(){
          console.log("a")
          if(this){
              this()
          }
      }
      
      var b=function(){
          console.log("b")
          if(this){
              this()
          }
      }
      
      var c=function(){
          console.log("c")
          if(this){
              this()
          }
      }
      
      var d=a.bind(b)
      var e=d.bind(c)
      d()
      e()
      
      
      </script>
      

      d = a.bind(b); d();
      这里d()的意思是将a的this绑定到b,然后执行。因此执行输出肯定是a, b。因此a中的this()实际上就等于b().但是当b中运行时,this的类型就已经是不是function了,因此运行肯定是会报错的。

      e = d.bind(c); e();
      同样的道理,这里会首先执行d函数,然后执行c函数,但是在执行d的时候this就已经是undefined了,肯定就不会继续向下执行了,因此也就仅仅只执行了d函数,输出了a, b。

      具体请参考:js 中多次bind的效果为什么会是这样?

  • this指向
    javascript中的 this 的指向不太好控制,理解不好的话很容易错误

    下面几个示例可以加深对this指向的理解

    • 内联事件

      <a href="#" onclick="alert(this.tagName)">
      
      click me
      
      </a>
      

      这种情况可以正常弹出a

      <a href="JavaScript:alert(this.tagName)">
      
      click me2
      
      </a>
      

      这样就不行了,会弹出 "undefined"

      因为使用 JavaScript: 相当于定义了一个全局函数,this则指向window对象

      如果定义一个全局变量,如var tagName = 'tag name';
      再点击’click me2‘时就会弹出 'tag name'

    • setTimeout和setInterval

      //全局变量
      var name = "全局";
      
      var duang = {
          name: "局部",
          hi: function() { alert("我是 " + this.name); }
      };
      
      duang.hi();
      

      执行结果为“我是 局部”

      setTimeout( duang.hi, 1000);
      setInterval( duang.hi, 1000);
      

      这两种情况都会弹出“我是 全局”

      因为setTimeout和setInterval都会改变this的指向为window

    • Dom节点.on×××

      <button id="btn" name="button">
      
      btn
      
      </button>
      
      var btn = document.getElementById("btn");
      var duang = {
      
      name: "duang",
      
      hi: function() { alert("I'm " + this.name); }
      
      };
      btn.onclick = duang.hi;
      

      点击按钮后,并没弹出duang的name属性值,而是弹出了button的name属性

      说明这种方法会使this指向dom节点本身

      如果想this指向duang对象,可以使用匿名函数解决

      btn.onclick = function (){
      duang.hi();
      }
      

      上面的setTimeout和setInterval情况也可以使用此方法处理

      setTimeout( function (){ duang.hi();}, 1000);
      setInterval( function (){ duang.hi();}, 1000);
      

      可以看到,这种直接调用和通过匿名函数间接调用 对this的指向影响很大,开发时需要特别注意

    • call和apply

      接着上面的例子,改动一下调用方式

      <button id="btn" name="button">
      
      btn
      
      </button>
      
      var name = "全局";
      var btn = document.getElementById("btn");
      var duang = {
          name: "duang",
      
      hi: function() { alert("I'm " + this.name); }
      };
      btn.onclick = function (){
      duang.hi.call();
      }
      

      这里使用了匿名函数,但是通过call方法调用了duang对象的hi函数

      这时的点击结果为 “I’m 全局”,说明this指向了window对象

      注意,使用call和apply调用方法时,this的指向会被改为window

  • 深拷贝与浅拷贝

    • 数组的浅拷贝

      如果是数组,我们可以利用数组的一些方法比如:slice、concat 返回一个新数组的特性来实现拷贝。

      比如:

      var arr = ['old', 1, true, null, undefined];
      
      var new_arr = arr.concat();
      
      new_arr[0] = 'new';
      
      console.log(arr) // ["old", 1, true, null, undefined]
      console.log(new_arr) // ["new", 1, true, null, undefined]
      

      用 slice 可以这样做:

      var new_arr = arr.slice();

      但是如果数组嵌套了对象或者数组的话,比如:

      var arr = [{old: 'old'}, ['old']];
      
      var new_arr = arr.concat();
      
      arr[0].old = 'new';
      arr[1][0] = 'new';
      
      console.log(arr) // [{old: 'new'}, ['new']]
      console.log(new_arr) // [{old: 'new'}, ['new']]
      

      我们会发现,无论是新数组还是旧数组都发生了变化,也就是说使用 concat 方法,克隆的并不彻底。

      如果数组元素是基本类型,就会拷贝一份,互不影响,而如果是对象或者数组,就会只拷贝对象和数组的引用,这样我们无论在新旧数组进行了修改,两者都会发生变化。

      我们把这种复制引用的拷贝方法称之为浅拷贝,与之对应的就是深拷贝,深拷贝就是指完全的拷贝一个对象,即使嵌套了对象,两者也相互分离,修改一个对象的属性,也不会影响另一个。

      所以我们可以看出使用 concat 和 slice 是一种浅拷贝。

    • 数组的深拷贝

      那如何深拷贝一个数组呢?这里介绍一个技巧,不仅适用于数组还适用于对象!那就是:

      var arr = ['old', 1, true, ['old1', 'old2'], {old: 1}]
      
      var new_arr = JSON.parse( JSON.stringify(arr) );
      
      console.log(new_arr);
      

      是一个简单粗暴的好方法,就是有一个问题,不能拷贝函数,我们做个试验:

      var arr = [function(){
          console.log(a)
      }, {
          b: function(){
              console.log(b)
          }
      }]
      
      var new_arr = JSON.parse(JSON.stringify(arr));
      
      console.log(new_arr);
      

      我们会发现 new_arr 变成了:

      JavaScript高级

    • 浅拷贝的实现

      以上三个方法 concat、slice、JSON.stringify 都算是技巧类,可以根据实际项目情况选择使用,接下来我们思考下如何实现一个对象或者数组的浅拷贝。

      想一想,好像很简单,遍历对象,然后把属性和属性值都放在一个新的对象不就好了~

      嗯,就是这么简单,注意几个小点就可以了:

      var shallowCopy = function(obj) {
          // 只拷贝对象
          if (typeof obj !== 'object') return;
          // 根据obj的类型判断是新建一个数组还是对象
          var newObj = obj instanceof Array ? [] : {};
          // 遍历obj,并且判断是obj的属性才拷贝
          for (var key in obj) {
              if (obj.hasOwnProperty(key)) {
                  newObj[key] = obj[key];
              }
          }
          return newObj;
      }
      
    • 深拷贝的实现

      那如何实现一个深拷贝呢?说起来也好简单,我们在拷贝的时候判断一下属性值的类型,如果是对象,我们递归调用深拷贝函数不就好了~

      var deepCopy = function(obj) {
          if (typeof obj !== 'object') return;
          var newObj = obj instanceof Array ? [] : {};
          for (var key in obj) {
              if (obj.hasOwnProperty(key)) {
                  newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
              }
          }
          return newObj;
      }
      
    • 性能问题

      尽管使用深拷贝会完全的克隆一个新对象,不会产生副作用,但是深拷贝因为使用递归,性能会不如浅拷贝,在开发中,还是要根据实际情况进行选择。