从函数到模块化!JS 开发必懂的 “超能力” 与 “解耦术”

216 阅读5分钟

🚀 掘金之旅:JS函数、类与模块化的奇妙冒险!

嘿,各位前端探险家们!👋 欢迎来到我们今天的掘金小课堂!你是不是还在为JavaScript里那些“花里胡哨”的函数、高大上的“类”以及让人摸不着头脑的“模块化”而挠头呢?别担心,今天我将带你一起,用最通俗易懂、最风趣幽默的方式,揭开它们神秘的面纱!

想象一下,JS的世界就像一个充满魔法的游乐园,而函数、类和模块化就是游乐园里最酷炫的三个项目。准备好了吗?系好安全带,我们这就出发!🚀

image.png

🎯 第一站:函数的“超能力”扩展

函数,作为JavaScript的“基石”,我们每天都在和它打交道。但你可能不知道,ES6(ECMAScript 2015)给函数加了好多“超能力”,让它变得更加灵活和强大!

✨ 默认参数:懒人福音,告别undefined!

你有没有遇到过这样的情况:调用一个函数,结果忘记传某个参数,然后控制台就无情地抛出了undefined?就像你点了一份外卖,结果忘记备注“不要香菜”,然后香菜就无情地出现在了你的餐盒里……🤢

ES6的默认参数就是来拯救你的!它允许你为函数的参数设置默认值,这样即使你忘记传参,函数也能“聪明”地使用预设的值,避免尴尬的undefined

function orderFood(dish, seasoning = '不要香菜') {
  console.log(`你点了${dish},备注是:${seasoning}`);
}

orderFood('麻辣烫'); // 输出:你点了麻辣烫,备注是:不要香菜
orderFood('小龙虾', '多放蒜蓉'); // 输出:你点了小龙虾,备注是:多放蒜蓉

执行结果:

看到了吗?seasoning参数有了默认值“不要香菜”,即使我们调用orderFood('麻辣烫')时没有传入第二个参数,它也能自动补上。是不是很贴心?

小贴士: 默认参数的赋值是惰性的,也就是说,只有在没有传入对应参数时,默认值表达式才会执行。这就像你有一个“备用方案”,只有在“主方案”失效时才会启用。

🔄 Rest参数:打包神器,收集散落的参数!

你有没有遇到过这样的函数:它需要接收不定数量的参数?比如,一个计算总和的函数,你可能想传两个数,也可能想传十个数。以前我们可能会用arguments对象,但它用起来有点“笨重”。

现在,有了Rest参数(...),就像有了一个“打包神器”,它可以把函数的多余参数“打包”成一个数组,让你轻松处理!

function sum(...numbers) {
  let total = 0;
  for (let num of numbers) {
    total += num;
  }
  console.log(`这些数的总和是:${total}`);
}

sum(1, 2, 3); // 输出:这些数的总和是:6
sum(10, 20, 30, 40, 50); // 输出:这些数的总和是:150

...numbers就是Rest参数,它把所有传入的数字都收集到了一个名为numbers的数组里。是不是感觉瞬间清爽了许多?

注意: Rest参数必须是函数的最后一个参数,因为它要“收集”所有剩下的参数。就像你打包行李,总得先把零散的东西都装进去,最后再把箱子盖上吧?📦

image.png

⚠️ 严格模式:代码规范的“守门员”

严格模式(Strict Mode)是JavaScript中一种更严格的解析和执行模式。它能帮助我们写出更安全、更规范的代码,避免一些潜在的错误。就像你家里的“门禁系统”,能帮你挡掉一些不速之客。

在ES6中,如果函数参数使用了默认值、解构赋值或者扩展运算符,那么函数内部会自动开启严格模式,即使你没有显式地写'use strict'。这就像你的“智能门禁”,会自动识别并开启最高级别的防护!

function doSomething(a, b = 1) {
  // 这个函数内部会自动开启严格模式
  // ...
}

了解了函数的这些“超能力”,是不是觉得JavaScript的函数变得更加强大和有趣了呢?接下来,我们将前往下一站,探索JavaScript中“类”的奥秘!敬请期待!✨

🏰 第二站:ES6中的“类”:面向对象的新篇章

在JavaScript的世界里,万物皆对象。但你有没有觉得,以前我们创建对象的方式有点“散漫”?就像你想要建造一栋房子,以前可能需要一块砖一块砖地去堆砌,而现在,有了“类”(Class),就像拥有了“建筑图纸”,可以更优雅、更高效地建造你的“对象大厦”!

ES6引入了class关键字,让JavaScript拥有了更接近传统面向对象语言的“类”的语法糖。它不是引入了一种新的继承机制,而是在ES5原型链继承的基础上,提供了一种更清晰、更方便的写法。

💡 类的定义与实例化:建造你的“对象大厦”

定义一个类,就像设计一张建筑图纸。它包含了你想要创建的对象的“蓝图”:有哪些属性(房间、窗户),有哪些方法(开门、关灯)。

class Person {
  constructor(name, age) {
    this.name = name; // 姓名
    this.age = age;   // 年龄
  }

  sayHello() {
    console.log(`大家好,我叫${this.name},今年${this.age}岁。`);
  }
}

// 实例化一个Person对象,就像根据图纸建造一栋房子
const person1 = new Person("小明", 18);
person1.sayHello(); // 输出:大家好,我叫小明,今年18岁。

const person2 = new Person("小红", 20);
person2.sayHello(); // 输出:大家好,我叫小红,今年20岁。

constructor方法是类的构造函数,当我们使用new关键字创建实例时,它会自动执行,用于初始化实例的属性。sayHello是类的方法,所有Person的实例都可以调用它。

🧬 类的继承:站在巨人的肩膀上

继承是面向对象编程中一个非常强大的特性。它允许你创建一个新类,继承现有类的属性和方法,并在此基础上进行扩展。就像你可以在已有的房子图纸上,增加新的房间或者改造结构,而不需要从头开始设计。

在JavaScript中,我们使用extends关键字来实现类的继承。

class Student extends Person {
  constructor(name, age, grade) {
    super(name, age); // 调用父类的constructor方法,初始化父类的属性
    this.grade = grade; // 学生特有的属性:年级
  }

  study() {
    console.log(`${this.name}正在努力学习,他是${this.grade}年级的学生。`);
  }
}

const student1 = new Student("小刚", 10, "五年级");
student1.sayHello(); // 继承自Person类的方法
student1.study();    // Student类特有的方法
// 输出:
// 大家好,我叫小刚,今年10岁。
// 小刚正在努力学习,他是五年级的学生。

super关键字在这里扮演了“连接父子”的角色,它代表了父类的构造函数。在子类的constructor中,如果使用了super,就必须在使用this之前调用它,否则会报错。

⚙️ 静态方法与静态属性:类自己的“秘密武器”

除了实例方法和实例属性,类还可以拥有静态方法和静态属性。它们是属于类本身,而不是属于类的实例。就像一个家族的“族规”或者“传家宝”,它们不属于某个特定的家庭成员,而是整个家族共有的。

静态方法使用static关键字定义,它们不能被实例调用,只能通过类本身来调用。

class Calculator {
  static add(a, b) {
    return a + b;
  }

  static PI = 3.1415926; // 静态属性
}

console.log(Calculator.add(5, 3)); // 输出:8
console.log(Calculator.PI);      // 输出:3.1415926

// const calc = new Calculator();
// calc.add(1, 2); // 报错:calc.add is not a function

静态方法通常用于与类相关的工具函数,或者不需要访问实例属性的方法。静态属性则可以用来存储一些与类相关的常量或者配置信息。

通过“类”这个强大的工具,我们能够更好地组织和管理代码,让复杂的程序结构变得清晰明了。接下来,我们将进入最后一站,探索JavaScript的“模块化”世界,让你的代码告别“意大利面条”!🍝

image.png

📦 第三站:模块化:告别“意大利面条”代码!

你有没有过这样的经历:写了一个超大的JavaScript文件,里面各种变量、函数、类混杂在一起,就像一盘“意大利面条”,剪不断理还乱?🍝 想要复用某个功能,结果发现它依赖了一大堆其他代码,牵一发而动全身?

这就是“模块化”要解决的问题!模块化就像把你的“意大利面条”拆分成一个个独立的“小盒子”,每个盒子里只装一个功能,盒子之间可以互相引用,但又保持独立。这样一来,代码结构清晰,复用方便,多人协作也不再是噩梦!

📜 模块化的前世今生:从CommonJS到ES6 Modules

在ES6之前,JavaScript并没有官方的模块化方案。但聪明的开发者们创造了一些“曲线救国”的方案,其中最著名的就是CommonJS和AMD。

  • CommonJS: 主要用于Node.js环境,采用同步加载模块的方式。就像你点外卖,必须等一份外卖送到了,才能点下一份。代表作:requiremodule.exports
  • AMD: 主要用于浏览器环境,采用异步加载模块的方式。就像你点外卖,可以同时点好几份,谁先做好谁先送。代表作:definerequire

虽然它们解决了燃眉之急,但终究不是语言层面的原生支持。直到ES6的到来,JavaScript终于有了官方的模块化方案——ES6 Modules(ESM),也就是我们今天要重点讲解的importexport

🚀 ES6 Modules:现代JS的“传送门”

ES6 Modules是JavaScript语言层面的模块化标准,它采用静态加载的方式,在编译阶段就能确定模块的依赖关系。这就像你使用“传送门”,在出发前就知道目的地是哪里,而不是到了地方再找路。

📤 export:把你的“宝贝”分享出去!

export命令用于规定模块的对外接口,也就是把模块内部的变量、函数、类等“宝贝”分享出去,让其他模块可以使用。

// utils.js
export const PI = 3.1415926; // 导出常量

export function add(a, b) { // 导出函数
  return a + b;
}

export class MyClass { // 导出类
  constructor(name) {
    this.name = name;
  }
  sayName() {
    console.log(`我的名字是:${this.name}`);
  }
}

// 也可以统一导出
const multiply = (a, b) => a * b;
export { multiply };
📥 import:把别人的“宝贝”拿过来用!

import命令用于加载其他模块导出的内容。就像你打开“传送门”,把别人分享的“宝贝”传送到你的模块里。

// main.js
import { PI, add, MyClass } from './utils.js'; // 导入具名导出

console.log(PI); // 输出:3.1415926
console.log(add(1, 2)); // 输出:3

const myInstance = new MyClass('小明');
myInstance.sayName(); // 输出:我的名字是:小明

import { multiply } from './utils.js';
console.log(multiply(2, 3)); // 输出:6
🌟 export default:模块的“主打产品”

每个模块可以有一个默认导出(export default),它代表了模块最主要的功能。就像一个商店的“主打产品”,你不需要知道它的具体名字,直接拿走就行。

// calculator.js
export default class Calculator {
  add(a, b) {
    return a + b;
  }
  subtract(a, b) {
    return a - b;
  }
}

// main.js
import MyCalculator from './calculator.js'; // 导入默认导出,可以任意命名

const calc = new MyCalculator();
console.log(calc.add(10, 5)); // 输出:15

注意: 一个模块只能有一个export default。导入默认导出时,你可以为它指定任意名称,因为它代表的是整个模块的默认输出。

🌐 模块的整体加载:一次性“打包”所有!

如果你想把一个模块的所有导出内容都导入进来,可以使用*进行整体加载,并指定一个别名。

// main.js
import * as Utils from './utils.js'; // 整体导入,并命名为Utils

console.log(Utils.PI); // 输出:3.1415926
console.log(Utils.add(5, 5)); // 输出:10
🔗 模块的继承与复合写法:模块间的“亲戚关系”

模块之间也可以存在继承关系,或者进行复合导出。

// base.js
export const baseName = 'Base';
export function baseFunc() { console.log('Base Function'); }

// extended.js
export * from './base.js'; // 继承base.js的所有导出
export const extendedName = 'Extended';

// main.js
import { baseName, extendedName } from './extended.js';
console.log(baseName); // 输出:Base
console.log(extendedName); // 输出:Extended
⏳ 动态导入 import():按需加载,性能优化!

传统的import是静态的,在代码执行前就确定了依赖。但有时我们希望根据条件或者用户行为,动态地加载某个模块,这时就可以使用import()函数。

import()函数返回一个Promise对象,这使得模块的加载可以异步进行,非常适合性能优化,比如按需加载组件或者路由。

// main.js
const loadModule = async () => {
  if (true) { // 假设某个条件成立
    const module = await import('./utils.js');
    console.log(module.PI);
  }
};

loadModule();

模块化是现代JavaScript开发中不可或缺的一部分,它让我们的代码更加健壮、可维护。恭喜你,已经完成了今天的掘金之旅!希望通过这次冒险,你对JS的函数扩展、类和模块化有了更深入的理解!

🎉 总结与展望

今天我们一起探索了JavaScript的三个重要领域:

  • 函数的扩展: 默认参数、Rest参数,让函数更灵活,告别undefinedarguments的烦恼。
  • 类的概念: class语法糖,让面向对象编程在JS中更加优雅,代码结构更清晰。
  • 模块化: importexport,让代码告别“意大利面条”,实现高内聚、低耦合,提升开发效率。

这些新特性不仅提升了JavaScript的开发体验,也让它在构建大型复杂应用时更加得心应手。前端的世界日新月异,只有不断学习,才能紧跟时代的步伐!

如果你觉得这篇博客对你有帮助,别忘了点赞、收藏、转发哦!👍 你的支持是我继续创作的最大动力!我们下期掘金再见!👋