面试题 — JS篇

问:JS 原始数据类型有哪些?引用数据类型有哪些?

答:在JS中,存在7中原始数据类型,分别是:
boolean,null,undefined,number,string,symbol,bigint
引用类型:Object(包含普通对象 Object, 数组对象 Array, 正则对象 RegExp,日期对象 Date,数字函数 Math,函数对象 Function)

  • Symbol表示独一无二的值,主要是用来定义对象的唯一属性名
  • bigint 可以表示任意大小的整数

问:请说出下面代码的运行结果,并解释原因

function test(person) {
person.age = 26
person = {
name: 'hzj',
age: 18
}
return person
}
const p1 = {
name: 'fyq',
age: 19
}
const p2 = test(p1)
console.log(p1) // -> {name: 'fyq',age: 26}
console.log(p2) // -> { name: 'hzj',age: 18 }

答:因为在test函数传参的时候,实际上传递的是p1对象在堆中的内存地址值,所以通过调用person.age = 26确实改变了p1的值,但是随后person变成了另一块内存空间的地址,并且在最后将这个内存空间的地址返回,然后赋值给了p2

问:下面代码的输出结果是什么?(. 和 = 操作符的优先级)

let a = {n : 1}
let b = a
a.x = a = {n: 2}

console.log(a.x) // undefined
console.log(b.x) // {n:2}

首先我们要知道 在js的运算中".“和”=“运算符同时出现时,会先执行”."运算
所以 a.x = a = {n:2} --> a.x = {n:2} --> a = {n:2}

问:0.1+0.2为什么不等于0.3?

答:(进制转换) js在做数字计算的时候,0.1和0.2都会被转成二进制后无限循环,但是js采用的IEEE 754二进制浮点运算,最大可以存储53位有效数字,于是大于53位后面的会全部截掉,将会导致精度缺失。
(对阶运算) 由于指数位数不相同,运算时需要对阶运算,这部分也可能产生精度缺失

https://juejin.cn/post/6940405970954616839
https://juejin.cn/post/6844903680362151950

你真的了解作用域吗?

var a = 0,b = 0;
function A(a) {
A = function (b) {
console.log(a + b++)
}
console.log(a++)
}
A(1) // 1
A(2) // 4

‘1’.toString()为什么可以调用?

实际上这个语句运行的过程中做了以下几件事:

var s = new Object("1") // 第一步:创建Object实例。
s.toString() // 第二步:调用实例方法
s = null // 第三步:执行完方法立即销毁这个实例

创建Object实例。注意为什么不是String?由于Symbol和BigInt的出现,对它们调用new都会报错,目前ES6规范也不建议用new来创建基本类型的包装类

整个过程体现了基本包装类型的性质,而基本包装类型恰恰属于基本数据类型,包括Boolean,Number和String

数据类型的判断

  • typeof:能判断所有值类型,函数。不可对null、对象、数组进行精确判断,因为都返回object

    console.log(typeof undefined); // undefined
    console.log(typeof 2); // number
    console.log(typeof true); // boolean
    console.log(typeof "str"); // string
    console.log(typeof Symbol("foo")); // symbol
    console.log(typeof 2172141653n); // bigint
    console.log(typeof function () {}); // function
    // 不能判别
    console.log(typeof []); // object
    console.log(typeof {}); // object
    console.log(typeof null); // object
  • instanceof:可用于判断对象类型

    instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

    const Person = function() {}
    const p1 = new Person() // p1.__proto__ === Person.prototype
    p1 instanceof Person // true 即Person构造函数的prototype出现在实例p1的原型链上

    var str1 = "hello"
    str1 instanceof String // false(str1不是对象实例)

    var str2 = new String("hello")
    str2 instanceof String // true
  • Object.prototype.toString.call():所有原始数据类型都能判断

    Object.prototype.toString.call(2); // "[object Number]"
    Object.prototype.toString.call(""); // "[object String]"
    Object.prototype.toString.call(true); // "[object Boolean]"
    Object.prototype.toString.call(undefined); // "[object Undefined]"
    Object.prototype.toString.call(null); // "[object Null]"
    Object.prototype.toString.call(Math); // "[object Math]"
    Object.prototype.toString.call({}); // "[object Object]"
    Object.prototype.toString.call([]); // "[object Array]"
    Object.prototype.toString.call(function () {}); // "[object Function]"

为什么要用call去调用toString方法呢?

借用Object原型对象上的toString方法,通过this绑定调用,返回"[object 类型]"

如何判断变量是否为数组?

Array.isArray(arr); // true
arr.__proto__ === Array.prototype; // true
arr instanceof Array; // true
Object.prototype.toString.call(arr); // "[object Array]"

== 和 === 有什么区别?

  • === 叫严格相等,左右两边不仅值要相等,类型也要相等

  • == 对于一般情况,只要值相等就返回 true,但 == 还涉及一些类型转换,转换规则如下:

    • 两边类型是否相同,相同的话就比较值的大小,如:1==2 false
    • 判断两边是否是 null 和 undefined,是的话就返回true
    • 两边是否是 String 和 Number,是的话,就把String类型转换成 Number,再进行比较
    • 判断其中一边是否是 Boolean,是的话就把Boolean转换成Number,再进行比较
    • 如果其中一边为 Object,而另一边为String、Number或Symbol,会将Object转换成字符串,再进行比较
    console.log({a: 1} == true) // false
    console.log({a: 1} == "[object Object]") // true

[ ] == ![ ]的结果是什么?

[] == ![] // 逻辑运算需进行布尔值转换, [] -> true
[] == !true
[] == false // false -> 0
[] == 0
// [] 转换为数值,经过 [].valueOf() --> []
// [].toString -> ""
// "".toNumber() -> 0
0 == 0 // true

对象转原始类型是根据什么流程运行的?

对象转原始类型,会调用内置的 [ToPrimitive] 函数,对于该函数,其逻辑如下:

Symbol.toPrimitive 是一个内置的 Symbol 值,它是作为对象的函数值属性存在的,当一个对象转换为对应的原始值时,会调用此函数。

  1. 如果有Symbol.toPrimitive()方法,优先调用再返回
  2. 调用valueOf(),如果转换后为原始类型,则返回,否则继续
  3. 调用toString(),如果转换后为原始类型,则返回
  4. 如果都没有返回原始类型,会报错
var obj = {
value: 3,
valueOf() {
return 4
},
toString() {
return '5'
},
[Symbol.toPrimitive]() {
return 6
}
}

console.log(obj + 1); // 7

如何让 if(a == 1 && a == 2)条件成立?

如果 a 是对象,则在执行 == 比较的时候,会执行 valueOf 方法(或toString),因此我们可以重写该方法让条件成立

var a = {
value: 0,
valueOf: function() {
this.value++
return this.value
}
}
console.log(a == 1 && a == 2); // true

谈谈你对闭包的理解

什么是闭包?

MDN对JS闭包的解释:

  • 一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure)。
  • 也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。
  • 在JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

也可以说:

闭包是指那些能够访问自由变量的函数。自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量

闭包 = 函数 + 函数能够访问的自由变量

闭包的产生

在某个内部函数的执行上下文创建时,会将父级函数的活动对象(VO)加到内部函数的 [[scope]]中,形成作用域链,所以即使父级函数的执行上下文销毁,但是因为其活动对象实际上还存储在内存中可被内部函数访问到,从而实现了闭包

闭包的应用

  • 函数作为参数被传递

    var a = 1;
    function foo(){
    var a = 2;
    function baz(){
    console.log(a);
    }
    bar(baz);
    }
    function bar(fn){
    // 这就是闭包
    fn();
    }
    foo(); // 2
  • 函数作为返回值被返回

    function foo() {
    const a = 100

    return function() {
    console.log(a);
    }
    }

    const fn = foo()
    const a = 200
    fn() // 100

闭包:自由变量的查找,是在函数定义的地方,向上级作用域查找。不是在执行的地方

如何解决下面的循环输出问题?

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

为什么全部输出6?如何改进让它输出1,2,3,4,5?

因为setTimeout为宏任务,由于JS中单线程eventLoop机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后setTimeout中的回调才依次执行,输出i的时候,当前作用域没有,往上一层找,因为循环已结束,上层作用域中的i为6。因此会全部输出6

解决方法:

  1. 利用IIFE,每次for循环时,把此时的 i 变量传递到定时器中

    IIFE( 立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数

    for(var i = 1;i <= 5;i++){
    (function(j){
    setTimeout(function timer(){
    console.log(j)
    }, 0)
    })(i)
    }
  2. 给定时器传入第三个参数,作为timer函数的第一个函数参数

    定时器从第三个参数开始是附加参数,一旦定时器到期,它们会作为参数传递给function

    for (var i = 1; i <= 5; i++) {
    setTimeout(function(j) {
    console.log(j);
    }, 0, i)
    }
  3. 使用ES6中的let

    let是块级作用域,用let后,变量只在它所在的代码块生效,没有什么作用域链了

    // i = 1
    {
    setTimeout(function timer() {
    console.log(1);
    }, 0)
    }
    // i = 2
    {
    setTimeout(function timer() {
    console.log(2);
    }, 0)
    }
    // i = 3
    {
    setTimeout(function timer() {
    console.log(3); // 3
    }, 0)
    }

    ...

    所以可以依次输出1,2,3,4,5

谈谈你对原型链的理解

  • 原型对象和构造函数有何关系?

在JavaScript中,每当定义一个函数数据类型(普通函数、类)的时候,这个函数就会自带一个prototype属性,这个属性指向函数的原型对象

另外,这个原型对象会有一个constructor属性指回函数本身

当函数被new调用的时候,这个函数就称为了构造函数

new调用会创建一个实例对象,这个实例对象会自带一个__proto__属性,这个属性指向构造函数的原型对象

o.__proto__ === Foo.prototype

image.png
  • 描述一下原型链?

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

原型链最顶层的原型对象就是Object的原型对象

另外,检查对象自身是否包含某属性,可以使用 hasOwnProperty()

使用 in 的话,即使自身没有,但是原型链中有,也会返回true

绿色箭头,原型链

o.__proto__.__proto__.__proto__

image.png

带着掘金的各种文章一起看,找到自己想要的答案
做了一份前端面试复习计划,保熟~
链接:https://juejin.cn/post/7061588533214969892
神十三-原生JS灵魂之问, 请问你能接得住几个
链接:https://juejin.cn/post/6844903974378668039
字节跳动最爱考的前端面试题:JavaScript 基础
链接:https://juejin.cn/post/6934500357091360781
「查缺补漏」送你 54 道 JavaScript 面试题
链接:https://juejin.cn/post/6854573211443544078#comment