交互之魂:JavaScript 核心原理

1. 认识 JavaScript:赋予网页灵魂

JavaScript (简称 JS) 是一门跨平台、面向对象的轻量级脚本语言。 如果说 HTML 是网页的骨架,CSS 是网页的皮肤,那么 JavaScript 就是网页的肌肉和神经,它让网页真正“动”起来,拥有交互能力。

1.1 JS 与 Java 的区别?

这是一个经典的新手误区。JavaScript 和 Java 是两门完全不同的语言,不论是概念还是设计,只是名字比较像而已(当年网景公司为了蹭 Java 的热度而改名)。

  • Java 是强类型、编译型语言,代码需要编译成字节码才能运行。
  • JavaScript 是弱类型、解释型脚本语言,代码由浏览器引擎直接实时解析执行。

1.2 JavaScript 可以做什么?

  1. 改变页面内容:比如点击按钮后,文字发生改变。
  2. 修改元素属性和样式:比如点击“开灯”按钮,替换图片的 src 属性,或者改变背景颜色。
  3. 表单输入校验:在用户提交注册信息前,检查用户名长度、手机号格式是否正确。
  4. 与服务器进行异步通信(Ajax/Fetch):实现无刷新获取数据(如无限下拉加载)。

2. JavaScript 的引入方式

与 CSS 类似,我们要让 JS 作用于 HTML,有内部和外部两种常见引入方式。

2.1 内部脚本

在 HTML 页面中,将 JS 代码写在 <script> 标签内。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>内部脚本示例</title>
</head>
<body>
  <h1>Hello JS</h1>
  
  <!-- 推荐将 script 放在 body 底部 -->
  <script>
    alert("这是内部脚本弹出的警告框!");
  </script>
</body>
</html>

👨‍🏫 IT 老师提示:为什么推荐把 <script> 放在 <body> 底部? 浏览器解析 HTML 是从上到下的。如果把 JS 放在 <head> 中,且 JS 代码很庞大或需要网络请求,会阻塞下方 HTML 内容的渲染,导致页面出现“白屏”。放在底部可以让用户先看到页面内容,提升体验。

2.2 外部脚本(🌟 行业规范)

将 JS 代码写在独立的 .js 文件中,然后在 HTML 中通过 <script src="..."> 引入。

demo.js

console.log("这是来自外部的 JS 文件");

index.html

<body>
  <!-- 引入外部 JS 文件 -->
  <script src="js/demo.js"></script>
</body>

⚠️ 易错坑点:

  1. 外部脚本文件中不能包含 <script> 标签,直接写 JS 代码即可。
  2. 引入外部文件的 <script src="..."></script> 标签不能自闭合,且标签内部不应再写代码(写了也会被忽略)。
  3. deferasync 属性:在现代开发中,如果 <script> 必须放在 <head> 里,建议加上 defer 属性(<script src="..." defer></script>),它会让脚本在后台异步下载,并在 HTML 解析完毕后按顺序执行,不会阻塞页面渲染。

3. 基础语法与数据类型

3.1 变量声明:抛弃 var,拥抱 let 与 const

JavaScript 是一门弱类型语言,变量可以存放任意类型的数据。

  • var(过时语法,极度不推荐):存在变量提升、作用域混乱(没有块级作用域)、允许重复声明等诸多缺陷。
  • let(现代规范):用于声明变量(值可以被修改)。拥有严格的块级作用域,不允许同一作用域内重复声明。
  • const(现代规范):用于声明常量。一旦声明必须初始化,且后续不允许重新赋值。对于对象和数组,const 保证的是引用的地址不变,内部属性依然可以修改。
{
  let age = 20;
  age = 21; // 合法
  
  const PI = 3.14159;
  // PI = 3.14; // 报错!常量不可修改

  const arr = [1, 2];
  arr.push(3); // 合法!数组内部的修改是被允许的
}
// console.log(age); // 报错!age 的作用域仅在上面的大括号内

🎨 前端设计师视角: 现代前端开发中,默认首选 const,只有明确知道这个变量未来会被修改时,才使用 let。这能极大减少代码中的意外 Bug。

3.2 数据类型

JavaScript 有几种基础的原始数据类型:

  • number:数字,包括整数、小数,以及特殊的 NaN(Not a Number,不是一个数字)。
  • string:字符串,单引号 ''、双引号 "" 或反引号 `` 均可。
  • boolean:布尔值,truefalse
  • undefined:未定义。当声明了变量但未赋值时,它的默认值就是 undefined
  • null:空值。表示一个有意设置的空对象引用。

🔍 面试高频题:undefinednull 的区别 undefined 意味着“系统还没给它赋值”,而 null 意味着“程序员故意给它赋了一个空值”。

3.3 运算符:===== 的致命区别

  • ==(宽松相等):只比较是否相等。如果类型不同,会发生隐式类型转换再比较。
  • ===(严格相等):比较类型。类型不同直接返回 false
console.log(20 == "20");  // true (字符串 "20" 被自动转换成了数字 20)
console.log(20 === "20"); // false (类型不同,一个是 number,一个是 string)

🛡️ 安全规范: 在企业级开发中,永远使用 ===!==,严禁使用 ==,以防诡异的类型转换引发的 Bug。


4. 核心内置对象

4.1 Array(数组)

JavaScript 的数组非常灵活:长度可变,且可以存储不同类型的数据

let arr = [1, "hello", true];

// 1. push(): 在数组末尾添加元素
arr.push("world");

// 2. splice(起始索引, 删除个数): 删除元素
arr.splice(0, 1); // 删除索引为 0 的 1 个元素

// 3. length 属性:获取数组长度
console.log(arr.length);

4.2 String(字符串)

let str = "  hello js  ";

// length: 获取字符串长度
console.log(str.length);

// trim(): 去除字符串两端的空格(极其常用,表单验证必写!)
let cleanStr = str.trim(); 
console.log(cleanStr); // "hello js"

5. BOM(浏览器对象模型)

BOM (Browser Object Model) 将浏览器的各个组成部分封装成了对象,让我们能用 JS 控制浏览器。

5.1 Window 对象(全局核心)

window 是浏览器的全局对象,可以直接省略 window. 前缀调用其方法。

  • 弹窗函数
    • alert("警告"):普通的警告框。
    • confirm("确定要删除吗?"):确认框。点击确定返回 true,取消返回 false
  • 定时器函数
    • setTimeout(回调函数, 毫秒)延迟执行一次
    • setInterval(回调函数, 毫秒)每隔固定时间循环执行
// 延迟 3 秒后执行
setTimeout(() => {
  console.log("3秒到了!");
}, 3000);

5.2 Location 对象(地址栏)

用于获取或修改当前页面的 URL。

// 页面重定向(跳转到百度)
// location.href = "https://www.baidu.com";

5.3 LocalStorage 与 SessionStorage(本地存储)

站长必学的客户端存储方案,用于在用户浏览器中保存数据(如登录凭证、深色模式偏好)。

  • localStorage:永久保存,除非用户手动清除。
  • sessionStorage:关闭浏览器标签页后即刻清除。
// 保存数据
localStorage.setItem("username", "Jack");
// 获取数据
let name = localStorage.getItem("username");

6. DOM(文档对象模型)

DOM (Document Object Model) 将 HTML 文档解析为一棵节点树。通过 DOM,JS 可以自由地操作页面上的任何标签。

6.1 获取 DOM 元素

  • document.getElementById("id"):根据 ID 获取单个元素。
  • document.querySelector(".class"):根据 CSS 选择器获取第一个匹配的元素(现代前端最常用!)。
  • document.querySelectorAll("div"):获取所有匹配的元素集合。

6.2 操作元素内容与样式

<img id="light" src="off.gif">
<div id="box">原内容</div>

<script>
  // 1. 修改属性
  let img = document.getElementById("light");
  img.src = "on.gif"; // 点亮灯泡
  
  // 2. 修改标签体内容
  let box = document.getElementById("box");
  box.innerHTML = "<span style='color:red'>修改后的红字</span>"; // 解析 HTML 标签
  // box.innerText = "修改后的纯文本"; // 不解析 HTML,只当做纯文本

  // 3. 修改 CSS 样式 (注意:CSS 中的短横线属性在 JS 中要转为驼峰命名)
  box.style.backgroundColor = "#f0f0f0";
  box.style.fontSize = "20px";
</script>

7. 事件监听:让页面产生交互

事件是 HTML 元素上发生的事情(如被点击、鼠标移入、获取焦点)。

7.1 事件绑定方式

方式一:DOM 属性绑定(传统方式)

<button id="myBtn">点我</button>
<script>
  document.getElementById("myBtn").onclick = function() {
    alert("我被点击了!");
  };
</script>

方式二:addEventListener 事件监听(🌟 现代规范推荐) 这是最专业、最强大的事件绑定方式。它允许为一个元素绑定多个同类型事件,而不会被覆盖。

const btn = document.getElementById("myBtn");
btn.addEventListener("click", function(event) {
  console.log("按钮被点击了");
  // event 是事件对象,包含了当前事件的所有信息
  console.log(event.target); // 获取被点击的真实 DOM 元素
});

7.2 常用核心事件

事件名 触发时机 常见应用场景
click 鼠标点击 按钮点击提交、展开折叠菜单
focus 元素获得焦点 点击输入框时高亮边框
blur 元素失去焦点 用户输入完毕移开鼠标后,立刻进行表单正则校验
mouseenter / mouseleave 鼠标移入 / 移出 鼠标悬浮图片放大效果
submit 表单提交时触发 拦截表单,调用 event.preventDefault() 阻止提交

8. 综合实战:表单验证与正则表达式

8.1 什么是正则表达式 (RegExp)?

正则表达式用于定义字符串的规则,常用于格式校验。

  • ^ 开始,$ 结束。
  • \d 数字,\w 字母/数字/下划线。
  • {m,n} 至少 m 次,至多 n 次。

创建与使用

// 定义一个规则:6到12位的字母/数字/下划线
let reg = /^\w{6,12}$/; 
// test() 方法返回 boolean
console.log(reg.test("abc123_")); // true

8.2 站长必修课:安全严谨的注册表单校验

在用户点击提交前,必须在前端拦截不合规的数据,减轻服务器压力!

<form id="reg-form" action="/api/register">
  <div>
    <label>用户名:</label>
    <input type="text" id="username" placeholder="6-12位英数">
    <span id="name-err" style="color:red; display:none;">用户名格式错误!</span>
  </div>
  <button type="submit">注册</button>
</form>

<script>
  const userInput = document.getElementById("username");
  const errSpan = document.getElementById("name-err");
  const form = document.getElementById("reg-form");

  // 1. 失去焦点时立刻校验
  function checkUsername() {
    const val = userInput.value.trim(); // 务必 trim 去空格!
    const reg = /^\w{6,12}$/;
    if (reg.test(val)) {
      errSpan.style.display = "none";
      return true;
    } else {
      errSpan.style.display = "inline";
      return false;
    }
  }
  
  // 使用现代 addEventListener 绑定失去焦点事件
  userInput.addEventListener("blur", checkUsername);

  // 2. 表单整体提交时的最后拦截
  form.addEventListener("submit", function(event) {
    let isValid = checkUsername(); 
    if (!isValid) {
      // 校验不通过,阻止表单默认的刷新跳转提交行为
      event.preventDefault(); 
    }
  });
</script>

9. 真正的“核心原理”入门

前面的内容解决的是“怎么写”,而这一节开始解决“为什么这样运行”。

很多初学者学完变量、函数、DOM、事件之后,代码能写,但一遇到这些问题就容易懵:

  • 为什么有些变量函数外拿不到
  • 为什么函数执行完了,里面的数据好像还“活着”
  • 为什么同一个函数在不同调用方式下,this 会变
  • 为什么任务明明先写在前面,打印顺序却在后面

这些问题背后,对应的就是 JavaScript 的几个核心原理。

9.1 作用域:变量到底在哪里能访问

作用域可以理解为:

一个变量在代码中的“可见范围”。

JavaScript 中最常见的是:

  • 全局作用域:在最外层声明,很多地方都能访问
  • 函数作用域:在函数内部声明,只能在函数内部访问
  • 块级作用域:用 letconst{} 中声明,只在当前代码块有效
const siteName = "Web Note";

function showMessage() {
  const msg = "Hello";

  if (true) {
    const inner = "作用域内部";
    console.log(siteName); // 可以访问外层
    console.log(msg); // 可以访问函数内部变量
    console.log(inner); // 可以访问当前块级变量
  }

  // console.log(inner); // 报错,块级作用域外无法访问
}

这就是为什么现代开发里更推荐 letconst,因为它们的作用域边界更清晰,能减少很多意外覆盖问题。

9.2 闭包:函数为什么能“记住”外部变量

闭包不是一个神秘语法,而是一种运行结果。

你可以先记住一句话:

当一个函数可以访问其外层作用域中的变量,并且在外层函数执行结束后依然保留这种访问能力,这种现象就叫闭包。

function createCounter() {
  let count = 0;

  return function () {
    count++;
    console.log(count);
  };
}

const counter = createCounter();
counter(); // 1
counter(); // 2
counter(); // 3

为什么 createCounter() 已经执行完了,count 还在?

因为返回出去的内部函数仍然引用着 count,所以这部分作用域不会马上被释放。

闭包的常见用途:

  • 封装私有变量
  • 让函数拥有自己的独立状态
  • 事件处理函数中保留上下文数据

9.3 this:函数执行时“当前对象”是谁

this 是 JavaScript 里最容易让人困惑的概念之一。

最重要的原则不是背定义,而是记住:

this 的值,通常取决于函数是如何被调用的,而不是它写在哪里。

const user = {
  name: "Alice",
  sayName() {
    console.log(this.name);
  }
};

user.sayName(); // Alice

这里的 sayName() 是通过 user 调用的,所以 this 指向 user

再看一个容易踩坑的例子:

const button = {
  text: "提交",
  click() {
    setTimeout(function () {
      console.log(this.text);
    }, 1000);
  }
};

上面这段代码里的普通函数,其 this 往往不会指向 button,所以很可能拿不到预期值。

这也是箭头函数常被用来解决问题的原因之一:

const button = {
  text: "提交",
  click() {
    setTimeout(() => {
      console.log(this.text);
    }, 1000);
  }
};

箭头函数不会创建自己的 this,它会沿用外层上下文里的 this

9.4 原型与原型链:对象为什么能访问“自己没有”的属性

JavaScript 中很多对象并不是彼此完全独立的,它们可以通过 原型链 共享属性和方法。

例如数组为什么能直接调用 .push()

const arr = [1, 2, 3];
arr.push(4);

并不是因为每个数组天生都单独保存了一份 push 方法,而是因为它们可以沿着原型链找到共享方法。

你可以粗略理解成:

  1. 先在对象自己身上找属性
  2. 自己没有,就沿着原型继续往上找
  3. 一直找到顶层为止

这也是为什么我们常说:

JavaScript 的对象系统,本质上是基于原型而不是传统类继承建立起来的。

在入门阶段,不必急着深挖底层细节,但一定要先建立这个认知:很多“对象方法”其实来自原型链。

9.5 事件循环:为什么异步代码的执行顺序会变

JavaScript 在浏览器中通常是单线程执行的,这意味着同一时间只能做一件事。

那为什么请求、定时器、点击事件这些异步任务还能工作?

答案就是 事件循环(Event Loop)

先看例子:

console.log("1");

setTimeout(() => {
  console.log("2");
}, 0);

console.log("3");

输出结果通常是:

1
3
2

原因不是 setTimeout(..., 0) 会立刻执行,而是:

  1. 主线程先执行同步代码
  2. 定时器回调先交给浏览器等待
  3. 当前同步任务执行完后,事件循环再把回调放回执行队列
  4. 主线程空闲后才会执行它

如果你后面继续学习 Promiseasync/await、Vue 更新机制,就会越来越频繁地遇到事件循环这个概念。


10. 进阶:ES6+ 现代前端基石

作为专业前端,不能只停留在老旧的 JS 语法上。ECMAScript 6 (ES6) 及其后续版本带来了质的飞跃。

10.1 模板字符串 (Template Literals)

告别繁琐的 + 号字符串拼接,使用反引号 `${}

let name = "张三";
let age = 22;
// 以前: "我叫" + name + ",今年" + age + "岁。"
// 现在:支持多行,且变量直接插入
let info = `我叫 ${name}今年 ${age} 岁。`;

10.2 解构赋值 (Destructuring)

快速从对象或数组中提取数据:

const person = { username: "Jack", city: "北京" };
// 直接将对象属性解构为独立变量
const { username, city } = person;
console.log(username); // "Jack"

10.3 箭头函数 (Arrow Functions)

极致精简函数的写法,且它不绑定自己的 this(解决了长期以来的 this 指向痛点):

// 传统写法
const sum1 = function(a, b) { return a + b; };

// 箭头函数写法(如果只有一行代码,可以省略花括号和 return)
const sum2 = (a, b) => a + b;

// 结合定时器极度优雅
setTimeout(() => console.log("执行了!"), 1000);

10.4 数组神技:Map、Filter 与 Reduce

  • map():对数组每一项加工,返回新数组。
  • filter():过滤出符合条件的元素,组成新数组。
  • reduce():对数组进行累加/汇总计算。
let arr = [1, 2, 3, 4, 5];
// 过滤出偶数,然后每个数字乘 10
let result = arr.filter(n => n % 2 === 0).map(n => n * 10); // [20, 40]

10.5 Promise 与异步操作 (async/await)

传统的 Ajax 依赖层层嵌套的“回调地狱”。ES6 引入了 Promise,而 ES8 的 async/await 更是让异步代码写起来像同步代码一样优雅。

// 现代 Fetch API 结合 async/await 示例
async function getUserData() {
  try {
    // await 会等待请求完成才执行下一行
    const response = await fetch("https://api.example.com/user");
    const data = await response.json();
    console.log("获取到用户数据:", data);
  } catch (error) {
    console.error("请求发生错误:", error);
  }
}

getUserData();

10.6 模块化 (Modules)

现代前端框架(Vue, React)的基础。允许将代码拆分成多个文件,互相导入导出。

// math.js
export const add = (a, b) => a + b;

// main.js
import { add } from './math.js';
console.log(add(10, 20));