JavaScript 基础问题

JavaScript 基础问题

一些网上收集到的面试题答案总结…

1. 类型转换

参考:https://juejin.cn/post/7018851549170368520

在JavaScript中,加法的规则其实很简单,只有两种情况:

  • 数字和数字相加
  • 字符串和字符串相加

所有其他类型的值都会被自动转换成这两种类型的值

在JavaScript中,一共有两种类型的值:

  • 原始值有:undefinednull、 布尔值(Boolean)、 数字(Number)、字符串(String)、Symbol
  • 对象值:其他的所有值都是对象类型的值,包括数组(arrays)和函数(functions)

加法运算符会触发三种类型转换:将值转换为原始值、转换为数字、转换为字符串,这刚好对应了JavaScript引擎内部的三种抽象操作:

  • ToPrimitive()
  • ToNumber()
  • ToString()

通过 ToPrimitive() 将值转换为原始值

1
ToPrimitive(input, PreferredType?)

可选参数PreferredType可以是Number或者String它只代表了一个转换的偏好,转换结果不一定必须是这个参数所指的类型,但转换结果一定是一个原始值。如果PreferredType被标志为Number,则会进行下面的操作来转换输入的值:

  1. 如果输入的值已经是个原始值,则直接返回它。
  2. 否则,如果输入的值是一个对象。则调用该对象的valueOf()方法。如果valueOf()方法的返回值是一个原始值,则返回这个原始值。
  3. 否则,调用这个对象的toString()方法。如果toString()方法的返回值是一个原始值,则返回这个原始值。
  4. 否则,抛出TypeError异常。

如果PreferredType被标志为String,则转换操作的第二步和第三步的顺序会调换。如果没有PreferredType这个参数,则PreferredType的值会按照这样的规则来自动设置: Date类型的对象会被设置为String,其它类型的值会被设置为Number

通过 ToNumber() 将值转换为数字

参数 结果
undefined NaN
null +0
布尔值 true被转换为 1,false转换为0
数字 无需转换
字符串 由字符串解析为数字。例如:"324"被转换为324

如果输入的值是一个对象,则会首先会调用ToPrimitive(obj, Number)将该对象转换为原始值,然后在调用ToNumber()将这个原始值转换为数字。

通过 ToString() 将值转换为字符串

参数 结果
undefined "undefined"
null "null"
布尔值 "true" 或者 "false"
数字 数字作为字符串,比如。 "1.765"
字符串 无需转换

如果输入的值是一个对象,则会首先会调用ToPrimitive(obj, String)将该对象转换为原始值,然后再调用ToString()将这个原始值转换为字符串。

2. 作用域

参考:https://blog.fundebug.com/2019/03/15/understand-javascript-scope/

ES6 之前

只有全局作用域函数作用域

全局作用域

  • 直接定义的变量和函数
  • 未定义直接赋值的变量
  • window/global对象的属性

函数作用域

在函数内部声明的变量或者内层函数,多层作用域中,内层可以访问到外层变量。

ES6 之后

新增块级作用域。使用关键字let / const 声明。

let / const不能被提升,在一个作用域内只能声明一次

3. 相等性判断

参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Equality_comparisons_and_sameness

ES2015中有四种相等算法:

  • 抽象(非严格)相等比较 (==)
  • 严格相等比较 (===): 用于 Array.prototype.indexOf, Array.prototype.lastIndexOf, 和 case-matching
  • 同值零: 用于 %TypedArray%ArrayBuffer构造函数、以及MapSet操作, 并将用于 ES2016/ES7 中的String.prototype.includes
  • 同值: 用于所有其他地方

JavaScript提供三种不同的值比较操作:

  • 严格相等比较 (也被称作”strict equality”, “identity”, “triple equals”),使用 === ,
  • 抽象相等比较 (“loose equality”,”double equals”) ,使用 ==
  • 以及 Object.is (ECMAScript 2015/ ES6 新特性)

严格相等===

全等操作符比较两个值是否相等,两个被比较的值在比较前都不进行隐式转换。如果两个被比较的值具有不同的类型,这两个值是不全等的。否则,如果两个被比较的值类型相同,值也相同,并且都不是 Number 类型时,两个值全等。最后,如果两个值都是 Number 类型,当两个都不是 NaN,并且数值相同,或是两个值分别为 +0-0 时,两个值被认为是全等的。

在日常中使用全等操作符几乎总是正确的选择。对于除了数值之外的值,全等操作符使用明确的语义进行比较:一个值只与自身全等。对于数值,全等操作符使用略加修改的语义来处理两个特殊情况:第一个情况是,浮点数 0 是不分正负的,全等操作符认为这两个值是全等的。第二个情况是,全等操作符认为 NaN 与其他任何值都不全等,包括它自己。(等式 (x !== x) 成立的唯一情况是 x 的值为 NaN

非严格相等==

一些总结规律:

  • undefinednull 互相相等,与其他值为false
  • NumberString/Boolean比较时,后者将会被转化ToNumber;与对象进行比较时,后者将会被转化ToPrimitive
  • StringNumber/Boolean比较时,均转换ToNumber;与对象比较时,后者将被转换ToPrimitive
  • BooleanNumber/String比较时,与上例相同
  • ObjectNumber/String/Boolean比较时,前者将被转换ToPrimitiveBoolean被转换ToNumber

同值相等

Object.is方法提供

JS 圣经图

JS比较运算

4. 回调函数

参考:https://www.cnblogs.com/lfri/p/11872294.html

回调函数是作为一个参数传递给另一个函数。回调函数可以方便地进行异步处理,因为 JavaScript 是事件驱动的语言。这意味着,JavaScript 不会因为要等待一个响应而停止当前运行,而是在监听其他事件时继续执行。

回调函数有哪些特点?

不会立刻执行

回调函数作为参数传递给一个函数的时候,传递的只是函数的定义并不会立即执行。和普通的函数一样,回调函数在函调用函数数中也要通过()运算符调用才会执行。

是个闭包

回调函数是一个闭包,也就是说它能访问到其外层定义的变量。

执行前类型判断

在执行回调函数前最好确认其是一个函数。

5. use strict

参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Strict_mode

在作用域内顶部使用表达式 "use strict";

  1. 严格模式通过抛出错误来消除了一些原有静默错误

    第一,严格模式下无法再意外创建全局变量。

    第二,严格模式会使引起静默失败的赋值操作抛出异常。例如, NaN 是一个不可写的全局变量。在正常模式下,给 NaN 赋值不会产生任何作用;开发者也不会受到任何错误反馈。但在严格模式下,给 NaN 赋值会抛出一个异常。任何在正常模式下引起静默失败的赋值操作(给不可写属性赋值,给只读属性(getter-only)赋值, 给不可扩展对象(non-extensible object)的新属性赋值)都会抛出异常。

    第三,在严格模式下,试图删除不可删除的属性时会抛出异常。

    第四,在Gecko版本34之前,严格模式要求一个对象内的所有属性名在对象内必须唯一。正常模式下重名属性是允许的,最后一个重名的属性决定其属性值。因为只有最后一个属性起作用,当代码要去改变属性值而不是修改最后一个重名属性的时候,复制这个对象就产生一连串的bug。在严格模式下,重名属性被认为是语法错误。

    第五,严格模式要求函数的参数名唯一。在正常模式下,最后一个重名参数名会掩盖之前的重名参数。

    第六,严格模式禁止八进制数字语法。ECMAScript并不包含八进制语法,但所有的浏览器都支持这种以零(0)开头的八进制语法:0644 === 420 还有 "\045" === "%"。在ECMAScript 6中支持为一个数字加”0o“的前缀来表示八进制数。

    第七,ECMAScript 6中的严格模式禁止设置primitive值的属性。

  2. 严格模式修复了一些导致 JavaScript引擎难以执行优化的缺陷:有时候,相同的代码,严格模式可以比非严格模式下运行得更快

    1. 简化变量的使用

      第一。严格模式禁用 withwith所引起的问题是块内的任何名称可以映射(map)到with传进来的对象的属性, 也可以映射到包围这个块的作用域内的变量(甚至是全局变量),这一切都是在运行时决定的:在代码运行之前是无法得知的。

      第二,严格模式下的 eval 不再为上层范围引入新变量。在严格模式下 eval 仅仅为被运行的代码创建变量,所以 eval 不会使得名称映射到外部变量或者其他局部变量。

      第三,严格模式禁止删除声明变量。delete name 在严格模式下会引起语法错误。

    2. evalarguments变的简单

      第一,名称 evalarguments 不能通过程序语法被绑定(be bound)或赋值。

      第二,严格模式下,参数的值不会随 arguments 对象的值的改变而变化。

      第三,不再支持 arguments.callee

    3. “安全的” JavaScript

      第一,在严格模式下通过this传递给一个函数的值不会被强制转换为一个对象。

      第二,在严格模式中再也不能通过广泛实现的ECMAScript扩展“游走于”JavaScript的栈中。

      第三,严格模式下的arguments不会再提供访问与调用这个函数相关的变量的途径。

  3. 严格模式禁用了在ECMAScript的未来版本中可能会定义的一些语法。

    第一,在严格模式中一部分字符变成了保留的关键字。这些字符包括implements, interface, let, package, private, protected, public, staticyield

    第二,严格模式禁止了不在脚本或者函数层面上的函数声明。

6. null 与 undefined

参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects

null 特指对象的值未设置。它是 JavaScript 基本类型之一,在布尔运算中被认为是falsy

全局属性undefined表示原始值undefined。它是一个JavaScript的 原始数据类型。

扩展

参考:https://developer.mozilla.org/zh-CN/docs/Glossary/Primitive

基本类型(基本数值、基本数据类型)是一种既非对象也无方法的数据。在 JavaScript 中,共有7种基本类型:stringnumberbigintbooleannullundefinedsymbol

7. 判断数字是否为整数

参考:https://www.cnblogs.com/snandy/p/3824828.html

JavaScript中不区分整数和浮点数,所有数字内部都采用64位浮点格式表示,。但实际操作中比如数组索引、位操作则是基于32位整数。

  1. 取余

    1
    2
    3
    const isInt = val => {
    return typeof val === 'number' && val % 1 === 0;
    }
  2. 使用Math内置方法判断

    1
    2
    3
    const isInt = val => {
    return Math.floor(val) === val;
    }
  3. 使用parseInt判断

    1
    2
    3
    4
    const isInt = val => {
    return parseInt(val, 10) === val;
    }
    // 缺陷,val 将被转换成string再判断
  4. 通过位运算

    1
    2
    3
    4
    const isInt = val => {
    return (val | 0) === val;
    }
    // 缺陷:只能判断32位以下数字
  5. 通过新增方法

    1
    Number.isInteger(val);

8. IIFE

参考:https://developer.mozilla.org/zh-CN/docs/Glossary/IIFE

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

1
2
3
(function () {
statements
})();

这是一个被称为 自执行匿名函数 的设计模式,主要包含两部分。第一部分是包围在 圆括号运算符 () 里的一个匿名函数,这个匿名函数拥有独立的词法作用域。这不仅避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域。

第二部分再一次使用 () 创建了一个立即执行函数表达式,JavaScript 引擎到此将直接执行函数。

9. 命名函数与匿名函数

命名函数也叫函数声明function fn() {}

此时存在变量提升,在声明这个函数之前就可以调用。

匿名函数包括函数表达式箭头函数回调函数的用法:

  • const fn = function() {}
  • const fn = () => {}
  • function foo(() => {})

函数表达式没有提升,在定义函数表达式之前不能使用。

10. 私有属性

参考:https://segmentfault.com/a/1190000017081250

  1. 约定实现:使用下划线_开头的属性为私有属性

  2. 使用constructor的闭包实现:

    1
    2
    3
    4
    5
    6
    7
    class Example {
    constructor() {
    var _private = '';
    _private = 'private';
    this.getName = function() {return _private}
    }
    }
  3. 使用IIFE创建闭包和作用域

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const Example = (function() {
    var _private = '';
    class Example {
    constructor() {
    _private = 'private';
    }
    getName() {
    return _private;
    }
    }
    return Example;
    })();
  4. 使用Symbol

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const Example = (function() {
    var _private = Symbol('private');
    class Example {
    constructor() {
    this[_private] = 'private';
    }
    getName() {
    return this[_private];
    }
    }
    return Example;
    })();
  5. WeakMap

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const Example = (function() {
    var _private = new WeakMap(); // 私有成员存储容器
    class Example {
    constructor() {
    _private.set(this, 'private');
    }
    getName() {
    return _private.get(this);
    }
    }
    return Example;
    })();
  6. 使用TypeScript

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Example {
    private _private: string;
    constructor(val: string) {
    this._private = val;
    }
    private getName(): string {
    return this._private;
    }
    }

    实际上也是转换为IIFE并修改原型链:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var Example = /** @class */ (function () {
    function Example(val) {
    this._private = val;
    }
    Example.prototype.getName = function () {
    return this._private;
    };
    return Example;
    }());

11. 原型模式与构造函数模式

参考:

https://zhuanlan.zhihu.com/p/94104346

https://juejin.cn/post/6975111113792978980

从设计模式的角度讲,原型模式是用于创建对象的一种模式。我们不再关心对象的具体类型,而是找到一个对象,然后通过克隆来创建一个一模一样的对象。

ECMAScript 5标准中定义的真正的原型继承,需要使用Object.createObject.create创建一个具有指定原型的对象,并且还可以包含指定的属性,例如Object.create( prototype, optionalDescriptorObjects )

原型模式必须拥有prototype的存在,而构造函数方式不必依赖prototype。

总结提问

1、为什么说 ES6 的 Class 只是语法糖

起源上说,JavaScript是基于原型的面向对象系统。而在原型编程的思想中,类并不是必需的,对象未必需要从类中创建而来, 一个对象是通过克隆另外一个对象所得到的。

ES6的 Class 语法,让 JavaScript 看起来像是一门基于类的语言,但其背后仍是通过原型机制来创建对象。

2、使用了new A() ,所以A属于类吗?

在这里 A 并不是类,而是函数构造器,JavaScript 的函数既可以作为普通函数被调用, 也可以作为构造器被调用。当使用 new 运算符来调用函数时,此时的函数就是一个构造器。

3、存在没有原型的对象吗?

通过设置构造器的 prototype 来实现原型继承的时候,除了根对象 Object.prototype 本身之外,任何对象都会有一个 原型。而通过 Object.create( null )可以创建出没有原型的对象。

12. 字符串同态

参考:https://github.com/kennymkchan/interview-questions-in-javascript

同态:两个字符串,如果A字符串中的每一个字符都可以在B字符串中找到唯一对应,并且顺序一一对应;如果存在这样的函数,那么A和B同态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const isIsomorphic = (firstString, secondString) => {
if (firstString.length !== secondString.length) return false
var letterMap = {};
for (let i = 0; i < firstString.length; i++) {
var letterA = firstString[i],
letterB = secondString[i];
if (letterMap[letterA] === undefined) {
letterMap[letterA] = letterB;
} else if (letterMap[letterA] !== letterB) {
return false;
}
}
return true;
}

13. Transpiling

为了使新标准编写的程序适应低版本的浏览器(或运行时)所进行的对代码进行转译。

可以将不支持的方法重写或者对旧标准上的对象进行补充(polyfill)

14. this的指向

this通常指向当前作用域,在当前作用域找不到对象时,向上级作用域查找。

Function.prototype.callFunction.prototype.apply都可以改变this的指向。

bind()方法会创建一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以创建它时传入 bind()方法的第一个参数作为 this,传入 bind() 方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。

15. map / reduce / filter

参考:https://www.freecodecamp.org/news/javascript-map-reduce-and-filter-explained-with-examples/

  • map:将数组的每一项传入回调函数中,获取返回值

    1
    2
    [1,2,3].map(v => v * v);
    // => [1,4,9]
  • filter:将每个元素传递到回调函数中,获取布尔值,为真则保留元素

    1
    2
    [0, true, false, 1, "str"].filter(v => v);
    // => [true, 1, 'str']
  • reduce:递归调用每一个元素,回调函数有三个参数:

    • accumulator - 上一次迭代的返回值
    • currentValue - 数组中的当前项
    • index - 当前项目的索引
    • array - 调用 reduce 的原始数组
    • initialValue参数是可选的。如果提供,它将在第一次调用回调函数时用作初始累加器值。
    1
    2
    3
    4
    [1,2,3].reduce((res, curr, idx) => {
    return res + curr + idx;
    });
    // => 9
打赏
  • 版权声明: 本博客采用 Apache License 2.0 许可协议。
    转载请注明出处: https://ryzenx.com/2022/03/JS-questions-2203/

谢谢你的喜欢~

支付宝
微信