JS 基础面试题

原始(Primitive)类型

涉及面试题:原始类型有哪几种?null 是对象嘛?

在 JS 中,存在着 6 种原始值,分别是:

  • boolean
  • null
  • undefined
  • number
  • string
  • symbol

首先原始类型存储的都是值,是没有函数可以调用的,比如 undefined.toString()

image-20200425090048089

此时你肯定会有疑问,这不对呀,明明 '1'.toString() 是可以使用的。其实在这种情况下,'1' 已经不是原始类型了,而是被强制转换成了 String 类型也就是对象类型,所以可以调用 toString 函数。

除了会在必要的情况下强转类型以外,原始类型还有一些坑。

其中 JS 的 number 类型是浮点类型的,在使用中会遇到某些 Bug,比如 0.1 + 0.2 !== 0.3,但是这一块的内容会在进阶部分讲到。string 类型是不可变的,无论你在 string 类型上调用何种方法,都不会对值有改变。

另外对于 null 来说,很多人会认为他是个对象类型,其实这是错误的。虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。

对象(Object)类型

涉及面试题:对象类型和原始类型的不同之处?函数参数是对象会发生什么问题?

在 JS 中,除了原始类型那么其他的都是对象类型了。对象类型和原始类型不同的是,原始类型存储的是值,对象类型存储的是地址(指针)。当你创建了一个对象类型的时候,计算机会在内存中帮我们开辟一个空间来存放值,但是我们需要找到这个空间,这个空间会拥有一个地址(指针)。

const a = []

对于常量 a 来说,假设内存地址(指针)为 #001,那么在地址 #001 的位置存放了值 [],常量 a 存放了地址(指针) #001,再看以下代码

const a = []
const b = a
b.push(1)

当我们将变量赋值给另外一个变量时,复制的是原本变量的地址(指针),也就是说当前变量 b 存放的地址(指针)也是 #001,当我们进行数据修改的时候,就会修改存放在地址(指针) #001 上的值,也就导致了两个变量的值都发生了改变。

接下来我们来看函数参数是对象的情况

function test(person) {
  person.age = 26
  person = {
    name: 'yyy',
    age: 30
  }
  return person
}
const p1 = {
  name: 'yck',
  age: 25
}
const p2 = test(p1)
console.log(p1) // -> {name: "yck", age: 26}
console.log(p2) // -> {name: "yyy", age: 30}

对于以上代码,你是否能正确的写出结果呢?接下来让我为你解析一番:

  • 首先,函数传参是传递对象指针的副本
  • 到函数内部修改参数的属性这步,我相信大家都知道,当前 p1 的值也被修改了
  • 但是当我们重新为 person 分配了一个对象时就出现了分歧,请看下图

image-20200425090130725

所以最后 person 拥有了一个新的地址(指针),也就和 p1 没有任何关系了,导致了最终两个变量的值是不相同的。

typeof VS instanceof

涉及面试题:typeof 是否能正确判断类型?instanceof 能正确判断对象的原理是什么?

typeof 对于原始类型来说,除了 null 都可以显示正确的类型

typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'

typeof 对于对象来说,除了函数都会显示 object,所以说 typeof 并不能准确判断变量到底是什么类型

typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'

如果我们想判断一个对象的正确类型,这时候可以考虑使用 instanceof,因为内部机制是通过原型链来判断

const Person = function() {}
const p1 = new Person()
p1 instanceof Person // true

var str = 'hello world'
str instanceof String // false

var str1 = new String('hello world')
str1 instanceof String // true

对于原始类型来说,你想直接通过 instanceof 来判断类型是不行的,当然我们还是有办法instanceof 判断原始类型的

class PrimitiveString {
  static [Symbol.hasInstance](x) {
    return typeof x === 'string'
  }
}
console.log('hello world' instanceof PrimitiveString) // true

你可能不知道 Symbol.hasInstance 是什么东西,其实就是一个能让我们自定义 instanceof 行为的东西,以上代码等同于 typeof 'hello world' === 'string',所以结果自然是 true 了。这其实也侧面反映了一个问题, instanceof 也不是百分之百可信的。

类型转换

涉及面试题:该知识点常在笔试题中见到,熟悉了转换规则就不惧怕此类题目了。

首先我们要知道,在 JS 中类型转换只有三种情况,分别是:

  • 转换为布尔值
  • 转换为数字
  • 转换为字符串

我们先来看一个类型转换表格,然后再进入正题

image-20200425090243873

转Boolean

在条件判断时,除了 undefinednullfalseNaN''0-0,其他所有值都转为 true,包括所有对象。

对象转原始类型

对象在转换类型的时候,会调用内置的 [[ToPrimitive]] 函数,对于该函数来说,算法逻辑一般来说如下:

  • 如果已经是原始类型了,那就不需要转换了
  • 调用 x.valueOf(),如果转换为基础类型,就返回转换的值
  • 调用 x.toString(),如果转换为基础类型,就返回转换的值
  • 如果都没有返回原始类型,就会报错

当然你也可以重写 Symbol.toPrimitive ,该方法在转原始类型时调用优先级最高。

let a = {
  valueOf() {
    return 0
  },
  toString() {
    return '1'
  },
  [Symbol.toPrimitive]() {
    return 2
  }
}
1 + a // => 3

四则运算符

加法运算符不同于其他几个运算符,它有以下几个特点:

如果对方含有字符串默认先转换成字符串,其他类型优先转换为数字

  • 运算中其中一方为字符串,那么就会把另一方也转换为字符串
  • 如果一方不是字符串或者数字,那么会将它转换为数字或者字符串
1 + '1' // '11'
true + true // 2
4 + [1,2,3] // "41,2,3"

如果你对于答案有疑问的话,请看解析:

  • 对于第一行代码来说,触发特点一,所以将数字 1 转换为字符串,得到结果 '11'
  • 对于第二行代码来说,触发特点二,所以将 true 转为数字 1
  • 对于第三行代码来说,触发特点二,所以将数组通过 toString 转为字符串 1,2,3,得到结果 41,2,3

另外对于加法还需要注意这个表达式 'a' + + 'b'

'a' + + 'b' // -> "aNaN"

因为 + 'b' 等于 NaN,所以结果为 "aNaN",你可能也会在一些代码中看到过 + '1' 的形式来快速获取 number 类型。

那么对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字

4 * '3' // 12
4 * [] // 0
4 * [1, 2] // NaN

比较运算符

  1. 如果是对象,就通过 toPrimitive 转换对象
  2. 如果是字符串,就通过 unicode 字符索引来比较
let a = {
  valueOf() {
    return 0
  },
  toString() {
    return '1'
  }
}
a > -1 // true

在以上代码中,因为 a 是对象,所以会通过 valueOf 转换为原始类型再比较值。

This指向问题:

涉及面试题:如何正确判断 this?箭头函数的 this 是什么

this 的指向,是在调用函数时根据执行上下文所动态确定的。

•函数在浏览器全局环境中被简单调用(非显式/隐式绑定下),

​ 严格模式下 this 绑定到 undefined,

​ 否则绑定到全局对象 window/global;

•在执行函数时,如果函数中的this是被上一级的对象所调用,那么this指向就是上一级的对象; 否则指向全局环境。

回调函数(除事件函数)

​ 数组的所有遍历方法forEach,map,filter,reduce,every,some,flatMap,sort;这些方法均使用了回调函数,因此在所有使用回调函数的方法中,所有回调函数中this都被指window,

​ setInterval,setTimeOut 函数中的回调函数的因为作用域不明(不知道在哪里调用)就会指向window:

事件函数中的this:指向侦听的对象

​ 这里的特殊情况(事件函数)是因为:在函数执行时底层函数调用了call和apply,因此此时的回调函数中的this就会被指向绑定的侦听对象上;

•在定义对象属性时,obj对象还没有创建完成;this仍旧指向window

•一般构造函数 new 调用,绑定到新创建的对象上;

•一般由 call/apply/bind 方法显式调用,绑定到指定参数的对象上;

​ 面试技巧:如果把这项放到最后说下个问题多半就是三者区别

•一般由上下文对象调用,绑定在该对象上;

箭头函数中,根据外层上下文绑定的 this 决定 this 指向。

*1:全局环境下的 this*

函数在浏览器全局环境中被简单调用,*ES5*非严格模式下指向 window,*ES6*严格模式下指向 undefined。

function fn1(  ) {
  console.log(this) }
fn1(  ) // window

function fn2(  ) {'use strict'
  console.log(this)}
fn2(  ) // undefined

在执行函数时,如果函数中的this是被上一级的对象所调用,那么this指向就是上一级的对象; 否则指向全局环境。

Var foo = {
  bar:10,
  fn:function(  ) {
    console.log(this)
    console.log(this.bar)}
}
***\*var fn1 = foo.fn\****
fn1(  ) // ***\*直接调用\****,this ***\*指向 window\****,window.bar => undefined
foo.fn(  ) // 通过 foo 调用,this 指向 foo,foo.bar => 10
    this.a=3;//this--->window
    var b=5;
    function fn(){
      var b=10;
      console.log(b+this.b);//this--->window
      // 这种方法仅限于ES5,在ES6严格模式中this将会变成undefined
    }
    fn()

*2、回调函数中的this*

数组的所有遍历方法forEach,map,filter,reduce,every,some,flatMap,sort;这些方法均使用了回调函数,因此在所有使用回调函数的方法中,除了特殊的情况外(事件函数),其他所有回调函数中this都被指向window,setInterval,setTimeOut 函数中的回调函数的因为作用域不明(不知道在哪里调用)就会指向window:

  var obj = {
        fn: function ( ) {
          // console.log(this);
          ***\*return\**** function ( ) {
            console.log(this);//this--->window
          }
      }
    }
    var fn=obj.fn( );
    fn( );//因为是在另外的作用域调用
//return中回调函数因为相当于var fn=obj.fn( )( );是在外部执行所以会指向window

这里的特殊情况(事件函数)是因为:在函数执行时底层函数调用了call和apply,因此此时的回调函数中的this就会被指向document;

*3、对象中的this*

在定义属性时,obj对象还没有创建完成;this仍旧指向window

箭头函数指向当前域外的内容

  var c=100;
  var obj={
    c:10,
    b:this.c,//this--->window 定义属性时,obj对象还没有创建完成,this仍旧指向window
    a:function(){
      // this;//this--->obj
      // console.log(obj.c);
      console.log(this.c);
    },
    d:()=>{
      //this--->window
      console.log(this);
    }
  }
  // console.log(obj);
  // obj.d();
  var obj1=obj;
  obj=null;
  obj1.a();

这里a:function( )This.c}中为什么不用obj而用this呢:因为obj的地址值可能改变;就会找不到这个引用变量obj对象;

*4、ES6class中的this*

class Box{
    a=3;
    static abc(){
      console.log(this);//Box 静态方法调用就是通过类名.方法
      // Box.abc();
      // 尽量不要在静态方法中使用this
    }
    constructor(_a){
      this.a=_a    }
    play(){
      // this就是实例化的对象
      console.log(this.a);
      // 这个方法是被谁执行的,this就是谁
    }
  let b=new Box(10);
  b.play();
  let c=new Box(5);
  c.play();
使用静态方法:就指向box:相当于box.abc( )调用该方法;所以指向box

  class Box{
    a=3;
    static abc(){
      console.log(this);//Box 静态方法调用就是通过类名.方法
      // Box.abc();
      // 尽量不要在静态方法中使用this
    }

*5、ES5中的this*

function Box(_a){
    this.a=_a;
  }
Box.prototype.play=function(){
    console.log(this.a);//this就是实例化的对象
  }
Box.prototype.a=5;
  Box.abc=function(){
    //this
    // 这样的方法,等同于静态方法
  }
  var a=new Box(10);
  a.play();
  Box.abc();

*6、事件函数中的this:指向侦听的对象*

document.addEventListener("click",clickHandler);
  function clickHandler(e){
    console.log(this);//this--->e.currentTarget
  }

*7、Call apply bind中的this:指向绑定的对象*

 function fn(a,b){
      this.a=a;//this如果使用了call,apply,bind,this将会被指向被绑定的对象
      this.b=b;
      return this;
    }
    var obj=fn.call({},3,5)
    var obj=fn.apply({},[3,5])
    var obj=fn.bind({})(3,5);

*8、箭头函数中的this:*指向当前函数外的函数或内容与自带bind(this)的作用

var obj={
        a:function(){
          document.addEventListener("click",e=>{
           console.log(this);//指向事件侦听外函数中的this/obj
          });
          var arr=[1,2,3];
          arr.forEach(item=>{
            console.log(this);//this-->obj
          });
          // 相当于自带bind(this)的作用
          arr.forEach((function(item){
          }).bind(this));
        }
     }

图中的流程只针对于单个规则。

image-20200425090316831

你可能感兴趣的:(javascript前端)