JavaScript 函数与对象:从入门到进阶的核心知识点详解
如果你正在学习 JavaScript,或者想要系统性地提升自己的前端开发能力,那么这篇文章绝对值得你认真阅读。我们将深入探讨 JavaScript 中两个最重要的概念------函数和对象的高级特性,这些知识点在实际开发中无处不在。
一、引言
JavaScript 作为一门「面向对象」且「函数式编程」的语言,函数和对象是它的两大基石。几乎所有的前端框架(Vue、React、Angular)都离不开对这两个概念的深入运用。很多开发者在学习初期只是掌握了基本的语法,但当项目复杂度提升时,往往会发现自己的知识储备不够用。
这篇文章,我们将从「函数的增强知识」和「对象的增强知识」两个维度,系统梳理那些面试中常考、开发中常用,但容易被忽视的核心知识点。
二、函数的增强知识
2.1 函数对象:函数也是对象
在 JavaScript 中,函数是一等公民。这意味着函数本身也是对象,拥有自己的属性和方法。
javascript
function greet(name) {
return `Hello, ${name}!`;
}
console.log(greet.length); // 1 ------ 形参个数
console.log(greet.name); // "greet" ------ 函数名
console.log(greet.prototype); // { constructor: f } ------ 原型对象
console.log(typeof greet.toString); // "function" ------ 方法
理解这一点非常重要:你可以把函数当作普通的对象来传递、赋值、作为参数或返回值。这为 JavaScript 的高阶函数编程奠定了基础。
2.2 arguments 对象:灵活的参数容器
在 ES6 之前,arguments 对象是获取函数所有实参的唯一方式:
javascript
function sum() {
let total = 0;
for (let i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}
console.log(sum(1, 2, 3, 4, 5)); // 15
arguments 的特点:
- 是一个类数组对象(Array-like Object),不是真正的数组
- 包含所有传入的参数
- 在箭头函数中不可用(箭头函数没有自己的 arguments)
2.3 剩余参数(Rest Parameters):现代解决方案
ES6 引入了更优雅的 ...rest 语法来解决参数收集问题:
javascript
function sum(...numbers) {
return numbers.reduce((acc, curr) => acc + curr, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // 15
剩余参数 vs arguments:
| 特性 | Rest Parameters | arguments |
|---|---|---|
| 类型 | 真正的数组 | 类数组对象 |
| 箭头函数可用 | ✅ | ❌ |
| 可选参数 | ✅(可在中间使用) | ❌(只能放在最后) |
| ES6+ 推荐 | ✅ | ❌ |
2.4 函数的 prototype:继承的基石
每个函数都有一个 prototype 属性,它指向一个包含 constructor 属性的对象。当使用 new 关键字创建实例时,实例会继承 prototype 上的属性和方法。
javascript
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function() {
return `Hi, I'm ${this.name}`;
};
const alice = new Person('Alice', 25);
console.log(alice.sayHello()); // "Hi, I'm Alice"
console.log(alice instanceof Person); // true
2.5 改变 this 的指向:call、apply、bind
这是 JavaScript 中最核心也最容易混淆的知识点之一。
call 方法
call() 方法调用一个函数,并指定 this 的值和单独给出的参数。
javascript
const person1 = { name: 'Alice' };
const person2 = { name: 'Bob' };
function greet(greeting, punctuation) {
return `${greeting}, I'm ${this.name}${punctuation}`;
}
console.log(greet.call(person1, 'Hello', '!')); // "Hello, I'm Alice!"
console.log(greet.call(person2, 'Hi', '~')); // "Hi, I'm Bob~"
apply 方法
apply() 与 call() 类似,但参数以数组形式传递。
javascript
const numbers = [3, 1, 4, 1, 5, 9, 2, 6];
console.log(Math.max.apply(null, numbers)); // 9
console.log(Math.min.apply(null, numbers)); // 1
bind 方法
bind() 方法创建一个新的函数,当被调用时,其 this 关键字指向指定的值。
javascript
const module = {
x: 42,
getX: function() {
return this.x;
}
};
const unboundGetX = module.getX;
console.log(unboundGetX()); // undefined(在严格模式或浏览器中是 undefined)
const boundGetX = unboundGetX.bind(module);
console.log(boundGetX()); // 42
实战场景:
javascript
// 场景一:数组方法借用
const arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 };
const arr = Array.prototype.slice.call(arrayLike);
console.log(arr); // ['a', 'b', 'c']
// 场景二:事件处理中的 this 绑定
class Button {
constructor() {
this.clicked = false;
// 确保回调中的 this 指向实例
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.clicked = !this.clicked;
console.log('Button clicked:', this.clicked);
}
}
2.6 函数作为参数和返回值
JavaScript 函数的强大之处在于它可以作为参数传递或作为返回值返回。
javascript
// 函数作为参数(回调函数)
function fetchData(url, callback) {
// 模拟异步请求
setTimeout(() => {
callback({ success: true, data: 'Some data' });
}, 1000);
}
fetchData('/api/users', function(response) {
console.log(response);
});
// 函数作为返回值(工厂函数)
function createMultiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
2.7 立即执行函数表达式(IIFE)
IIFE(Immediately Invoked Function Expression)是一种在定义后立即执行的函数,常用于创建独立的作用域,避免变量污染全局命名空间。
javascript
// 经典写法
(function() {
const privateVar = 'I am private';
console.log(privateVar);
})();
// 现代写法(箭头函数)
(() => {
const message = 'Hello from IIFE';
console.log(message);
})();
// 带参数
((name) => {
console.log(`Hello, ${name}!`);
})('World');
// IIFE 的典型应用:模块化
const Counter = (function() {
let count = 0; // 私有变量
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
getCount: function() {
return count;
}
};
})();
Counter.increment();
Counter.increment();
console.log(Counter.getCount()); // 2
2.8 闭包:JavaScript 的魔法
闭包是指函数能够记住并访问其词法作用域,即使函数在其词法作用域之外被执行。
javascript
function createCounter(initialValue = 0) {
let count = initialValue;
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
reset: function() {
count = initialValue;
return count;
}
};
}
const counter1 = createCounter(10);
const counter2 = createCounter();
console.log(counter1.increment()); // 11
console.log(counter1.increment()); // 12
console.log(counter2.increment()); // 1
console.log(counter2.decrement()); // 0
闭包的经典面试题:
javascript
// 错误示例
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 输出 3, 3, 3
}, 100);
}
// 正确解决方案
// 方案一:使用 let(块级作用域)
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 输出 0, 1, 2
}, 100);
}
// 方案二:使用闭包(ES5 兼容)
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // 输出 0, 1, 2
}, 100);
})(i);
}
三、对象的增强知识
3.1 属性描述符:精准控制对象属性
JavaScript 提供了 Object.defineProperty() 方法,允许我们对属性的特性进行精细控制。
javascript
const person = {};
// 定义一个只读属性
Object.defineProperty(person, 'name', {
value: 'Alice',
writable: false, // 不可修改
enumerable: true, // 可枚举
configurable: false // 不可删除或重新定义
});
console.log(person.name); // "Alice"
person.name = 'Bob'; // 在非严格模式下静默失败
console.log(person.name); // 仍然是 "Alice"
3.2 属性描述符的两种类型
数据属性描述符(Data Descriptor)
| 特性 | 说明 |
|---|---|
configurable |
属性是否可以通过 delete 删除,是否可以修改描述符 |
enumerable |
属性是否可以通过 for...in 或 Object.keys() 枚举 |
writable |
属性的值是否可以被修改 |
value |
属性的值 |
javascript
const obj = {};
Object.defineProperty(obj, 'id', {
value: 1001,
writable: true,
enumerable: true,
configurable: true
});
console.log(obj.id); // 1001
存取属性描述符(Accessor Descriptor)
| 特性 | 说明 |
|---|---|
configurable |
属性是否可以通过 delete 删除 |
enumerable |
属性是否可以被枚举 |
get |
读取属性时调用的函数 |
set |
写入属性时调用的函数 |
存取属性描述符的典型应用:
javascript
class Temperature {
constructor(celsius) {
this._celsius = celsius;
}
get fahrenheit() {
return this._celsius * 9/5 + 32;
}
set fahrenheit(value) {
this._celsius = (value - 32) * 5/9;
}
}
const temp = new Temperature(25);
console.log(temp.fahrenheit); // 77
temp.fahrenheit = 86;
console.log(temp._celsius); // 30
数据属性描述符与存取属性描述符对比:
| 特性 | 数据描述符 | 存取描述符 |
|---|---|---|
| configurable | ✅ 可设置 | ✅ 可设置 |
| enumerable | ✅ 可设置 | ✅ 可设置 |
| value | ✅ 可设置 | ❌ 不可设置 |
| writable | ✅ 可设置 | ❌ 不可设置 |
| get | ❌ 不可设置 | ✅ 可设置 |
| set | ❌ 不可设置 | ✅ 可设置 |
3.3 Object.defineProperties:批量定义属性
当你需要同时定义多个属性时,可以使用 Object.defineProperties():
javascript
const product = {};
Object.defineProperties(product, {
name: {
value: 'Laptop',
writable: true,
enumerable: true,
configurable: true
},
price: {
value: 9999,
writable: true,
enumerable: true,
configurable: true
},
discount: {
get: function() {
return this._discount || 0;
},
set: function(value) {
if (value < 0 || value > 100) {
throw new Error('折扣必须在0-100之间');
}
this._discount = value;
},
enumerable: true,
configurable: true
}
});
product.discount = 15;
console.log(product.name, product.price, product.discount); // Laptop 9999 15
3.4 获取属性描述符
javascript
const person = { name: 'Alice', age: 25 };
console.log(Object.getOwnPropertyDescriptor(person, 'name'));
// { value: 'Alice', writable: true, enumerable: true, configurable: true }
console.log(Object.getOwnPropertyDescriptors(person));
// { name: {...}, age: {...} }
3.5 对象方法补充
preventExtensions:禁止扩展
javascript
const obj = { a: 1 };
Object.preventExtensions(obj);
obj.b = 2; // 严格模式下抛出 TypeError
console.log('b' in obj); // false
seal:密封对象
javascript
const obj = { a: 1 };
Object.seal(obj);
obj.a = 2; // 可以修改
// obj.b = 2; // 不可添加(严格模式报错)
// delete obj.a; // 不可删除(严格模式报错)
console.log(Object.isSealed(obj)); // true
freeze:冻结对象
javascript
const CONFIG = { apiUrl: 'https://api.example.com', timeout: 5000 };
Object.freeze(CONFIG);
// CONFIG.apiUrl = 'another'; // 静默失败
// CONFIG.newProp = 'value'; // 静默失败
// delete CONFIG.timeout; // 静默失败
console.log(Object.isFrozen(CONFIG)); // true
三者的对比:
| 方法 | 禁止添加属性 | 禁止删除属性 | 禁止修改属性值 |
|---|---|---|---|
preventExtensions |
✅ | ❌ | ❌ |
seal |
✅ | ✅ | ❌ |
freeze |
✅ | ✅ | ✅ |
四、实际应用场景与最佳实践
4.1 数据绑定与响应式系统
属性描述符是实现响应式数据绑定的核心技术,Vue 2 的响应式系统就是基于 Object.defineProperty 实现的:
javascript
function defineReactive(obj, key, value) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function() {
console.log(`读取属性 ${key}`);
return value;
},
set: function(newValue) {
if (newValue !== value) {
console.log(`设置属性 ${key}: ${value} -> ${newValue}`);
value = newValue;
// 触发更新逻辑
}
}
});
}
const data = {};
defineReactive(data, 'message', 'Hello');
console.log(data.message); // 触发 getter
data.message = 'World'; // 触发 setter
4.2 私有属性实现
在 ES6 class 之前,利用闭包和属性描述符可以实现私有属性:
javascript
function BankAccount(initialBalance) {
// 私有变量
let balance = initialBalance;
let accountNumber = Math.random().toString(36).substr(2, 9);
// 公共方法通过闭包访问私有变量
this.getBalance = function() {
return balance;
};
this.deposit = function(amount) {
if (amount > 0) {
balance += amount;
return true;
}
return false;
};
this.withdraw = function(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return true;
}
return false;
};
// 定义只读属性
Object.defineProperty(this, 'accountNumber', {
get: function() {
return accountNumber;
},
enumerable: true
});
}
const account = new BankAccount(1000);
console.log(account.getBalance()); // 1000
account.deposit(500);
console.log(account.getBalance()); // 1500
console.log(account.accountNumber); // 随机生成的账号
4.3 配置对象的验证
javascript
function createButton(options) {
const defaultOptions = {
text: 'Click me',
color: '#333',
backgroundColor: '#fff',
borderRadius: 4,
disabled: false,
onClick: function() {}
};
// 合并配置
const config = Object.assign({}, defaultOptions, options);
// 验证配置
if (typeof config.text !== 'string' || config.text.length === 0) {
throw new Error('按钮文本不能为空');
}
if (config.borderRadius < 0 || config.borderRadius > 50) {
config.borderRadius = Math.max(0, Math.min(50, config.borderRadius));
}
// 创建按钮元素
const button = document.createElement('button');
button.textContent = config.text;
button.style.color = config.color;
button.style.backgroundColor = config.backgroundColor;
button.style.borderRadius = `${config.borderRadius}px`;
button.disabled = config.disabled;
button.onclick = config.onClick;
return button;
}
4.4 不可变配置的最佳实践
javascript
// 应用配置应该被冻结
const APP_CONFIG = Object.freeze({
apiBaseUrl: 'https://api.example.com',
version: '1.0.0',
features: Object.freeze({
darkMode: true,
notifications: true,
analytics: false
})
});
// 数据库配置也应该冻结
const DB_CONFIG = Object.freeze({
host: 'localhost',
port: 5432,
database: 'myapp',
pool: Object.freeze({
min: 2,
max: 10
})
});
函数部分
- ✅ 函数是一等公民,可以作为参数、返回值、赋值给变量
- ✅
arguments对象提供参数访问(已被 rest parameters 取代) - ✅
prototype是实现继承的关键 - ✅
call/apply/bind改变函数执行上下文 - ✅ IIFE 创建独立作用域,避免全局污染
- ✅ 闭包是 JavaScript 最强大的特性之一
对象部分
- ✅
Object.defineProperty实现精细的属性控制 - ✅ 数据描述符和存取描述符各有适用场景
- ✅
Object.defineProperties批量定义属性 - ✅
preventExtensions/seal/freeze实现不同级别的对象保护 - ✅ 属性描述符是实现响应式系统的基础
掌握了这些知识点,你将在以下场景中游刃有余:
- 编写更优雅、更高效的 JavaScript 代码
- 理解和阅读 Vue/React 等框架的源码
- 面试中应对各种高级 JavaScript 问题
- 设计自己的工具库和框架
希望这篇文章对你有帮助!如果你觉得有用,欢迎转发给身边学习 JavaScript 的朋友。如果有任何问题或建议,欢迎在评论区留言交流。