一、数据类型与类型转换
1. JavaScript有哪些数据类型?它们之间有什么区别?
基本数据类型(7种):
Number:数字类型,包括整数和浮点数String:字符串类型Boolean:布尔类型,true/falseUndefined:未定义Null:空值Symbol(ES6):唯一标识符BigInt(ES2020):大整数
引用数据类型:
Object:对象,包括普通对象、数组、函数、日期等
核心区别:
- 存储方式:基本类型存储在栈内存,引用类型存储在堆内存(栈中存储引用地址)
- 赋值方式:基本类型赋值传值,引用类型赋值传引用
- 比较方式:基本类型比较值,引用类型比较引用地址
javascript
// 基本类型
let a = 10;
let b = a;
b = 20;
console.log(a); // 10,a不受影响
// 引用类型
let obj1 = { name: 'Alice' };
let obj2 = obj1;
obj2.name = 'Bob';
console.log(obj1.name); // 'Bob',obj1被修改
2. 如何准确判断数据类型?
方法对比:
javascript
// 1. typeof - 适合基本类型(除null)
typeof 123 // 'number'
typeof 'abc' // 'string'
typeof true // 'boolean'
typeof undefined // 'undefined'
typeof Symbol() // 'symbol'
typeof null // 'object' ⚠️ 历史遗留问题
typeof [] // 'object' ⚠️ 无法区分数组和对象
// 2. instanceof - 检测引用类型的原型链
[] instanceof Array // true
[] instanceof Object // true
({}) instanceof Object // true
// 3. Object.prototype.toString - 最准确的方法
Object.prototype.toString.call(123) // '[object Number]'
Object.prototype.toString.call([]) // '[object Array]'
Object.prototype.toString.call(null) // '[object Null]'
Object.prototype.toString.call(new Date()) // '[object Date]'
// 4. 封装通用类型检测函数
function getType(value) {
return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
}
getType([]) // 'array'
getType(null) // 'null'
getType(new Map()) // 'map'
3. 说明隐式类型转换的规则
转换规则:
javascript
// 1. 转Boolean - 假值只有7个
Boolean(0) // false
Boolean('') // false
Boolean(null) // false
Boolean(undefined) // false
Boolean(NaN) // false
Boolean(false) // false
Boolean(document.all) // false(历史遗留)
// 2. 转Number
Number('123') // 123
Number('12.5') // 12.5
Number('') // 0
Number('abc') // NaN
Number(true) // 1
Number(false) // 0
Number(null) // 0
Number(undefined) // NaN
// 3. 转String
String(123) // '123'
String(true) // 'true'
String(null) // 'null'
String(undefined) // 'undefined'
// 4. 复杂情况 - ToPrimitive
// 对象转基本类型:先调用valueOf,再调用toString
let obj = {
valueOf() { return 1; },
toString() { return '2'; }
};
console.log(obj + 1); // 2(调用valueOf)
console.log(`${obj}`); // '2'(调用toString)
// 5. == 运算符的转换规则
null == undefined // true
'0' == 0 // true(字符串转数字)
0 == false // true(布尔转数字)
'' == false // true
[] == false // true(对象转基本类型)
[] == ![] // true(复杂转换)
二、作用域与闭包
4. 解释JavaScript的作用域链
概念: 作用域链是变量查找的机制,从当前作用域逐级向外查找,直到全局作用域。
javascript
let global = 'global';
function outer() {
let outerVar = 'outer';
function inner() {
let innerVar = 'inner';
// 作用域链:inner -> outer -> global
console.log(innerVar); // 'inner' - 当前作用域
console.log(outerVar); // 'outer' - 父作用域
console.log(global); // 'global' - 全局作用域
}
inner();
}
outer();
特点:
- 静态作用域(词法作用域):函数作用域在定义时确定,不是调用时
- 变量查找顺序:当前作用域 → 外层作用域 → ... → 全局作用域
- 查找终止:找到变量或到达全局作用域
5. 什么是闭包?闭包的应用场景有哪些?
定义: 闭包是指有权访问另一个函数作用域中变量的函数。
javascript
// 基本闭包示例
function createCounter() {
let count = 0; // 私有变量
return {
increment() {
return ++count;
},
decrement() {
return --count;
},
getCount() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
应用场景:
- 数据私有化和封装
javascript
function createPerson(name) {
let _name = name; // 私有变量
return {
getName() { return _name; },
setName(newName) { _name = newName; }
};
}
- 函数柯里化
javascript
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function(...nextArgs) {
return curried.apply(this, args.concat(nextArgs));
};
};
}
const sum = (a, b, c) => a + b + c;
const curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3)); // 6
- 防抖和节流
javascript
function debounce(fn, delay) {
let timer = null; // 闭包保存定时器
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
- 模块化模式
javascript
const Calculator = (function() {
let result = 0; // 私有变量
return {
add(num) { result += num; return this; },
subtract(num) { result -= num; return this; },
getResult() { return result; }
};
})();
注意事项:
- 闭包会使变量驻留在内存中,可能导致内存泄漏
- 及时释放不需要的闭包引用
6. var、let、const的区别
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
| 变量提升 | 有(值为undefined) | 有(暂时性死区) | 有(暂时性死区) |
| 重复声明 | 允许 | 不允许 | 不允许 |
| 重新赋值 | 允许 | 允许 | 不允许 |
| 全局对象属性 | 是 | 否 | 否 |
javascript
// 1. 作用域差异
if (true) {
var a = 1;
let b = 2;
const c = 3;
}
console.log(a); // 1
console.log(b); // ReferenceError
console.log(c); // ReferenceError
// 2. 变量提升
console.log(x); // undefined
var x = 5;
console.log(y); // ReferenceError(暂时性死区)
let y = 5;
// 3. 循环中的经典问题
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 3 3 3
}
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 100); // 0 1 2
}
// 4. const特性
const obj = { name: 'Alice' };
obj.name = 'Bob'; // ✅ 允许修改属性
obj = {}; // ❌ TypeError,不能重新赋值
// 冻结对象
const frozenObj = Object.freeze({ name: 'Alice' });
frozenObj.name = 'Bob'; // 严格模式下报错,非严格模式静默失败
三、this指向与调用
7. JavaScript中this的指向规则
四种绑定规则(优先级从低到高):
javascript
// 1. 默认绑定 - 独立函数调用
function foo() {
console.log(this); // 非严格模式:window,严格模式:undefined
}
foo();
// 2. 隐式绑定 - 对象方法调用
const obj = {
name: 'Alice',
sayName() {
console.log(this.name);
}
};
obj.sayName(); // 'Alice',this指向obj
// 隐式绑定丢失
const fn = obj.sayName;
fn(); // undefined,this指向window/undefined
// 3. 显式绑定 - call/apply/bind
function greet(greeting, punctuation) {
console.log(greeting + ', ' + this.name + punctuation);
}
const person = { name: 'Bob' };
greet.call(person, 'Hello', '!'); // 'Hello, Bob!'
greet.apply(person, ['Hi', '?']); // 'Hi, Bob?'
const boundGreet = greet.bind(person, 'Hey');
boundGreet('!!!'); // 'Hey, Bob!!!'
// 4. new绑定 - 构造函数
function Person(name) {
this.name = name;
}
const p = new Person('Charlie');
console.log(p.name); // 'Charlie'
// 5. 箭头函数 - 捕获外层this
const obj2 = {
name: 'David',
sayName() {
setTimeout(() => {
console.log(this.name); // 'David',继承外层this
}, 100);
}
};
obj2.sayName();
优先级: new绑定 > 显式绑定 > 隐式绑定 > 默认绑定
8. 手写实现call、apply、bind
javascript
// 实现call
Function.prototype.myCall = function(context, ...args) {
// 处理context为null/undefined的情况
context = context || window;
// 创建唯一属性,避免覆盖原有属性
const fnSymbol = Symbol();
context[fnSymbol] = this;
// 执行函数
const result = context[fnSymbol](...args);
// 删除临时属性
delete context[fnSymbol];
return result;
};
// 实现apply
Function.prototype.myApply = function(context, argsArray) {
context = context || window;
const fnSymbol = Symbol();
context[fnSymbol] = this;
// apply接收数组参数
const result = argsArray
? context[fnSymbol](...argsArray)
: context[fnSymbol]();
delete context[fnSymbol];
return result;
};
// 实现bind
Function.prototype.myBind = function(context, ...args) {
const fn = this;
return function boundFn(...newArgs) {
// 判断是否被new调用
if (this instanceof boundFn) {
return new fn(...args, ...newArgs);
}
return fn.apply(context, args.concat(newArgs));
};
};
// 测试
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
const person = { name: 'Alice' };
greet.myCall(person, 'Hello', '!'); // 'Hello, Alice!'
greet.myApply(person, ['Hi', '?']); // 'Hi, Alice?'
const boundGreet = greet.myBind(person, 'Hey');
boundGreet('!!!'); // 'Hey, Alice!!!'
四、原型与继承
9. 解释原型链机制
核心概念:
- 每个对象都有一个
__proto__属性指向其原型对象 - 每个构造函数都有一个
prototype属性 - 原型链:对象 → 构造函数.prototype → Object.prototype → null
javascript
function Person(name) {
this.name = name;
}
Person.prototype.sayName = function() {
console.log(this.name);
};
const alice = new Person('Alice');
// 原型链关系
console.log(alice.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null
// 属性查找
alice.sayName(); // 查找顺序:alice -> Person.prototype -> Object.prototype
alice.toString(); // 最终在Object.prototype找到
关键属性:
javascript
// constructor属性
console.log(Person.prototype.constructor === Person); // true
console.log(alice.constructor === Person); // true(继承自原型)
// instanceof原理:检查原型链
console.log(alice instanceof Person); // true
console.log(alice instanceof Object); // true
10. JavaScript实现继承的多种方式
javascript
// 1. 原型链继承
function Parent() {
this.name = 'parent';
this.hobbies = ['reading'];
}
Parent.prototype.getName = function() {
return this.name;
};
function Child() {}
Child.prototype = new Parent();
// 缺点:引用类型共享,无法传参
const child1 = new Child();
const child2 = new Child();
child1.hobbies.push('gaming');
console.log(child2.hobbies); // ['reading', 'gaming']
// 2. 构造函数继承
function Parent(name) {
this.name = name;
this.hobbies = ['reading'];
}
function Child(name, age) {
Parent.call(this, name); // 调用父类构造函数
this.age = age;
}
// 优点:可传参,引用类型独立
// 缺点:无法继承原型方法,函数无法复用
// 3. 组合继承(最常用)
function Parent(name) {
this.name = name;
this.hobbies = ['reading'];
}
Parent.prototype.getName = function() {
return this.name;
};
function Child(name, age) {
Parent.call(this, name); // 第二次调用
this.age = age;
}
Child.prototype = new Parent(); // 第一次调用
Child.prototype.constructor = Child;
// 缺点:调用两次父类构造函数
// 4. 寄生组合继承(最优方案)
function Parent(name) {
this.name = name;
this.hobbies = ['reading'];
}
Parent.prototype.getName = function() {
return this.name;
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
// 创建中间对象,避免调用父类构造函数
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
// 5. ES6 Class继承(语法糖)
class Parent {
constructor(name) {
this.name = name;
this.hobbies = ['reading'];
}
getName() {
return this.name;
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 必须先调用super
this.age = age;
}
getAge() {
return this.age;
}
}
const child = new Child('Alice', 18);
console.log(child.getName()); // 'Alice'
继承方式对比:
| 方式 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|
| 原型链继承 | 简单 | 引用类型共享 | ⭐ |
| 构造函数继承 | 可传参、引用独立 | 无法继承原型 | ⭐⭐ |
| 组合继承 | 结合前两者优点 | 调用两次构造函数 | ⭐⭐⭐ |
| 寄生组合继承 | 完美解决组合继承问题 | 实现复杂 | ⭐⭐⭐⭐⭐ |
| ES6 Class | 语法简洁、易读 | 需要编译 | ⭐⭐⭐⭐⭐ |