前端面试必备:JS 基础知识点大揭秘

5 阅读7分钟

搞前端开发面试的时候,JavaScript 那可是重中之重,面试官肯定会问一些 JS 知识。今天就给大家好好唠唠 JS 面试里常见的考点,希望能帮大家在面试里顺顺利利,脱颖而出。


一、数据类型

类型分类

JavaScript 的数据类型分为 原始类型引用类型

  • 原始类型:number、string、boolean、undefined、null(ES6 之前 5 种)、symbol、bigint。
  • 引用类型:object、function、array、Date(ES6 新增)、Map、Set、RegExp 等。

类型判断

1. typeof

  • 优点:快速判断原始类型(如 numberstring 等)。
  • 缺点
    • typeof null 返回 object(错误)。
    • 对引用类型,除 function 外均返回 object
console.log(typeof 'hello'); // string
console.log(typeof {}); // object
console.log(typeof null); // object ❌

2. instanceof`

判断对象是否是某个构造函数的实例:

console.log([] instanceof Object); // true
console.log(new Date() instanceof Date); // true

3. Object.prototype.toString()

通过 [[Class]] 内部属性精准判断类型:

console.log(Object.prototype.toString.call([])); // [object Array]
console.log(Object.prototype.toString.call(null)); // [object Null]
console.log(Object.prototype.toString.call(/abc/)); // [object RegExp]
  1. 对象上的 toString() 返回 '[object 变量类型]'格式的字符串
  2. 数组上的 toString() 返回 数组元素以逗号拼接的字符串
  3. 其他类型的 toString() 返回 字符串字面量

类型转换

显式转换

使用 Number()String()Boolean() 等方法:

console.log(Number('123')); // 123
console.log(String(456)); // "456"

隐式转换

在运算符或条件判断中自动触发:+ - * / == != ! > < >= <= if while

1. 原始类型 -> 原始类型

console.log(1 + '2'); // "12"(数字转字符串)
console.log(true && 123); // 123(布尔值转数字)

2. 引用类型 -> 原始类型

  • (对象 ==> 数字 ToNumber({})) 或者 (对象 ==> 字符串 ToString({})
    1. ToNumberToNumber 只能处理原始类型
    2. 当它们遇到引用类型内部执行 ToPrimitive({})

ToPrimitive 规则

当需要将对象转换为原始值时,按以下步骤执行:

  • 转换为数字

    1. 调用对象的 valueOf(),若返回原始值则结束。
    2. 调用对象的 toString(),若返回原始值则结束。
    3. 抛出错误。
  • 转换为字符串

    1. 调用对象的 toString(),若返回原始值则结束。
    2. 调用对象的 valueOf(),若返回原始值则结束。
    3. 抛出错误。
const obj = {
  valueOf() { return 123; },
  toString() { return 'abc'; }
};
console.log(+obj); // 123(转数字调用 valueOf)
console.log(obj + ''); // "abc"(转字符串调用 toString)

二、this 指向

默认绑定

函数独立调用时:

  • 浏览器环境指向 window,Node 环境指向 undefined
var a = 1;
function foo() {
    console.log(this.a);
}
foo(); // 浏览器输出 1,Node 输出 undefined

隐式绑定

函数被对象调用时,this 指向该对象:

let obj = {
    a: 2,
    fn: foo
};
obj.fn(); // 输出 2

隐式丢失

当函数被一连串对象调用时,this 指向最近的调用对象:

const obj = {
  fn: function() {
    return function() {
      console.log(this); // window(独立调用时)
    };
  }
};
obj.fn()(); // this 指向 window,而非 obj

显式绑定

通过 callapplybind 明确指定 this

let obj3 = {
    a: 2
};
function foo() {
    console.log(this.a);
}
foo.call(obj3); // 输出2

callapplybind 的区别

  • call:接收参数列表,直接调用函数。
  • apply:接收参数数组,直接调用函数。
  • bind:返回新函数,新函数可接收额外参数:
function foo(a, b) {
  console.log(this, a, b);
}
const obj = { name: 'obj' };
foo.call(obj, 1, 2); // obj 1 2
foo.apply(obj, [3, 4]); // obj 3 4
const bound = foo.bind(obj);
bound(5, 6); // obj 5 6

三、== vs ===

== 的隐式转换

会尝试类型转换后再比较:

console.log(1 == '1'); // true(数字转字符串)
console.log('' == 0); // true(空字符串转数字为 0)
console.log([1] == '1'); // true(数组转字符串为 "1")

=== 的严格比较

直接比较值和类型:

console.log(1 === '1'); // false(类型不同)
console.log({} === ![]); // false(对象与布尔值比较)

四、闭包

定义

根据词法作用的规则,内部函数总是可以访问其外部函数中的变量的。 在函数执行完毕后,它的执行上下文对象要被销毁

当函数 A 内部声明了一个函数 B,函数 B 被拿到 A 的外部调用,为了同时满足以上两个规则,函数 A 的执行执行上下文被销毁后会遗留下来一个对象空间,用于存放函数 B 有权访问的所有变量。这个对象空间被称为闭包。

function foo() {
    let a = 1;
    return function () {
        console.log(a);
    };
}
const bar = foo();
bar(); // 输出1

风险

闭包可能导致内存泄漏:

function createCounter() {
  let count = 0;
  return function() {
    return ++count; // 闭包保留 count 的引用
  };
}
const counter = createCounter();
// 若 counter 长期存在,count 不会被回收

五、拷贝

浅拷贝

新对象受原对象影响:

let obj = { a: 1, b: { n: 2 } };
let newObj = Object.assign({}, obj);
obj.b.n = 200;
console.log(newObj.b.n); // 200

常用方法

  1. Object.assign()

用于将一个或多个源对象的所有可枚举属性复制到目标对象。它会返回目标对象。

let originalObj = { a: 1, b: { c: 2 } };
let copiedObj = Object.assign({}, originalObj);

// 修改原对象的嵌套对象属性
originalObj.b.c = 3;

console.log(copiedObj.b.c); // 输出: 3,说明浅拷贝受原对象影响
  1. Object.create()

创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__

let original = { x: 10, y: { z: 20 } };
let copy = Object.create(Object.getPrototypeOf(original), Object.getOwnPropertyDescriptors(original));

// 修改原对象的嵌套对象属性
original.y.z = 25;

console.log(copy.y.z); // 输出: 25,浅拷贝受原对象影响
  1. 展开运算符 ...

将一个可迭代对象(如数组或对象)展开为其各个元素。

let originalArr = [1, { value: 2 }];
let copiedArr = [...originalArr];

// 修改原数组中嵌套对象的属性
originalArr[1].value = 3;

console.log(copiedArr[1].value); // 输出: 3,浅拷贝受原对象影响

let originalObj = { a: 4, b: { c: 5 } };
let copiedObj = { ...originalObj };

// 修改原对象的嵌套对象属性
originalObj.b.c = 6;

console.log(copiedObj.b.c); // 输出: 6,浅拷贝受原对象影响
  1. Array.prototype.slice()

slice() 方法返回一个新的数组对象,这一对象是一个由 begin 和 end 决定的原数组的浅拷贝(包括 begin,不包括 end)。原始数组不会被改变。

let originalArray = [1, { num: 2 }];
let slicedArray = originalArray.slice();

// 修改原数组中嵌套对象的属性
originalArray[1].num = 4;

console.log(slicedArray[1].num); // 输出: 4,浅拷贝受原对象影响
  1. Array.prototype.concat()

用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。

let arr1 = [1, { value: 2 }];
let arr2 = [3];
let combinedArray = arr1.concat(arr2);

// 修改原数组中嵌套对象的属性
arr1[1].value = 5;

console.log(combinedArray[1].value); // 输出: 5,浅拷贝受原对象影响

深拷贝

新对象不受原对象影响:

  1. JSON.parse
  2. 递归拷贝
  3. structuredClone
// 方法 1:JSON.parse(不支持 symbol、bigint)
const newObj = JSON.parse(JSON.stringify(obj));

// 方法 2:递归拷贝
function deepClone(obj) {
  if (typeof obj !== 'object' || obj === null) return obj;
  const clone = Array.isArray(obj) ? [] : {};
  for (const key in obj) {
    clone[key] = deepClone(obj[key]);
  }
  return clone;
}

// 方法 3:structuredClone(兼容性需注意)
const newObj = structuredClone(obj);

六、原型与继承

原型链

每个对象都有 __proto__(隐式原型),指向其构造函数的 prototype(显式原型)。属性查找遵循 __proto__ 链,直到 null

function Parent() {}
Parent.prototype.say = function() { console.log('Hello'); };
const child = Object.create(Parent.prototype);
child.__proto__ === Parent.prototype; // true
child.say(); // "Hello"

new 的实现

function myNew(constructor, ...args) {
  const obj = Object.create(constructor.prototype);
  const result = constructor.apply(obj, args); //调整 this 指向
  return result instanceof Object ? result : obj;
}

继承方式

1. 原型链继承:

  • 子类无法给父类传参。
  • 示例:
    function Parent(name) { this.name = name; }
    Parent.prototype.say = function() { console.log('Hello'); };
    Child.prototype = new Parent();
    

2. 借用构造函数继承:

  • 无法继承父类的原型方法。
  • 示例:
    function Child(name) { Parent.call(this, name); }
    

3. 组合继承:

  • 父类构造函数执行两次(父类实例化两次)。
  • 示例:
    Child.prototype = new Parent(); // 父类第一次执行
    function Child(name) { Parent.call(this, name); } // 父类第二次执行
    

4. 寄生组合继承:

  • 通过 Object.create 避免父类重复执行。
  • 示例:
    function inheritPrototype(subType, superType) {
      const prototype = Object.create(superType.prototype);
      prototype.constructor = subType;
      subType.prototype = prototype;
    }
    inheritPrototype(Child, Parent);
    

5. ES6 类继承:

  • 使用 extendssuper()
    class Child extends Parent {
      constructor(name) {
        super(name); // 调用父类构造函数
        this.sex = 'male';
      }
    }
    

七、varletconst

关键区别

特性varlet / const
块级作用域
声明提升无(存在暂时性死区)
重复声明允许不允许
变量提升变量和值都提升变量提升,值为 undefined
全局变量挂载挂载到 window不挂载

暂时性死区

在块级作用域内,变量声明前访问会报错:

if (true) {
  console.log(temp); // ReferenceError(暂时性死区)
  let temp = 123;
}

八、常见问题

0.1 + 0.2 的精度丢失

console.log(0.1 + 0.2); // 0.30000000000000004
// 解决方案:使用整数运算或第三方库(如 decimal.js)

超过 JS 最大安全数

console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
// 处理大数:使用 BigInt 或字符串拼接
const bigInt = 4856315546156412n + 15546156412n; // 使用 BigInt

通过以上内容,你已经掌握了 JS 面试的基础知识点。祝你面试顺利,斩获心仪的 offer!