前端面试之道记录

1.JS分为哪两大类?各有什么特点?如何正确判断类型?

  • 原始类型

    • js的原始类型为string,number,boolean,undefined,nullsymbol6种,它们储存的是值,但是平常使用中会发现1.toString()这样的情况,原因是因为此时的1已经不是原始类型而是强制转换成了Number对象类型,而Number对象里面含了toString这个方法,所以可以调用,但实际上这样的函数调用后并不会影响原有的值。

    • 此外原始类型中还有一些奇怪的现象:

      比如0.1+0.2!= 0.3,因为js的number类型是浮点类型,它的有效处理精度为17位,在精度大于17的情况下它会取一个近似数。对于0.1以及0.2来说它们的二进制是个无限循环小数,所以它会去取结果的近似数,而二者的近似数相加结果为0.300…04 (中间14个0)。如果要得到正确结果可以先放大倍数计算结果后再缩小相应倍数。

      typeof null === 'object' 所以null不应该是对象吗?事实是不对的,这只是 JS 存在的一个悠久的历史 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。

    • 原始类型的判断除了null之外都可以用typeof判断

  • 对象类型

    除了上所说的原始类型,剩下的就是对象类型。

    • 对象类型有String,Number,Boolean,RegExp,Date,Function,Object,Array,Math,Error等内置对象,也有DOM,BOM这样的宿主(浏览器)对象

    • 对象储存的不是值,而且指针。对于表达式const a = {},我们创建了一个对象并赋予变量a,计算机内部则是在内存中分配了空间给改对象,并将内存地址存放于a,此后在使用变量a的时候计算机就能找到对应内存地址并进行相应的操作。 然后对于表达式const b = a,其实就是把a所指向的地址复制给了b,也就是同一个对象,当对b进行一些修改操作后会发现a也发生了同样的变化。所以对于对象类型的操作要小心小心。

    延伸:对象拷贝。

    • 在js中对象拷贝分为两种,深拷贝浅拷贝
    • 浅拷贝可以通过 Object.assign 来解决这个问题,很多人认为这个函数是用来深拷贝的。其实并不是,Object.assign 只会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的是地址,所以并不是深拷贝
      const a = {b:1,c:2,d:{e:4}}
      const b = Object.assign({}, a)
      // a !== b 但是a跟b中的d属性是相等的,他们指向同一个地址
      
    • 正常情况下,浅拷贝能解决大部分问题,但是如果这个时候需要修改d属性,那么就需要用到深拷贝,对于深拷贝,目前通常使用JSON.parse(JSON.stringify(object)),不过该方法也有局限性:会忽略 undefined、symbol,不能序列化函数,不能解决循环引用的对象。那怎么办?没有关西,这里还有种解决方案MessageChannel,它可以解决上面方法中的大部分问题。MessageChannel开辟了一个通信通道,通道两头为两个端口,每个端口都能发送和接受消息:
      var channel = new MessageChannel();
      var port1 = channel.port1;
      var port2 = channel.port2;
      port1.onmessage = function(event) {
          console.log("port1收到来自port2的数据:" + event.data);
      }
      port2.onmessage = function(event) {
          console.log("port2收到来自port1的数据:" + event.data);
      }
      
      port1.postMessage("发送给port2");
      port2.postMessage("发送给port1");
      
      然而,大部分不是所有,它无法拷贝带有函数的对象=_=. 那就没完美的解决方案了吗?您好。有的!这里就有现成的轮子lodash 的深拷贝函数,或者如果你想自己实现一个深拷贝轮子并且考虑好多种边界情况,比如原型链如何处理、DOM 如何处理等,那么完全可以尝试下突破自我~

    如何判断对象类型呢

    • 无法使用typeof精确判断对象类型,除了函数,其他对象类型的结果为object,用Array.prototype.slice.call(obj)是一种方法
    • 对于对象类型,除了上面的方法,还可以使用instanceof,它的内部机制是通过原型链来判断。
      // a instanceof b
      function _instanceof(a, b) {
          if (a === null || typeof a !== 'object') return false
          let proto = a.__proto__
          while(proto) {
              if (proto === b.prototype) return true
              proto = proto.__proto__
          }
          return false
      }
      
    • 但是instanceof也不是百分百可以判断的,在class中,我们可以通过静态方法[Symbol.hasInstance]自定义instanceof在某个类上的行为, 因此我们也可以使用该方式去实现用instanceof判断原始类型的
      方法。
      class PrimitiveString {
        static [Symbol.hasInstance](x) {
          return typeof x === 'string'
        }
      }
      console.log('hello world' instanceof PrimitiveString) // true
      

2. 你理解的原型是什么?

  • 行走江湖,不会点武功本事怎么行,这个高手会很多武功,他可以收徒弟,并将这些武功传授给他,学不会没关系,还可以记在身体上,到需要时能想起来就行,就像天下第一里的成是非…而徒弟不仅学会了师傅的武功,也有自己的际遇学会其他厉害的武功,他也可以继续收徒弟传授下去…可以说你强就是我强,青出于蓝胜于蓝!

  • 在js中所有的函数默认都有一个公有并且不可枚举的对象属性prototype,该对象默认的只有一个叫做constructor的属性,指向这个函数本身。不仅如此,每个对象都有一个隐藏的属性——__proto__,这个属性引用了创建这个对象的函数的prototype,也就是说

     function A() {
         //...
     }
     const a = new A()
     a.__proto__ == A.prototype
    
  • 那么__proto__这个属性是干什么用的呢?当我们访问对象a中的某个属性,首先会在a中查找是否存在这个属性,如果没有,就去a__proto__中查找,如果有则返回a.__proto__中的这个属性,但是如果还是没有,那么继续从a.__proto__.__proto__中查找,一直到查到目标属性或者之后的__proto__不存在为止,而这个查找链路就是原型链。延伸:原型继承, class 继承

  • 不过,也不是所有函数都有prototype属性,在规范中有这么段描述:Built-in functions that are not constructors do not have a prototype property unless otherwise specified in the description of a particular function..非构造函数的内置函数没有prototype属性,除非在特定函数的描述中另有说明。prototype 存在的意义是在 function 作为 constructor 用时(new 或 super)能复制到生成对象的__proto__ 上。对于一些内部方法明确是不会作为 constructor 的,所以没有 prototype 是很合理的,比如迭代器箭头函数bind返回的函数

3. call, apply, bind各自有什么区别

三个都是为了修改this指向

  • 功能上来说他们没什么区别, call、apply会在函数调用后马上执行,同时它们两个接受参数的形式不同,call除了第一个指定的this外,接受的参数跟方法体一一对应,而apply则是只有两个参数,第一个跟call一样,第二个则是一个参数数组。至于bind,它的参数形式跟call一样,只不过它返回的是原函数,需要再执行一次,同时,因为返回的是个函数,所以它可以调用的时候再传参数。
  • 在某些情况下可以通过类似that这样的变量缓存this,并通过该变量去使用。

延伸:this绑定

  • 默认绑定:非严格模式下全局环境调用函数执行,this指向window

    function foo() {
        "use strict"
        console.log(this.a)
    }
    var a = 1
    foo() // TypeError: this is undefined 
    
  • 隐式绑定:this总是指向调用该函数的对象

    function foo() { 
        console.log( this.a );
    }
    var obj2 = { 
        a: 42,
        foo: foo 
    };
    var obj1 = { 
        a: 2,
        obj2: obj2,
        foo: foo
    };
    obj1.foo() // 2
    obj1.obj2.foo(); // 42
    

    不过要注意以下隐式丢失的情况:

    function foo() { 
        console.log( this.a );
    }
    function baz(fn) {
        fn()
    }
    var obj = { 
        a: 2,
        foo: foo 
        
    };
    var bar = obj.foo; // 函数别名!此时的bar是foo的函数引用
    var a = "global";
    bar() //global 此时的bar()相当于是 foo() 以下也是
    barz(obj.foo) 
    setTimeout(obj.foo, 100)
    /*
    ↓
    相当于
    function setTimeout(fn, delay) {
        // delay
        fn()
    }
    */
    
  • 显式绑定
    前面说到的call,apply,bind

  • new绑定

    使用new来调用函数会发生以下操作:

    1. 创建一个全新的对象。
    2. 对新对象执行原型链设置。
    3. 绑定函数调用的this为该新对象。
    4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
        function new(fn, ...args) {
            const obj = Object.create(fn.prototype)
            let result = Parent.apply(child, args);
            return typeof result  === 'object' ? result : obj;
            
        }
    
  • 箭头函数
    严格来说箭头函数它没有this,它的this来自外层(函数或者全局)作用域,它跟普通变量一样会向上逐级寻找,所以改变作用域中的this也会改变箭头函数中的。

参考文献

  • MessageChannel是什么,怎么使用?
  • 继承与原型链
  • JavaScript ES6 Symbol.hasInstance的理解。
  • 深入理解javascript原型和闭包系列
  • 你不知道的JavaScript(上)

你可能感兴趣的:(js)