JavaScript 零基础入门:引入方式、变量与数据类型,吃透原始与引用类型

写在前面

JavaScript 是浏览器的原生语言,也是当下 Web 开发不可绕过的地基。它的语法表面简单,却在"变量提升""引用传递""类型转换"这些底层机制上埋着大量坑------很多写了两三年的人,面对 typeof null === "object"0.1 + 0.2 !== 0.3 依然会答错。

本文将讲明白以下三件事:

  1. 怎么把 JS 代码跑起来(三种引入方式与执行时机);
  2. 怎么声明变量var / let / const 及背后的作用域与提升机制);
  3. 数据分几类、有什么区别(原始类型与引用类型,这是理解后续一切的核心)。

第三点是本文的重头戏。只有真正分清"值"和"引用",你才能解释为什么 const 声明的对象还能被修改、为什么两个看似相等的对象 === 却为 false


一、引入方式:让浏览器执行 JS 的三种途径

1.1 内联脚本:<script> 标签

在 HTML 中直接写 JS,最直接的方式:

html 复制代码
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>内联脚本</title>
</head>
<body>
  <h1>打开控制台(F12)查看输出</h1>

  <script>
    // 浏览器逐行执行这里的代码
    console.log("Hello, JavaScript!");
    let a = 10;
    console.log(a * 2); // 20
  </script>
</body>
</html>
  • 代码写在 </body> 之前,可以确保 DOM 已解析完成;
  • 脚本中的语法错误会导致整段脚本中断(后面的语句不执行)。

1.2 外部文件:<script src>

把 JS 独立成 .js 文件,用 src 引入,是工程化的标准做法:

html 复制代码
<script src="./js/main.js"></script>
js 复制代码
// js/main.js
console.log("外部脚本执行了");

推荐理由:结构分离、可缓存、可复用。代码量一大,内联脚本就是灾难。

1.3 内联事件属性(了解即可)

把 JS 直接写在 HTML 属性里(如 onclick)。能跑,但不推荐------HTML 与逻辑耦合,难以维护:

html 复制代码
<button onclick="alert('点击了')">点我</button>

1.4 加载时机:普通脚本、defer 与 async

普通 <script>同步阻塞的:浏览器遇到它必须下载并执行完,才继续渲染后面的 HTML。这导致:

  • 脚本放 <head> 会阻塞首屏渲染;
  • 脚本放 <body> 末尾可以避免阻塞,但脚本会等 DOM 解析完才执行。

deferasync 解决外部脚本的加载问题:

html 复制代码
<!-- defer:下载不阻塞,DOM 解析完成后按顺序执行 -->
<script defer src="./js/a.js"></script>
<script defer src="./js/b.js"></script>

<!-- async:下载不阻塞,下载完立刻执行(不保证顺序) -->
<script async src="./js/c.js"></script>
属性 下载是否阻塞 执行时机 顺序保证
阻塞 立即执行 按文档顺序
defer 不阻塞 DOM 解析完成后 按文档顺序
async 不阻塞 下载完成立即执行 不保证

现代工程实践:脚本一律放 <body> 末尾或加 defer;模块化项目则用 <script type="module">(自带 defer 语义)。


二、变量声明:var / let / const

2.1 三种声明方式

js 复制代码
var a = 1;      // ES5 时代的声明
let b = 2;      // ES6:块级作用域,可重新赋值
const c = 3;    // ES6:块级作用域,不可重新赋值

2.2 作用域:这是 var 与 let/const 最本质的区别

  • var函数作用域(或全局),没有块级概念;
  • let / const块级作用域{} 内有效)。
js 复制代码
if (true) {
  var x = 1;     // var 无视块级
  let y = 2;     // let 只在块内有效
}
console.log(x);  // 1 ------ 变量泄漏到了外面
console.log(y);  // ReferenceError: y is not defined

2.3 变量提升与暂时性死区

var 会提升声明,不会提升赋值

js 复制代码
console.log(a);  // undefined(不报错!)
var a = 10;

等价于:

js 复制代码
var a;           // 声明被提升到顶部,值为 undefined
console.log(a);  // undefined
a = 10;

let / const 存在暂时性死区(TDZ, Temporal Dead Zone):声明之前访问直接报错:

js 复制代码
console.log(b);  // ReferenceError: Cannot access 'b' before initialization
let b = 10;

从进入作用域到 let b = 10 执行完之前的这段区域就是 TDZ,期间访问变量会抛错。这比 var 的静默 undefined 更安全,能尽早暴露错误。

2.4 赋值规则:const 不变量,只防"重新绑定"

const 禁止的是重新赋值,而不是禁止修改值本身------这一点是高频考点:

js 复制代码
const obj = { name: "Tom" };
obj.name = "Jerry";   // ✅ 允许:修改对象的属性
obj = {};             // ❌ TypeError: Assignment to constant variable

因为 obj 变量保存的是对象的引用 (内存地址),const 锁定的是"地址不能变",对象内部怎么改都管不着。

2.5 实践原则

  • 默认用 const:变量一旦初始化就不要再重新绑定;
  • 确定需要重新赋值时才用 let
  • 不要用 var(除非在兼容 ES5 的老代码里)。

三、数据类型总览:七种原始 + 一种引用

ECMAScript 定义了 8 种数据类型:

类型 分类 说明
string 原始 字符串
number 原始 数值(含 NaN、Infinity)
boolean 原始 true / false
undefined 原始 声明未赋值
null 原始 显式空值
symbol 原始 唯一标识(ES6)
bigint 原始 任意精度整数(ES2020)
object 引用 对象(含数组、函数等)

原始类型(Primitive) :直接存值,复制即拷贝值。 引用类型(Reference):存内存地址,复制拷贝的是引用。

typeof 判断类型:

js 复制代码
typeof "hi";            // "string"
typeof 42;              // "number"
typeof true;            // "boolean"
typeof undefined;       // "undefined"
typeof null;            // "object"  ← 历史 bug,注意!
typeof Symbol();        // "symbol"
typeof 10n;             // "bigint"
typeof {};              // "object"
typeof [];              // "object"  ← 数组也是 object
typeof function() {};   // "function"

两个著名的坑:typeof null 返回 "object"(ES 规范遗留 bug,无法修复);数组用 typeof 也返回 "object"


四、原始类型详解

4.1 string(字符串)

三种写法,模板字符串支持插值与换行:

js 复制代码
const s1 = "单引号";
const s2 = '双引号';
const name = "Tom";
const s3 = `你好,${name},这是模板字符串`;

常用操作:

js 复制代码
const str = "hello world";
str.length;        // 11
str.toUpperCase(); // "HELLO WORLD"
str.includes("llo"); // true
str.slice(0, 5);   // "hello"
str.split(" ");    // ["hello", "world"]

4.2 number(数值)

包含三个特殊值:NaNInfinity-Infinity

浮点精度问题------必考

js 复制代码
0.1 + 0.2;        // 0.30000000000000004
0.1 + 0.2 === 0.3 // false

原因:小数在内存中以二进制存储,0.1 无法被二进制精确表示。解决方案:换算成整数运算,或用 toFixed 处理展示:

js 复制代码
// 方案一:转整数计算
(0.1 * 10 + 0.2 * 10) / 10;  // 0.3

// 方案二:只处理展示
(0.1 + 0.2).toFixed(2);      // "0.30"

NaN 是"不是数字的数字",NaN 不等于任何值,包括它自己

js 复制代码
NaN === NaN;        // false
isNaN("abc");       // true(会先做类型转换)
Number.isNaN("abc");// false(严格判断,推荐)

4.3 boolean 与真值判断

布尔只有 true / false,但任何值都可以转成布尔。假值(falsy)只有 6 个:

js 复制代码
false
0
""        // 空字符串
null
undefined
NaN

其余全是真值(truthy),包括 "0"" "[]{}

js 复制代码
if ([] && {}) { console.log("都是真值"); }  // 会执行

4.4 null 与 undefined 的区别

undefined null
语义 声明了但没赋值(系统给的"未定义") 主动声明"这里没有值"
typeof "undefined" "object"
出现场景 变量未赋值、函数无返回、访问不存在的属性 手动清空/占位
js 复制代码
let a;
console.log(a);     // undefined

const user = null;  // 开发者主动表示"暂无用户"

4.5 symbol 与 bigint(了解)

symbol 用于生成唯一标识(常用于对象私有属性 / 消除命名冲突):

js 复制代码
const s1 = Symbol("id");
const s2 = Symbol("id");
s1 === s2;          // false,即使描述相同也不相等

bigint 解决大整数精度问题:

js 复制代码
const big = 9007199254740993n;  // 尾部 n 表示 bigint
typeof big;         // "bigint"

五、引用类型详解

引用类型统称 object,常见形态:普通对象、数组、函数、日期、正则等。它们的共同点:变量保存的是堆内存中的地址

5.1 对象(Object)

js 复制代码
const user = {
  name: "Tom",
  age: 18,
  "爱好": "coding",       // 属性名可以是中文
};
user.age = 19;            // 修改属性
user.email = "t@e.com";   // 动态添加属性
delete user.age;          // 删除属性

访问属性两种方式:user.name(点)与 user["name"](方括号,可接变量)。

5.2 数组(Array)

数组本质是"特殊对象",下标即属性名:

js 复制代码
const arr = [1, 2, 3];
arr.push(4);      // [1, 2, 3, 4] 尾部追加
arr.pop();        // 删除尾部
arr.map(x => x * 2); // [2, 4, 6] 不修改原数组
arr.filter(x => x > 1); // [2, 3]

5.3 函数(Function)

函数也是对象,可以被赋值、传参、作为返回值(即"一等公民"):

js 复制代码
const add = function (a, b) {
  return a + b;
};
const result = add(1, 2);   // 3

箭头函数是 ES6 的简洁写法:

js 复制代码
const add = (a, b) => a + b;

六、原始 vs 引用:核心区别与传值机制

这是全篇最重要的部分。

6.1 赋值行为:拷贝值 vs 拷贝引用

原始类型赋值 = 值拷贝,两个变量互不影响:

js 复制代码
let a = 10;
let b = a;      // 把 a 的值复制一份给 b
b = 20;
console.log(a); // 10 ------ a 没变

引用类型赋值 = 引用拷贝,两个变量指向同一对象:

js 复制代码
const obj1 = { count: 1 };
const obj2 = obj1;      // 拷贝的是地址,不是对象
obj2.count = 999;
console.log(obj1.count); // 999 ------ obj1 也变了!

内存示意:对象本身在堆内存,变量只存堆地址。obj2 = obj1 让两个变量持有同一个地址。

6.2 函数参数:也是"值传递",但引用类型传的是"地址值"

js 复制代码
function change(x) {
  x = 100;              // 重新绑定参数,不影响外部
}
let n = 1;
change(n);
console.log(n);         // 1

function modify(o) {
  o.name = "Jerry";     // 修改对象内部,外部可见
}
const u = { name: "Tom" };
modify(u);
console.log(u.name);    // "Jerry"

结论:参数永远是值传递------原始类型传值,引用类型传"地址值"。所以"改了对象的属性,函数外也能看到",但"把参数重新赋值成别的对象"不影响外部。

6.3 相等性比较:== / === / Object.is

  • ==:比较前会类型转换
  • ===:类型和值都相同才相等(推荐默认使用);
  • Object.is:类似 ===,但能正确区分 NaN±0
js 复制代码
1 == "1";        // true(类型转换后相等)
1 === "1";       // false(类型不同)

null == undefined;  // true(== 的经典特例)
null === undefined; // false

NaN === NaN;     // false
Object.is(NaN, NaN); // true

0 === -0;        // true
Object.is(0, -0);   // false

引用类型的 === 比较的是地址

js 复制代码
const o1 = { a: 1 };
const o2 = { a: 1 };
o1 === o2;       // false ------ 内容一样,但不是同一个对象
const o3 = o1;
o1 === o3;       // true ------ 同一地址

6.4 深拷贝与浅拷贝

引用类型赋值(=)只拷贝引用,这叫"浅"到极点。所谓浅拷贝:只复制一层,嵌套对象仍是共享的:

js 复制代码
const original = { a: 1, nested: { b: 2 } };

// 浅拷贝
const shallow = { ...original };
shallow.nested.b = 999;
console.log(original.nested.b); // 999 ------ 嵌套对象仍共享

// 深拷贝(结构化克隆,可处理 JSON 数据)
const deep = JSON.parse(JSON.stringify(original));
deep.nested.b = 888;
console.log(original.nested.b); // 2 ------ 完全独立

JSON.parse(JSON.stringify()) 是最常用的深拷贝手段,但有局限(丢失 undefined、函数、Date 等)。生产环境可用 structuredClone()(浏览器原生支持)或第三方库。


七、类型转换

7.1 显式转换

js 复制代码
String(123);      // "123"
Number("42");     // 42
Number("abc");    // NaN
Boolean(0);       // false
Boolean("hi");    // true
parseInt("12px"); // 12
parseFloat("3.14"); // 3.14

7.2 隐式转换(最常见的坑)

js 复制代码
"1" + 2;      // "12"   ------ 字符串拼接优先
"1" - 0;      // 1      ------ 减法触发数值转换
"5" * "2";    // 10
+ "3";        // 3      ------ 一元加号转数值

[] + [];      // ""     ------ 空数组转字符串
{} + [];      // 0 或 "[object Object]"(看上下文)

规避策略:类型不确定时先显式转换 ,比较一律用 ===


八、实战练习

实战 1:可靠的类型判断函数

typeofnull 和数组不可靠,封装一个更准确的判断:

js 复制代码
function getType(value) {
  // Object.prototype.toString 返回 "[object Array]" 之类
  return Object.prototype.toString.call(value).slice(8, -1);
}

getType(null);        // "Null"
getType([]);          // "Array"
getType({});          // "Object"
getType(42);          // "Number"
getType("hi");        // "String"
getType(undefined);   // "Undefined"

实战 2:手写深拷贝(递归版)

js 复制代码
function deepClone(source) {
  if (source === null || typeof source !== "object") {
    return source;                     // 原始类型直接返回
  }
  const result = Array.isArray(source) ? [] : {};
  for (const key in source) {
    result[key] = deepClone(source[key]);  // 递归处理嵌套
  }
  return result;
}

const a = { list: [1, 2, { x: 3 }] };
const b = deepClone(a);
b.list[2].x = 99;
console.log(a.list[2].x);  // 3 ------ 互不影响

实战 3:变量作用域与提升演示

js 复制代码
console.log(v);      // undefined(var 提升)
var v = 1;

if (true) {
  var leak = "leak";
  let safe = "safe";
}
console.log(leak);   // "leak" ------ var 泄漏
// console.log(safe); // ReferenceError

for (var i = 0; i < 3; i++) { /* ... */ }
console.log(i);      // 3 ------ var 泄漏到循环外

for (let j = 0; j < 3; j++) { /* ... */ }
// console.log(j);   // ReferenceError

九、高频误区清单

误区 正解
typeof null"object" ES 历史遗留 bug,判断空值用 value === null
var 提升后值为 undefined 是"声明提升、赋值不提升"
0.1 + 0.2 === 0.3 false,浮点精度问题,整数化运算
const 对象不能改属性 能改,const 只禁止重新绑定变量
两个 {a:1}=== 相等 false,比较的是内存地址
== 比较"值" 会先做隐式类型转换,默认用 ===
NaN === NaN false,用 Number.isNaNObject.is

十、总结

用一张表收束本文核心:

维度 原始类型 引用类型
包含 string / number / boolean / null / undefined / symbol / bigint object(数组、函数等)
存储 栈,直接存值 堆,变量存地址
赋值 拷贝值,互不影响 拷贝引用,共享对象
比较 比较值 比较地址
传参 传值 传地址值(改内部可见)
常见坑 浮点精度、NaN 浅拷贝、意外共享

三条实践心法收尾:

  1. 声明 :默认 const,需要重绑才用 let,放弃 var
  2. 比较 :一律 ===,判空用 value === null,判 NaN 用 Number.isNaN
  3. 拷贝:知道"等号只拷引用",需要独立数据时主动做深拷贝。
相关推荐
不可能片场17 分钟前
puppeteer 调试实录:CDP 双层响应让取值永远 None
前端·electron
咖啡无伴侣18 分钟前
2. 从零搭建企业级 Monorepo 工程化模板:ESLint 10 (基础骨架)+ Prettier 配置与避坑指南
前端·架构
光影少年22 分钟前
react navite实现全局弹窗、Toast 组件
前端·react native·react.js
WebInfra26 分钟前
Rslib 1.0 正式发布:面向多场景的 JavaScript 库开发工具
前端·javascript·github
Tongsr1 小时前
别只混淆代码:用 Kaleido 加固整个 Android Release AAB
前端·算法·github
郭邯1 小时前
从零到一:我用 AI 写了一个贷款计算器,顺便把等额本息公式彻底搞懂了
前端
汉堡大王95271 小时前
面试官:讲讲归并排序 —— 为什么它能在面试里反复出现?
前端·javascript·面试
kyriewen1 小时前
别再只给人写页面了:AI 已经开始自己点你的按钮、填你的表单
前端·javascript·人工智能
天天喝旺仔1 小时前
Vue 3 组合式 API:从 Options 迁移到 script setup
前端·javascript·vue.js