关于面试题的总结, Js基础篇(一)

缘起

笔者最近在近两周的面试中, 遇到了大大小小形形色色的面试题。很多问题都是知道但是说不出来,所以想记录下来, 希望可以帮到大家,也可以方便自己以后查阅。
面试题无疑就是那几种类型的题, 所以笔者将按分类 分几次发, 也会有对应的问题扩展的解答。 希望可以帮到大家。
说明一下,面试题远不止这些,笔者只是就自己所遇到的做一个记录。若有说的不对的地方,还请大家指出来,大家共同进步!

Js基础的数据类型相关问题

1. Js 的基础数据类型有哪些?

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

数据类型扩展问题

1-1. js 的引用数据类型有哪些?

引用数据类型:

对象 Object(普通对象 Object, 数组对象 Array, 函数对象 Function, 正则对象 RegExp, 日期对象 Date, 数学函数 Math)

1-2. 0.1+0.2 为什么不等于 0.3?

0.10.2在转换成二进制后会无限循环,由于标准位数的限制后面多余的位数会被截掉,此时就已经出现了精度的损失,相加后因浮点数小数位的限制而截断的二进制数字在转换为十进制就会变成0.30000000000000004

1-3. 为什么添加BigInt数据类型?

由于js中,所有的数字都以双精度64位浮点格式表示,这导致JS中的Number无法精确表示非常大的整数,它会将非常大的整数四舍五入,也就是说 js 只能表示在-( 2^53 - 1) 到 2^53 - 1 之间的的数,任何超出范围的的数都可能会失去精度。所以需要BigInt ,BigInt 可以表示任意大的整数。

console.log(999999999999999);  //=>10000000000000000

// 由于精度受损,也会有一定的安全性问题,如下
9007199254740992 === 9007199254740993;    // -> true 居然是true!

更多关于 BigInt 的知识 点这里 这里就不过多的讲述了。

1-4. null 是对象吗?

typeof null // -> "object"

答案: null 不是对象;

原因:
虽然使用 typeof 判断 null 的数据类型为 object,但是 null 不是对象 。这也是js 存在的一个有机的 Bug。 在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象然而 null 表示为全零,所以将它错误的判断为 object

关于js 判断数据类型的方法

js 判断数据类型的方法有哪几种?

方法1. 使用 typeof 判断数据类型

对于原始数据类型而言, 除了 null 都可以使用typeof 判断出正确的数据类型。

对于引用数据类型, 除了function ,都显示为 "object"

缺点: 使用 typeof 判断引用数据类型的数据并不准确。

方法2. 使用instanceof 判断数据类型

instanceof的原理其实就是基于原型链的查询,只要是在原型链上能找到的都返回 true

注意:

1 instanceof Number // false
'1' instanceof String  // false

let a= {};
a instanceof Object  // true

大家也可以尝试一下;
缺点: 实际上相对于基础数据类型的话 instanceof 是不能准确的判断数据类型的。

方法3. 使用 Object.prototype.toString.call 方法判断

实际是继承Object 的原型方法判断

Object.prototype.toString.call('1')  //  "[object String]"
Object.prototype.toString.call(1)  //  "[object Number]"
Object.prototype.toString.call({})  //  "[object Object]"

使用这个方法就可以对基本数据类型和引用数据类型的做一个区分了。

关于 == 和 === 的区别?

关于 == 的一些说明

== 会对等式两边的值 自动转换数据类型之后在比较。

'1'==1  //  true

由此可以看出,== 只要两边的值相等,就会返回true(NaN 除外,NaN 不等于任何数值,包括他本身)。
== 的转换规则如下:

  • 两边数据类型相同,就比较值的大小是否相等;
  • 数据类型不同时:

1.判断双方是否为 nullundefined;是的话返回 true

null == undefined  //  true
null == null  //  true
undefined == undefined  //  true

.
2. 判断数据类型是否为 stringnumber;是的话 将 string 转为 number 再比较。

'1' == 1  // true

.

  1. 判断其中一方是否为 Boolean; 是的话就把Boolean 转化成 Number,再比较(true 为 1,false为 0)。
  2. 如果其中一方为 Object,且另一方为StringNumber或者Symbol,会将Object转换成字符串,再进行比较。
console.log({a: 1} == true);//false
console.log({a: 1} == "[object Object]");//true

.

关于 === 全等于的一些说明

=== 不会自动转换数据类型,也叫严格相等。如果等式两边表达式的数据类型不一致 , 就会直接返回false;也就是说等式两边的数据类型都要相等才会返回true

所以避免出现一些错误的判断都建议使用 === 来对数据进行一个相等判断;

注意:由于 NaN 的特殊性,只要等式两边由一方为 NaN 都会返回false

当然也有判断 NaN 的方法,那就是 isNaN() 函数

isNaN(NaN)  //  true

JS 中类型转换有哪些?

主要分为三类:

  • 转为数字
  • 转为字符串
  • 装维布尔值
    具体转换规则如下:


    js数据类型转换

    图表形式:


    在这里插入图片描述

关于闭包

什么是闭包?闭包有哪些作用?

(红宝书)闭包是指那些引用了另外一个函数作用域中变量的函数,通常是在嵌套函数中实现的。

闭包的作用:

  • 读取函数内部的变量
  • 让这些变量的值保存在内存里面,实现数据共享

闭包不能滥用,否则会导致内存泄露,影响网页的性能。闭包使用完了后,要立即释放资源,将引用变量指向null

闭包有那些表现形式?

1.函数作为返回值

var num = 2
function outer() {
 var num = 0 //内部变量
 return function add() {
 //通过return返回add函数,就可以在outer函数外访问了。
 num++ //内部函数有引用,作为add函数的一部分了
 console.log(num)
 }
}

var func1 = outer() //
func1() //实际上是调用add函数, 输出1

2.函数作为参数传递

var num = 2
function outer() {
 var num = 0 //内部变量
 function add() {
    num++ //内部函数有引用,作为add函数的一部分了
    console.log(num)
 }
 foo (add)
}
function foo (fn){
  // 函数以参数的方式传递执行 闭包 
    fn()
}

outer() // 1

3.在定时器、事件监听、Ajax请求、跨窗口通信、Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。

保存了全局作用域 window当前作用域

setTimeOut(function timeHandler(){
    console.log(123)
},100)

4.立即执行函数(IIFE)也是闭包的形式,保存了 全局作用域window当前函数作用域。可以使用全局变量。

var num = 5;
(function init(){
    console.log(num)  // 5
})()

闭包的经典笔试题

1.以下代码输出什么 ? 为什么 ? 如何修改 ?

 for ( var i=1;i<=5;i++) {
    setTimeout(function timer(){
          console.log(i);
     },i*1000)
}

因为setTimeout 是一个宏任务,在主线程主任务执行完之后才执行宏任务。因此,在循环结束之后才会执行 setTimeout回调。这个时候要使用 i 当前作用域下面并没有,就为向上级查找,这个时候的 i 变成了6, 所以会输出一堆6

解决办法
1. 使用 闭包(立即执行函数的形式)

for ( var i=1;i<=5;i++) {
  (function (j){
    setTimeout(function timer(){
          console.log(j);
     },j*1000)
  })(i)  
} // 1,2,3,4,5

2. 使用 setTimeout的第三个参数

for ( var i=1;i<=5;i++) {
    setTimeout(function timer(j){
          console.log(j);
     },i*1000,i)
} // 1,2,3,4,5

3. 使用ES6的 let 定义

 for ( let i=1;i<=5;i++) {
    setTimeout(function timer(){
          console.log(i);
     },i*1000)
}

由于let 的块级作用域,let 之后的作用域链也就不存在了。

对原型链的理解

JavaScript对象通过__proto__ 指向父类对象,直到指向Object对象为止,这样就形成了一个原型指向的链条, 即原型链。

原型链的顶端是什么?

原型链顶端是Object.prototype

Object 的原型最后是什么?

object 的原型最后的值是 null

console.log(Object.prototype.__proto__ === null),返回true

null 表示没有对象,即该处不应有值,所以Object.prototype没有原型。

proto是什么

function Person(){
}

绝大部分浏览器支持这个非标准的方法访问原型,然而它并不存在与Person.prototype中。

实际上它来自Object.prototype,当使用obj.__proto__时,可以理解为返回来Object.getPrototype(obj)

js有几种继承方法

每个对象都会从原型继承属性,继承意味着复制操作,然而JS默认不会复制对象的属性,相反,JS只是在两个对象之间创建一个关联,这样子 一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承。

以下继承方法参考js灵魂之问

第一种: 借助call

 function Parent1(){
    this.name = 'parent1';
  }
  Parent1.prototype.add = function () {}
  
  function Child1(){
    Parent1.call(this);
    this.type = 'child1'
  }
  console.log(new Child1);

注意: 这种方式子类只能拿到父类的属性,获取不到父类原型对象中的方法。

在这里插入图片描述

第二种: 原型链继承

  function Parent2() {
    this.name = 'parent2';
    this.play = [1, 2, 3]
  }
 Parent2.prototype.add = function () {}
 
  function Child2() {
    this.type = 'child2';
  }
  Child2.prototype = new Parent2();

  console.log(new Child2());

结果如下:
在这里插入图片描述

这种方法是可以成功继承到父类的属性和原型方法,但是有个潜在的问题,如下:

  var s1 = new Child2();
  var s2 = new Child2();
  s1.play.push(4);
  console.log(s1.play, s2.play);

在这里插入图片描述

注意: 修改s1 的属性值, s2 的属性也变化了,因为两个实例使用的是同一个原型对象。

call 和原型 的组合继承

  function Parent3 () {
    this.name = 'parent3';
    this.play = [1, 2, 3];
  }
   Parent3.prototype.add = function () {}

  function Child3() {
    Parent3.call(this); // 执行一次
    this.type = 'child3';
  }
  Child3.prototype = new Parent3();  // 执行两次
  console.log(new Child3 () )
  
  var s3 = new Child3();
  var s4 = new Child3();
  s3.play.push(4);
  console.log(s3.play, s4.play);

在这里插入图片描述

可以看到属性和方法都继承了,并且也没有共享实例的问题。但是这一种写法其实也有一个问题,那就是 Parent3 的构造函数会执行两次,增加了性能的消耗。

组合继承 优化

  function Parent4 () {
    this.name = 'parent4';
    this.play = [1, 2, 3];
  }
   Parent4.prototype.add = function () {}
    
  function Child4() {
    Parent4.call(this);
    this.type = 'child4';
  }
  Child4.prototype = Parent4.prototype;
  console.log(new Child4())

将父类原型对象直接给到子类,父类构造函数只执行一次,而且父类的属性和方法都能继承.

在这里插入图片描述

注意: 子类实例的构造函数 是Parent4,显然这是不对的,应该是Child4

(推荐)寄生组合继承

  function Parent5 () {
    this.name = 'parent5';
    this.play = [1, 2, 3];
  }
   Parent5.prototype.add = function () {}
   
  function Child5() {
    Parent5.call(this);
    this.type = 'child5';
  }
  Child5.prototype = Object.create(Parent5.prototype);
  Child5.prototype.constructor = Child5;

这是最推荐的一种方式,接近完美的继承,也是组合继承的进阶版。

ES6 的extends 继承

extends被编译之后的实现原理也是使用了 Object.create方法继承父类的静态方法。

extends 的语法

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y);
    this.color = color; // 正确
  }
}

在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。

面向对象的设计不一定是最好的,还是的依据具体的业务场景判断。

继承的最大问题在于:无法决定继承哪些属性,所有属性都得继承。

一方面父类是无法描述所有子类的细节情况的,为了不同的子类特性去增加不同的父类,代码势必会大量重复,另一方面一旦子类有所变动,父类也要进行相应的更新,代码的耦合性太高,维护性不好。

那如何来解决继承的诸多问题呢?

用组合,这也是当今编程语法发展的趋势,比如golang完全采用的是面向组合的设计方式
顾名思义,面向组合就是先设计一系列零件,然后将这些零件进行拼装,来形成不同的实例或者类。
这样复用性也很好,代码耦合小。

.

参考:
原生JS灵魂之问
yck前端面试之道
原型链的详细讲解

你可能感兴趣的:(关于面试题的总结, Js基础篇(一))