目录
[一、函数:JS 的 "代码积木"](#一、函数:JS 的 “代码积木”)
[1.1 函数的基本定义与调用](#1.1 函数的基本定义与调用)
[1.2 函数的参数特性:灵活的 "传参规则"](#1.2 函数的参数特性:灵活的 “传参规则”)
[1.3 函数表达式:把函数 "存进变量里"](#1.3 函数表达式:把函数 “存进变量里”)
[1.4 作用域与作用域链:函数的 "变量访问规则"](#1.4 作用域与作用域链:函数的 “变量访问规则”)
[1.4.1 全局作用域与局部作用域(函数作用域)](#1.4.1 全局作用域与局部作用域(函数作用域))
[1.4.2 作用域链:变量的 "链式查找规则"](#1.4.2 作用域链:变量的 “链式查找规则”)
[二、对象:JS 的 "万物皆对象"](#二、对象:JS 的 “万物皆对象”)
[2.1 对象的三种创建方式](#2.1 对象的三种创建方式)
[2.1.1 字面量方式创建对象【推荐】](#2.1.1 字面量方式创建对象【推荐】)
[2.1.2 使用 new Object 创建对象](#2.1.2 使用 new Object 创建对象)
[2.1.3 构造函数创建对象:批量创建 "同款对象"](#2.1.3 构造函数创建对象:批量创建 “同款对象”)
[(2) 用 new 关键字创建对象](#(2) 用 new 关键字创建对象)
[(3) new 关键字的执行过程](#(3) new 关键字的执行过程)
[2.2 this 关键字:对象的 "身份标识"](#2.2 this 关键字:对象的 “身份标识”)
[2.3 JS 对象与 Java 对象的核心区别](#2.3 JS 对象与 Java 对象的核心区别)
[2.3.1 JS 没有 "类" 的概念(ES6 之前)](#2.3.1 JS 没有 “类” 的概念(ES6 之前))
[2.3.2 JS 对象不区分 "属性" 和 "方法"](#2.3.2 JS 对象不区分 “属性” 和 “方法”)
[2.3.3 JS 对象没有访问控制机制](#2.3.3 JS 对象没有访问控制机制)
[2.3.4 JS 通过原型实现 "继承",而非类继承](#2.3.4 JS 通过原型实现 “继承”,而非类继承)
[2.3.5 JS 没有 "多态" 的语法支持](#2.3.5 JS 没有 “多态” 的语法支持)
[三、函数与对象的结合:JS 编程的核心思维](#三、函数与对象的结合:JS 编程的核心思维)
前言
在上一篇 JavaScript 语法基础中,我们聊到了 JS 的基本数据类型、运算符、流程控制和数组,这些内容是 JS 入门的基石。而函数和对象作为 JS 的核心组成部分,是实现代码复用、模块化开发以及模拟面向对象编程的关键,更是我们从 "入门" 走向 "进阶" 的必经之路。本文将结合实际代码案例,深入浅出地讲解 JS 中函数和对象的定义、使用、特性以及核心细节,让你彻底吃透这两大核心知识点!下面就让我们正式开始吧!

一、函数:JS 的 "代码积木"
在编程中,我们经常会遇到需要重复执行的代码逻辑,比如计算两个数的和、判断一个数是否为质数、渲染页面的某个模块。如果每次都重复写一遍相同的代码,不仅会让代码变得臃肿不堪,还会大大降低开发效率和可维护性。函数就是为了解决这个问题而生的 ------ 它是一段被封装起来的、可以重复调用的代码块,就像搭积木的零件,能被反复组合使用。
1.1 函数的基本定义与调用
JS 中定义函数的最基本方式是函数声明,语法格式清晰易懂,就像给一段代码起个名字,再规定它的输入和输出:
javascript
// 函数声明:function 函数名(形参列表) { 函数体; return 返回值; }
function sum(num1, num2) {
var result = num1 + num2;
return result; // 返回计算结果
}
function:JS 中定义函数的关键字,必须写在最前面;- 函数名:遵循标识符命名规则,尽量做到 "见名知意",比如求和用
sum、求阶乘用factorial;- 形参列表:相当于函数的 "输入",可以理解为函数内部的临时变量,调用函数时才会被赋值;
- 函数体:需要重复执行的代码逻辑,是函数的核心;
return:指定函数的 "输出",执行到return时函数会立即结束,后面的代码不会执行;如果没有return,函数默认返回undefined。
定义函数后,必须调用才会执行 ,调用的方式也很简单:函数名(实参列表),实参就是给形参传递的具体值:
javascript
// 调用函数,实参10和20会赋值给形参num1和num2
var total = sum(10, 20);
console.log(total); // 输出:30
// 不接收返回值也可以调用
sum(5, 8);
这里有一个非常重要的点:函数的定义和调用顺序没有要求,哪怕先调用再定义,代码也能正常执行,这一点和变量完全不同(变量必须先定义再使用):
javascript
// 先调用函数,后定义函数,正常执行
hello();
function hello() {
console.log("Hello JavaScript!");
}
1.2 函数的参数特性:灵活的 "传参规则"
JS 的函数传参非常灵活,实参和形参的个数可以不匹配,这是 JS 作为动态类型语言的典型特征,不过在实际开发中,我们还是建议让实参和形参个数保持一致,避免出现不必要的问题。具体的匹配规则分为两种:
(1)实参个数多于形参:多出的实参不会参与函数内部的运算,函数只会取前 N 个实参(N 为形参个数);
javascript
function sum(num1, num2) {
return num1 + num2;
}
// 实参有3个,形参只有2个,第三个实参30被忽略
var total = sum(10, 20, 30);
console.log(total); // 输出:30
(2)实参个数少于形参 :未被赋值的形参值为**undefined,此时如果进行运算,很可能得到NaN**;
javascript
function sum(num1, num2) {
return num1 + num2;
}
// 只传1个实参,num2为undefined
var total = sum(10);
console.log(total); // 输出:NaN
如果我们需要让函数支持任意个数的参数 ,可以使用 JS 内置的**arguments对象 ------ 它是函数内部的一个伪数组,包含了调用函数时传递的所有实参,能通过下标访问,也能通过length**获取参数个数:
javascript
// 计算任意个数数字的和
function add() {
var sum = 0;
// 遍历arguments,累加所有实参
for (var i = 0; i < arguments.length; i++) {
sum += arguments[i];
}
return sum;
}
console.log(add(10, 20)); // 输出:30
console.log(add(1, 2, 3, 4)); // 输出:10
console.log(add(5, 8, 10, 15, 20)); // 输出:58
1.3 函数表达式:把函数 "存进变量里"
除了函数声明,JS 中还有另一种定义函数的方式 ------函数表达式,它的核心是将一个匿名函数赋值给一个变量,通过变量来调用函数:
javascript
// 函数表达式:匿名函数赋值给变量add
var add = function() {
var sum = 0;
for (var i = 0; i < arguments.length; i++) {
sum += arguments[i];
}
return sum;
};
// 通过变量调用函数
console.log(add(10, 20)); // 输出:30
console.log(typeof add); // 输出:function,说明变量的类型是函数
这里的function() {}是匿名函数 ,没有函数名,只能通过赋值的变量来使用。需要注意的是,函数表达式的定义必须在调用之前,否则会报错,这一点和函数声明正好相反:
javascript
// 先调用后定义,报错:add is not a function
add(10, 20);
var add = function() {
return num1 + num2;
};
函数表达式的出现,印证了 JS 中一个重要的概念:函数是一等公民。也就是说,函数可以像普通变量一样被赋值、传递,也可以作为其他函数的参数或返回值,这为后续的高阶函数、闭包等知识点打下了基础。
1.4 作用域与作用域链:函数的 "变量访问规则"
写函数时,我们经常会遇到一个问题:不同位置的变量,哪些能访问,哪些不能访问?这就涉及到作用域------ 某个标识符(变量 / 函数名)在代码中的有效范围。在 ES6 之前,JS 的作用域主要分为两种:
1.4.1 全局作用域与局部作用域(函数作用域)
- 全局作用域 :在整个**
script标签中或单独的 JS 文件中生效,全局作用域中定义的变量称为全局变量**,任何地方都能访问;- 局部作用域(函数作用域) :在函数内部生效,函数内部定义的变量称为局部变量,只有函数内部能访问,外部无法访问。
javascript
// 全局变量:在script标签中定义,任何地方都能访问
var num = 10;
console.log(num); // 输出:10
function test1() {
// 局部变量:只有test1内部能访问
var num = 20;
console.log(num); // 输出:20
}
function test2() {
// 局部变量:只有test2内部能访问
var num = 30;
console.log(num); // 输出:30
}
test1();
test2();
// 外部依然访问全局变量num
console.log(num); // 输出:10
这里有一个坑需要注意:创建变量时如果不写var关键字,会默认创建全局变量,哪怕是在函数内部:
javascript
function test() {
// 没有写var,num成为全局变量
num = 100;
}
test();
// 外部能访问到函数内部定义的num
console.log(num); // 输出:100
另外,和 Java、C++ 等语言不同,JS 在 ES6 之前,局部作用域不是按大括号{}划分的,if、for 等语句的大括号中定义的变量,依然是全局变量:
javascript
if (1 < 2) {
// 大括号中定义的变量a,是全局变量
var a = 10;
}
// 外部能正常访问a
console.log(a); // 输出:10
1.4.2 作用域链:变量的 "链式查找规则"
JS 中函数可以嵌套定义,也就是在一个函数内部定义另一个函数。此时,内层函数可以访问外层函数的局部变量 ,这种访问规则遵循作用域链 :变量的查找会从当前作用域开始,从内到外依次查找 ,找到则使用,找不到则继续向上,直到全局作用域;如果全局作用域也没有,就会报错**xxx is not defined**。
javascript
// 全局作用域的num
var num = 1;
function test1() {
// test1作用域的num
var num = 10;
function test2() {
// test2作用域的num
var num = 20;
console.log(num); // 先找test2内部,找到num=20,输出:20
}
test2();
}
test1();
如果我们删除 test2 内部的 num,变量查找会向上到 test1 作用域;如果再删除 test1 内部的 num,就会找到全局作用域的 num:
javascript
var num = 1;
function test1() {
var num = 10;
function test2() {
// 没有自己的num,向上找test1的num=10
console.log(num); // 输出:10
}
test2();
}
test1();
作用域链的本质是:内部函数会保存外部函数的作用域,形成一个链式的作用域结构,这也是闭包的核心原理。
二、对象:JS 的 "万物皆对象"
在现实生活中,我们接触的每一个事物都是一个对象,比如一台电脑、一只猫咪、一个学生:电脑有品牌、型号、配置等特征 ,也有开机、关机、运行程序等行为;猫咪有名字、品种、颜色等特征,也有叫、跑、跳等行为。
在 JS 中,对象就是对现实事物的抽象,它是一个无序的键值对集合,其中:
- 键(key) :表示事物的特征或行为,特征称为属性 ,行为称为方法;
- 值(value) :属性的值可以是任意基本数据类型(数字、字符串、布尔等),方法的值是函数。
JS 中有一句经典的话:万物皆对象。字符串、数值、数组、函数都是对象,甚至连浏览器窗口、文档节点都是对象。对象的出现,让我们可以更清晰、更结构化地存储和操作数据,相比数组(只能通过下标访问),对象通过属性名访问数据,语义更明确,可读性更高。
比如表示一个学生的信息,用数组的话是这样的:
javascript
// 数组:下标0是姓名,1是身高,2是体重,语义不明确
var student = ['蔡徐坤', 175, 170];
别人看到这段代码,根本不知道 175 和 170 分别代表什么;而用对象的话,语义一目了然:
javascript
// 对象:属性名name、height、weight明确表示含义
var student = {
name: '蔡徐坤',
height: 175,
weight: 170
};
2.1 对象的三种创建方式
JS 中创建对象的方式有三种,其中字面量方式是最常用、最简洁的,构造函数方式适合创建多个具有相同结构的对象,我们逐一讲解。
2.1.1 字面量方式创建对象【推荐】
使用**大括号{}创建对象,是 JS 中最常用的方式,直接在{}中定义属性和方法,键值对 的形式组织,键和值之间用:分隔,键值对之间用,**分隔,最后一个键值对的,可以省略。
javascript
// 创建空对象
var obj = {};
// 创建包含属性和方法的对象
var student = {
// 属性:键是name,值是'蔡徐坤'
name: '蔡徐坤',
height: 175,
weight: 170,
// 方法:键是sayHello,值是匿名函数
sayHello: function() {
console.log("Hello, 我是" + this.name);
}
};
这里的**this是 JS 中的关键字,在对象的方法中,this指向当前对象**,也就是调用这个方法的对象,后面会详细讲解。
访问对象的属性和方法有两种方式:
- 点访问符(.) :最常用,语法为对象名.属性名/对象名.方法名(),
.可以理解为 "的";- 方括号访问符([]) :语法为对象名['属性名']/对象名'方法名',属性名必须用引号引起来,适合属性名包含特殊字符的场景。
javascript
// 访问属性
console.log(student.name); // 点访问,输出:蔡徐坤
console.log(student['height']); // 方括号访问,输出:175
// 调用方法,别忘记加()
student.sayHello(); // 点调用,输出:Hello, 我是蔡徐坤
student['sayHello'](); // 方括号调用,输出:Hello, 我是蔡徐坤
对象的属性和方法可以随时新增和修改,这也是 JS 对象的灵活之处:
javascript
// 新增属性
student.age = 25;
student['gender'] = '男';
// 修改属性
student.height = 178;
// 新增方法
student.study = function() {
console.log(this.name + '正在学习JavaScript');
};
// 调用新方法
student.study(); // 输出:蔡徐坤正在学习JavaScript
console.log(student); // 输出包含所有属性和方法的对象
2.1.2 使用 new Object 创建对象
**new Object()**是 JS 中创建对象的原生方式,相当于字面量方式的 "官方写法",先创建空对象,再通过点访问符或方括号访问符添加属性和方法:
javascript
// 创建空对象
var student = new Object();
// 新增属性
student.name = "蔡徐坤";
student.height = 175;
student['weight'] = 170;
// 新增方法
student.sayHello = function () {
console.log("Hello, 我是" + this.name);
};
// 访问属性和调用方法
console.log(student.name); // 输出:蔡徐坤
student.sayHello(); // 输出:Hello, 我是蔡徐坤
这种方式和字面量方式创建的对象本质上是一样的,只是写法不同,实际开发中更推荐字面量方式,因为更简洁。
2.1.3 构造函数创建对象:批量创建 "同款对象"
如果我们需要创建多个具有相同结构的对象,比如多只猫咪、多个学生、多个商品,用字面量或**new Object**的方式会重复写大量相同的代码,非常麻烦。此时,构造函数就是最佳选择 ------ 它是一个特殊的函数,专门用于创建对象,能把对象的公共属性和方法提取出来,实现批量创建。
(1)构造函数的基本语法
构造函数的定义和普通函数类似,但有几个特殊的规范和要求:
- 构造函数的函数名首字母必须大写,用于区分普通函数;
- 函数内部使用this 关键字 表示当前正在构建的对象,给 this 添加属性和方法,就是给创建的对象添加属性和方法;
- 构造函数不需要 return,会自动返回创建的对象;
- 创建对象时必须使用 new 关键字,不能直接调用。
javascript
// 构造函数:首字母大写,参数为对象的公共属性
function Cat(name, type, sound) {
// this指向即将创建的猫咪对象
this.name = name;
this.type = type;
// 给对象添加方法
this.miao = function () {
console.log(sound);
}
}
(2) 用 new 关键字创建对象
使用**new 构造函数名(实参)**就能批量创建对象,每个对象都有独立的属性和方法,互不影响:
javascript
// 创建3只猫咪对象,传递不同的实参
var mimi = new Cat('咪咪', '中华田园喵', '喵');
var xiaohei = new Cat('小黑', '波斯喵', '猫呜');
var ciqiu = new Cat('刺球', '金渐层', '咕噜噜');
// 访问对象的属性
console.log(mimi.name); // 输出:咪咪
console.log(xiaohei.type); // 输出:波斯喵
// 调用对象的方法
mimi.miao(); // 输出:喵
ciqiu.miao(); // 输出:咕噜噜
(3) new 关键字的执行过程
很多同学会好奇,为什么只要写new 构造函数就能创建对象?其实new关键字背后做了 4 件事,这是 JS 的核心面试考点,一定要掌握:
- 在内存中创建一个空的对象;
- 将 this 指向这个空对象,让构造函数的 this 绑定到新对象上;
- 执行构造函数的代码,给空对象添加属性和方法;
- 自动返回这个新对象(构造函数不需要写 return,new 会代劳)。
理解了 new 的执行过程,就能明白为什么构造函数必须用 new 调用:如果不用 new,构造函数就是普通函数,this 会指向全局对象(浏览器中是 window),而不是新创建的对象,会造成全局变量污染。
2.2 this 关键字:对象的 "身份标识"
在 JS 中,this是一个非常重要且灵活的关键字,它的核心是指向调用者 ,简单来说:谁调用,this 就指向谁 。在不同的场景下,this的指向不同,我们这里重点讲解对象方法中的 this,这是最常用的场景。
在对象的方法中,this 指向调用这个方法的对象 ,也就是当前对象。这样一来,不同的对象调用同一个方法,this会指向不同的对象,从而实现方法的复用。
javascript
function Person(name, age) {
this.name = name;
this.age = age;
this.sayHi = function() {
// this指向调用sayHi的Person对象
console.log("我是" + this.name + ", 今年" + this.age + "岁");
}
}
var p1 = new Person('张三', 20);
var p2 = new Person('李四', 25);
// p1调用sayHi,this指向p1
p1.sayHi(); // 输出:我是张三, 今年20岁
// p2调用sayHi,this指向p2
p2.sayHi(); // 输出:我是李四, 今年25岁
如果直接单独调用方法,而不是通过对象调用,this 会指向全局对象 window(浏览器中),此时属性会变成 window 的属性,这是我们需要避免的:
javascript
// 直接调用方法,this指向window
var sayHi = p1.sayHi;
sayHi(); // 输出:我是undefined, 今年undefined岁
2.3 JS 对象与 Java 对象的核心区别
很多同学都是先学 Java 再学 JS,很容易把两个语言的对象概念混淆。虽然 JS 和 Java 都有 "对象" 的概念,也都有属性和方法,但二者的差异非常大,核心区别有 5 点,掌握这些区别能让你更深刻地理解 JS 的对象模型:
2.3.1 JS 没有 "类" 的概念(ES6 之前)
Java 是纯粹的面向对象语言,一切皆类,对象是类的实例,必须先定义类,再通过类创建对象;而 JS 在 ES6 之前没有类的概念 ,对象是直接通过字面量、new Object或构造函数创建的,构造函数只是起到了类似 "类" 的作用,用于批量创建对象。
ES6 中引入了**class关键字,让 JS 可以像 Java 一样定义类和创建对象,但这只是语法糖**,底层依然是基于构造函数和原型实现的,并不是真正的类。
2.3.2 JS 对象不区分 "属性" 和 "方法"
在 Java 中,属性是成员变量,方法是成员函数,二者是严格区分的;而在 JS 中,函数是一等公民 ,方法本质上是 "值为函数的属性",和普通属性没有本质区别,只是这个属性的值可以通过**()**调用。
javascript
var obj = {
// 普通属性:值是字符串
name: 'JS',
// 方法:值是函数,本质也是属性
sayHello: function() {
console.log("Hello");
}
};
// 可以像修改普通属性一样修改方法
obj.sayHello = function() {
console.log("Hello JavaScript");
};
obj.sayHello(); // 输出:Hello JavaScript
2.3.3 JS 对象没有访问控制机制
Java 中有private、public、protected等访问修饰符,用于控制属性和方法的访问权限,比如private的属性只能在类内部访问;而 JS 中没有任何访问控制机制,对象的所有属性和方法都能被外界随意访问、修改和删除,没有 "私有属性 / 方法" 的概念(ES6 中可以通过 Symbol 实现私有属性,属于进阶内容)。
2.3.4 JS 通过原型实现 "继承",而非类继承
Java 的继承是类的继承 ,子类继承父类,能继承父类的属性和方法;而 JS 中没有类的继承 ,而是通过原型(prototype) 机制实现类似的继承效果,核心是让一个对象的__proto__属性指向另一个对象,从而实现属性和方法的复用。
当访问一个对象的属性或方法时,如果对象自身没有,会去它的原型对象中查找;如果原型对象也没有,会去原型的原型中查找,直到找到 Object 的原型(Object.prototype),如果还没找到,就返回undefined,这个过程称为原型链。
javascript
// 原型对象:包含公共的方法eat
var animal = {
eat: function() {
console.log(this.name + "正在吃饭");
}
};
// 猫对象:__proto__指向animal,继承eat方法
var cat = {
name: '咪咪',
__proto__: animal
};
// 狗对象:__proto__指向animal,继承eat方法
var dog = {
name: '旺财',
__proto__: animal
};
// 猫和狗都能调用原型对象的eat方法
cat.eat(); // 输出:咪咪正在吃饭
dog.eat(); // 输出:旺财正在吃饭
原型和原型链是 JS 的核心难点,也是面试的高频考点,后续会专门写一篇文章详细讲解。
2.3.5 JS 没有 "多态" 的语法支持
Java 的多态是基于类的继承和方法重写 实现的,比如父类引用指向子类对象,调用方法时会执行子类的重写方法;而 JS 作为动态类型语言,本身就支持 "多态" 的特性 ------ 调用对象的方法时,不需要关注对象的类型,只要对象有这个方法就能调用,因此不需要在语法层面专门支持多态。
javascript
// 定义一个add函数,只要参数list有add方法就能调用
function add(list, s) {
list.add(s);
}
// 自定义一个数组对象,有add方法
var arr1 = {
data: [],
add: function(s) {
this.data.push(s);
console.log("添加成功:" + s);
}
};
// 原生数组,本身有push方法,我们给它添加add方法
var arr2 = [1,2,3];
arr2.add = function(s) {
this.push(s);
console.log("数组添加:" + s);
};
// 调用add函数,传入不同的对象,只要有add方法就能执行
add(arr1, 'JS'); // 输出:添加成功:JS
add(arr2, 4); // 输出:数组添加:4
从上面的代码可以看出,JS 的多态是 "天生的",不需要像 Java 那样通过类继承来实现,这也是 JS 作为动态语言的灵活性体现。
三、函数与对象的结合:JS 编程的核心思维
函数和对象并不是孤立的,而是紧密结合的,二者的结合形成了 JS 编程的核心思维 ------将函数作为对象的方法,实现代码的结构化和复用。
在实际开发中,我们不会单独写一堆函数,而是会将相关的函数封装到一个对象中,作为对象的方法,这样不仅能让代码的语义更明确,还能避免全局变量污染。比如实现一个 "计算器" 功能,我们可以把加法、减法、乘法、除法封装到一个计算器对象中:
javascript
// 计算器对象:包含计算相关的方法
var calculator = {
add: function(num1, num2) {
return num1 + num2;
},
sub: function(num1, num2) {
return num1 - num2;
},
mul: function(num1, num2) {
return num1 * num2;
},
div: function(num1, num2) {
if (num2 === 0) {
console.log("除数不能为0");
return undefined;
}
return num1 / num2;
}
};
// 调用计算器对象的方法
console.log(calculator.add(10, 20)); // 输出:30
console.log(calculator.mul(5, 8)); // 输出:40
console.log(calculator.div(20, 0)); // 输出:除数不能为0,返回undefined
这种方式就是模块化编程的雏形,将相关的功能封装到一个对象中,形成一个独立的模块,后续可以直接复用这个模块,也方便维护和扩展。
总结
函数和对象是 JS 的核心,也是后续学习 DOM/BOM、AJAX、Vue/React 等框架的基础,一定要多敲代码、多练习,把基础打牢。比如尝试用构造函数创建一个 "学生管理系统",包含添加学生、删除学生、查询学生等方法;或者用对象封装一个 "时钟" 模块,包含获取当前时间、格式化时间等方法。只有通过实践,才能真正理解和掌握这些知识点。
创作不易,如果这篇文章对你有帮助,欢迎点赞、收藏、关注一波~
