前端八股文JS基础篇上
目录
一、JS的数据类型+区别
1. 基本数据类型
Number
String
Boolean
Null
Undefined
Symbol
(ES6新增,代表唯一标识符)BigInt
(ES10新增,用于处理高精度整数)
特性:
- 直接存储在栈内存中,占用固定大小的内存空间。
- 赋值时是值的复制,修改一个变量不会影响另一个变量。
2. 引用数据类型
Object
(包括普通对象、数组、函数、日期等)Array
(本质是Object
的子类型)Date
Function
(特殊的对象,可被调用)RegExp
(正则表达式对象)
特性:
- 栈内存存储对象的引用地址,堆内存存储对象的实际数据。
- 赋值时是引用的复制,多个变量指向同一对象,修改会相互影响。
3. 核心区别
维度 | 基本数据类型 | 引用数据类型 |
---|---|---|
存储位置 | 栈内存 | 栈内存存地址,堆内存存数据 |
赋值/传递行为 | 值复制 | 引用复制(指向同一堆内存数据) |
比较方式 | 值比较(=== 判断值是否相等) | 引用比较(=== 判断地址是否相同) |
内存占用 | 固定大小 | 动态大小(由对象属性数量决定) |
二、判断数据类型的6种方法
1. typeof
操作符
返回值:'number'
、'string'
、'boolean'
、'object'
、'function'
、'undefined'
、'symbol'
、'bigint'
。
局限性:
typeof null
返回'object'
(是历史遗留问题)。- 无法区分具体的引用类型(如数组、普通对象均返回
'object'
)。 - 对基本类型的包装对象(如
new String('a')
)返回'object'
。
console.log(typeof NaN); // 'number'(特殊数值)
console.log(typeof []); // 'object'
2. instanceof
运算符
原理:检查对象的原型链是否包含构造函数的prototype
。
注意:
- 基本数据类型需转换为包装对象才能使用(如
(123).instanceof Number
)。 - 跨框架/窗口环境可能失效(不同窗口的构造函数不同)。
console.log([] instanceof Array); // true
console.log(function(){} instanceof Function); // true
3. Array.isArray()
方法
优势:跨环境可靠,专门用于判断数组。
console.log(Array.isArray([1, 2])); // true
4. Object.prototype.toString.call()
最通用的方法,返回格式为[object Type]
的字符串,可精确区分:
- 基本类型(包括
Null
、Undefined
) - 引用类型(数组、函数、日期、正则等)
- 包装对象(如
new String
与普通字符串均返回[object String]
,需额外逻辑区分)
console.log(Object.prototype.toString.call(null)); // '[object Null]'
console.log(Object.prototype.toString.call(/\d/)); // '[object RegExp]'
缺点补充:
- 自定义类实例:若未定义
Symbol.toStringTag
,统一返回[object Object]
。 - 宿主对象:浏览器环境的
window
、document
等可能返回非标准标签(如[object Window]
)。 - 包装对象混淆:
new String('a')
与普通字符串'a'
的标签相同,需结合typeof
进一步判断。
5. constructor
属性
原理:对象的constructor
指向其构造函数(如[].constructor === Array
)。
风险:
- 构造函数的原型可能被修改(如
Array.prototype = {}
会导致constructor
指向错误)。 - 基本数据类型的包装对象(如
new Number(1)
)的constructor
可被篡改。
6. Symbol.toStringTag
自定义标签
用法:通过在对象/类中定义Symbol.toStringTag
属性,自定义toString.call()
的返回值。
class MyClass {
[Symbol.toStringTag] = 'MyClass';
}
console.log(Object.prototype.toString.call(new MyClass())); // '[object MyClass]'
三、作用域与作用域链
1. 作用域分类
- 全局作用域:代码最外层的作用域,全局变量/函数在此定义。
- 函数作用域(ES5及之前):函数内部形成独立作用域,
var
声明的变量在此作用域内有效。 - 块级作用域(ES6+):
let
/const
在{}
块内形成作用域(如if
、for
块)。
2. 作用域链
- 查找规则:当访问变量/函数时,从当前作用域逐级向上查找,直到全局作用域(若未找到则报错
ReferenceError
)。 - 示例:
const a = 1; function fn() { const b = 2; function inner() { const c = 3; console.log(a); // 查找顺序:inner作用域 → fn作用域 → 全局作用域 } }
四、闭包
1. 核心概念
- 定义:函数内部返回一个函数,使得内部变量能被外部访问,形成“私有作用域”。
- 形成条件:
- 函数嵌套;
- 内部函数引用外部函数的变量;
- 外部函数返回内部函数。
2. 核心作用
- 数据保护:避免全局变量污染(如模块模式
(function(){})()
)。 - 状态保存:
function counter() { let count = 0; return function() { count++; console.log(count); }; } const add = counter(); add(); // 1,闭包保存了count的状态
- 解决循环变量问题:
for (let i = 0; i < 3; i++) { // 块级作用域,每次循环创建独立的i setTimeout(() => console.log(i), 0); // 输出 0, 1, 2 } // 若用var,需通过闭包包裹: for (var i = 0; i < 3; i++) { (function(j) { setTimeout(() => console.log(j), 0); // 闭包保存j的值 })(i); }
3. 缺点与内存管理
- 内存泄露风险:闭包长期持有变量引用,若不再使用需手动释放(如设置为
null
)。 - 性能影响:过度使用闭包会增加内存开销,需合理设计。
五、JS获取DOM元素
1. 传统方法
getElementById(id)
:返回单个元素(唯一,效率最高)。getElementsByClassName(className)
:返回HTMLCollection
(动态集合,实时更新)。getElementsByTagName(tagName)
:返回HTMLCollection
(匹配所有子元素)。
2. CSS选择器方法
querySelector(selector)
:返回第一个匹配的元素(静态,返回Element
)。querySelectorAll(selector)
:返回所有匹配的元素(静态,返回NodeList
,可遍历)。
注意:
- 传统方法返回的
HTMLCollection
是动态的,修改DOM会实时更新; querySelectorAll
返回静态NodeList
,性能更优(尤其在多次遍历时)。
六、原型与原型链
1. 核心概念
- 显式原型(
prototype
):每个函数都有prototype
属性,指向原型对象(默认包含constructor
属性)。 - 隐式原型(
__proto__
):每个对象(除了null
)都有__proto__
,指向其构造函数的prototype
。 - 原型链:对象查找属性时,若自身不存在,则沿
__proto__
向上查找,直到Object.prototype
(其__proto__
为null
)。
2. 关键关系
const obj = {};
obj.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true
const arr = [];
arr.__proto__ === Array.prototype; // true
Array.prototype.__proto__ === Object.prototype; // true
七、call
、apply
、bind
的区别
方法 | 参数形式 | 执行时机 | 返回值 | 特性 |
---|---|---|---|---|
call | 逐个参数(thisArg, arg1, arg2... ) | 立即执行 | 函数执行结果 | 适合动态传参 |
apply | 数组/类数组(thisArg, [argsArray] ) | 立即执行 | 函数执行结果 | 适合处理数组参数 |
bind | 逐个参数(同call ) | 返回新函数,需手动调用 | 新函数(绑定this ) | 用于预先绑定上下文,延迟执行 |
示例:
function fn(a, b) { console.log(this.name, a, b); }
const obj = { name: 'obj' };
fn.call(obj, 1, 2); // 立即执行,输出 'obj 1 2'
fn.apply(obj, [1, 2]); // 同上
const boundFn = fn.bind(obj, 1, 2); // 返回新函数
boundFn(); // 手动调用,输出 'obj 1 2'
八、继承方式对比
1. 常见继承方案
方式 | 核心实现 | 优点 | 缺点 |
---|---|---|---|
原型继承 | Sub.prototype = new Super() | 简单,共享父类方法 | 无法向父类构造函数传参,引用属性共享 |
构造函数继承 | Super.call(this, args) | 可传参,避免引用属性共享 | 无法继承父类原型方法 |
组合继承 | 结合前两者 | 兼顾传参与原型方法继承 | 父类构造函数被调用两次,性能损耗 |
寄生组合继承 | 优化组合继承,避免重复调用父类 | 高效,接近完美 | 代码稍复杂 |
ES6 extends | class Sub extends Super | 语法简洁,内置super机制 | 依赖ES6环境 |
2. ES6继承关键点
- 子类
constructor
中必须调用super()
才能使用this
。 super
可用于调用父类方法(如super.method()
)。
九、内存泄露与垃圾回收
1. 内存泄露场景
- 意外的全局变量:未声明直接赋值(如
a = 1
,隐式创建全局变量)。 - 未清除的定时器/回调:
setInterval
未调用clearInterval
。 - DOM引用未释放:内存中保存DOM元素引用,但DOM已从页面移除。
- 闭包过度使用:闭包长期持有不必要的变量引用。
2. 垃圾回收机制
- 标记清除算法(现代浏览器主流):
- 标记活动对象(被引用的对象);
- 清除未标记的对象(释放内存)。
- 引用计数算法(早期,存在循环引用问题):
变量引用计数为0时释放内存,但a.b = b.a = {}
会导致循环引用无法回收(需手动置空a.b = b.a = null
)。
十、变量提升与暂时性死区
1. 提升规则
声明方式 | 提升内容 | 初始化时机 | 能否重复声明 |
---|---|---|---|
var | 声明+初始化(undefined ) | 进入作用域时 | 允许 |
let /const | 仅声明(无初始化) | 代码执行到声明处 | 不允许(同一作用域内) |
2. 暂时性死区(TDZ)
- 在块级作用域中,
let
/const
声明的变量在声明前处于TDZ,访问会报错ReferenceError
。
if (true) {
console.log(x); // ReferenceError(x在TDZ中)
let x = 1;
}
十一、箭头函数 vs 普通函数
特性 | 箭头函数 | 普通函数 |
---|---|---|
this 指向 | 继承外层作用域的this (静态绑定) | 动态绑定(调用时由上下文决定) |
prototype 属性 | 无 | 有(指向原型对象) |
arguments 对象 | 无(需用...rest 参数) | 有(类数组对象) |
作为构造函数 | 不可(调用报错) | 可(需用new ) |
super 关键字 | 继承外层作用域的super | 指向父类原型(需配合extends ) |
new.target | 无 | 指向构造函数(new 调用时) |
典型场景:
- 箭头函数适合定义回调函数(避免
this
指向混乱,如setTimeout
、数组方法map/filter
)。 - 普通函数适合需要动态
this
或作为构造函数的场景。