这篇也是 2024 年刚开始学前端时记下的,分量比 HTML 和 CSS 那两篇重得多——笔记本身就把大纲写成了三块:ECMAScript 语法、DOM、BOM,后面还补了 JSON。
写这篇的时候我已经知道「Web 三件套」里的 JS 才是重头戏,所以记得格外细,连 arguments、this 指向、事件冒泡这些都没放过。现在回看,这些依然最容易被问到、也最容易被自己忘掉。
内容还是基础中的基础,留个档。
编写方式与注意事项#
三种编写位置#
- 行内写在 HTML 标签上(不推荐)。
- 写在
<script>标签里。 - 写成外部
.js文件,通过<script>的src属性引入。
noscript 元素#
<noscript></noscript> 用于给不支持(或被关闭)JavaScript 的浏览器提供替代内容。
四条注意事项#
<script>不能写成单标签。外链引入 JS 文件时,标签里不能再写代码,也不能写成<script src="index.js" />。- 可以省略
type。以前要写type="text/javascript",现在不用了,JavaScript 是所有现代浏览器和 HTML5 的默认脚本语言。 - 加载顺序。作为 HTML 文档的一部分,JS 默认遵循自上而下的加载顺序。
- JS 严格区分大小写,这一点和 HTML 元素、CSS 属性都不一样。
浏览器交互方式#
alert("Hello World"); // 弹窗
console.log("Hello World"); // 输出到控制台
document.write("Hello World"); // 写进页面
var result = prompt("请输入你的名字: "); // 接收用户输入
alert("您刚才输入的内容是:" + result);基本语法#
变量命名方法#
用 var 关键字声明变量(variable 的缩写),后续还有 ES6 的 let 和 const。
var name = "genshin";常见的数据类型#
JavaScript 是动态类型语言,常见类型有八个:
- Number:整数或浮点数。支持加减乘除等运算,还包括两个特殊值——
Infinity(无穷大,比如1/0)和NaN(代表一个计算错误,比如字符串和数字相乘)。字面量可以写十进制、十六进制(0x)、二进制(0b)、八进制(0o)。- 最小正数值
Number.MIN_VALUE是5e-324,比它小的数会被转成 0;最大正数值Number.MAX_VALUE是1.7976931348623157e+308。 isNaN用来判断「是不是不是一个数字」:不是数字返回true,是数字返回false。
- 最小正数值
- String:字符串,可以包含 0 个或多个字符,所以没有单独的单字符类型。字符串要用双引号、单引号或反引号包起来,前后符号一致;转义字符用
\;拼接用+,或者用反引号和${变量/表达式};长度用name.length。 - Boolean:
true和false。 - Undefined:只有一个值
undefined的独立类型,用于未初始化的值。变量定义时最好直接初始化;不要显式把变量赋值为undefined,刚开始什么都没有时可以初始化为 0、空字符串或null。 - Null:只有一个值
null的独立类型,通常表示一个对象为空,所以对象初始化时常赋null。 - Object:用于更复杂的数据结构。其他类型通常称为「原始类型」,因为它们的值只包含一个单独的内容;对象往往表示一组数据的集合,用花括号
{}表示。
var person = {
name: "why",
age: 18,
height: 1.88,
};- BigInt:用于任意长度的整数。
- Symbol:用于唯一的标识符。
typeof 操作符#
- 作用:确定任意变量的数据类型。
- 用法:
typeof(x)或typeof x。 - 注意:typeof 是一个操作符,不是函数,
()只是把后面的内容当成一个整体。
数据类型的转换#
转成 String
- 隐式转换:和其他类型做
+运算,左右两边有一个是字符串,另一边就会自动转成字符串再拼接。
var num1Str = num1 + "";- 显式转换:调用
String()函数,或者调用toString()方法。
var num1Str2 = String(num1);转成 Number
- 隐式转换:算术运算中通常会把其他类型转成数字再计算,比如
"6" / "2"得到 3。 - 显式转换:使用
Number()函数。
console.log(Number(undefined)); // NaN
console.log(Number(true)); // 1
console.log(Number(false)); // 0
console.log(Number(null)); // 0
console.log(Number("abc123")); // NaN
console.log(Number(" 123 ")); // 123
console.log(Number("")); // 0转成 Boolean
- 调用
Boolean(value)。 - 直观上为「空」的值(
0、空字符串、null、undefined、NaN)会变成false,其他值都是true。
运算符#
- 算术运算符:
+-*/%**,分别是加、减、乘、除(有小数)、取余、求幂。 - 赋值运算符:
=;支持链式赋值(var a = b = c = 2 + 3);原地修改有+=-=*=/=%=**=。 - 自增自减:
a++、++a。注意5++、++5这种写法会报错,因为自增只能作用于变量。 - 比较运算符:
><>=<===!=。==两侧的值会先被转成数字再比较,空字符串和false也不例外(都转成 0)。===比较时不做任何类型转换。!=和!==同理。
分支语句#
if 分支
- 单分支
if...;多分支if...else...、if...else if...else。 if(...)会计算括号里的表达式并转成布尔值:0、""、null、undefined、NaN都会转成false,其他值都是true。
三元运算符
var result = condition ? value1 : value2;常用场景是赋默认值,防止外来变量是 undefined:
var info = { name: "why" };
var obj = info ? info : {};
console.log(obj); // why逻辑运算符
||(或):从左到右依次计算,每个操作数都先转成布尔值;遇到第一个真值就停止并返回它的初始值,如果全是假值,就返回最后一个操作数。- 记住一句话:或运算返回第一个真值,没有真值就返回最后一个值;返回的是操作数的初始形式,不会被转成布尔类型。
var info = "abc";
var obj = { name: "why" };
var message = info || obj || "我是默认值";
console.log(message.length); // abc&&(与):同样从左到右,遇到第一个假值就停止并返回它的初始值,全是真值则返回最后一个。- 一句话:与运算返回第一个假值,没有假值就返回最后一个值。
obj && obj.friend && obj.friend.eating && obj.friend.eating();!(非):先把操作数转成布尔类型,再返回相反的值。两个非运算!!常用来把某个值转成布尔类型。
switch 语句
var btnIndex = 0;
switch (btnIndex) {
case 0:
console.log("点击了上一首");
break;
case 1:
console.log("点击了播放/暂停");
break;
case 2:
console.log("点击了下一首");
break;
default:
console.log("当前按钮的索引有问题~");
break;
}- case 穿透:一条 case 语句结束后,会自动执行下一个 case 的语句。
break关键字用来解决穿透问题,在每个 case 的代码块后加上它。- 注意:这里的相等是严格相等,被比较的值必须类型也相同才能匹配。
循环#
while循环、do...while循环、for循环。break跳出循环,continue立刻进入下一次循环。
函数#
定义与调用#
function 函数名(形参) {
// 函数封装的代码
}
函数名(实参);形参和实参#
- 形参(parameter):定义函数时小括号里的参数,在函数内部当变量用。
- 实参(argument):调用函数时小括号里的参数,用来把数据传进函数内部。
返回值#
- 用
return返回结果。 - 一旦执行 return,当前函数就会终止。
- 没有 return 语句,函数的默认返回值是
undefined;return 后面没有值,返回值同样是undefined。
arguments 对象#
- 所有非箭头函数里都可以使用的局部变量。
- 里面按顺序存放着调用者传入的所有实参,从 0 开始。
- 类型是 object(array-like),不是数组,但用法看起来很像。
- 调用者传入的参数多于函数接收的参数时,可以用它把多出来的参数都取到。
function sum() {
var total = 0;
for (var i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}
console.log(sum(10, 20)); // 30
console.log(sum(10, 20, 30, 40)); // 100递归#
函数自己调用自己:
function pow(x, n) {
if (n === 1) return x;
return x * pow(x, n - 1);
}局部变量与全局变量#
- 局部变量:函数内定义,只有函数内部能访问。
- 全局变量:函数外、script 内定义,在任何函数中都可见。
- 访问变量时优先找自己函数里的,找不到再往外找,都没有就报错。
- 用
var声明的全局变量,会在 window 对象上添加一个同名属性。
作用域#
- 变量能在哪个范围内使用,这个范围就叫它的作用域(scope)。
- ES5 之前没有块级作用域,
var定义的变量没有块级作用域,比如 for 循环的代码块也没有自己的作用域——在 for 里用var定义的变量,for 外面照样能访问。 - ES5 之前只有函数代码块会形成自己的作用域。
函数表达式#
不管函数怎么创建,它本身都是一个值,类型是对象。
var bar = function () {
// 省略函数名
console.log("bar 函数被执行了~");
};函数声明与函数表达式#
- 函数声明:在主代码流中声明为单独语句的函数。它在被定义之前就可以调用——JS 准备运行脚本时会先找出全局函数声明并创建它们。
- 函数表达式:在表达式或其他语法结构中创建的函数,代码执行到它时才被创建,从那一刻起可用。
头等公民#
函数可以:赋值给变量、在变量之间传递、作为另一个函数的参数、作为另一个函数的返回值、存进其他数据结构。
var foo1 = function () {
console.log("foo1 函数被执行~");
};
var foo2 = foo1;
foo2();
function bar(fn) {
fn();
}
bar(foo1);
function sayHello() {
function hi() {
console.log("hi kobe");
}
return hi;
}
var fn = sayHello();
fn();回调函数与匿名函数#
function foo(fn) {
// 通过 fn 调用 bar,这个过程叫函数的回调
fn();
}
function bar() {
console.log("bar 函数被执行了~");
}
foo(bar);- 高阶函数:接受一个或多个函数作为输入,或者输出一个函数。
- 匿名函数:传入函数时没有指定名字,也没有通过函数表达式指定对应的变量。
request("url", function (res) {
console.log("拿到结果:", res);
});立即执行函数#
- 函数定义完后立即执行。前半部分是定义了一个匿名函数(有自己的独立作用域),后面的
()表示执行它。 - 作用:创建一个独立的执行上下文环境,避免外部访问或修改内部的变量。
(function () {
console.log("立即执行函数被调用~");
})();
// 其他写法(了解)
(function (fn) {
console.log("立即执行函数被调用");
})();
+(function foo() {})();对象#
什么是对象#
把现实世界抽象成对象,比如一辆车(属性:颜色、重量;方法:行驶)。对象用 {...} 创建,里面是键值对:key 是字符串类型(ES6 之后可以是 Symbol),value 可以是任意类型。
创建方法#
// 1. 对象字面量
var obj1 = { name: "why" };
// 2. new Object() 后动态添加属性
var obj2 = new Object();
obj2.name = "kobe";
// 3. new 其他类
function Person() {}
var obj3 = new Person();对象操作#
- 访问属性:用
.或[]。 - 修改属性:直接改。
- 添加属性:直接加。
- 删除属性:用
delete操作符。
var info = {
name: "why",
age: 18,
friend: { name: "kobe", age: 30 },
running: function () {
console.log("running~");
},
};
// 访问
console.log(info.name);
console.log(info.friend.name);
info.running();
// 修改
info.age = 25;
// 添加
info.height = 1.88;
info.studying = function () {
console.log("I am studying~");
};
// 删除
delete info.age;
delete info.height;方括号#
属性名里有空格之类的特殊字符时,只能用方括号访问:
info["my friend"];对象的遍历#
Object.keys(对象) 会返回一个由对象自身可枚举属性组成的数组。
var infoKeys = Object.keys(info);
for (var i = 0; i < infoKeys.length; i++) {
var key = infoKeys[i];
console.log(`key: ${key}, value: ${info[key]}`);
}
// for in
for (var key in info) {
console.log(`key: ${key}, value: ${info[key]}`);
}栈内存和堆内存#
- 原始类型占据的空间在栈内存中分配,也叫值类型。
- 对象类型占据的空间在堆内存中分配,也叫引用类型。
this 指针#
- 普通函数被默认调用时,this 指向 window。
- 函数被某个对象引用并调用时,this 指向那个对象。
目前掌握两个判断方法:全局环境下 this 指向 window;通过对象调用,this 指向调用的对象。
工厂函数#
封装一个函数来帮我们创建对象,创建出来的是 object 类型。
function createStudent(name, age, height) {
var stu = {};
stu.name = name;
stu.age = age;
stu.height = height;
stu.running = function () {
console.log("running~");
};
return stu;
}
var stu1 = createStudent("why", 18, 1.88);构造函数(类)#
- 用一个函数帮我们创建对象,创建出来的是想要的类型(比如 student 类型,而不是 object 类型)。
- 普通函数被
new操作符调用,这个函数就是构造函数,扮演的是类的角色。
function Coder(name, age, height) {
this.name = name;
this.age = age;
this.height = height;
this.running = function () {
console.log("running~");
};
}
var stu1 = new Coder("why", 18, 1.88);函数被 new 调用时会执行这几步:
- 在内存中创建一个新的空对象;
- 这个对象内部的
[[prototype]]属性被赋值为构造函数的prototype属性(后面细讲); - 构造函数内部的 this 指向这个新对象;
- 执行函数体代码;
- 如果构造函数没有返回非空对象,就返回创建出来的新对象。
内置类#
原始类型的包装类#
原始类型不是对象,按理没法获取属性或调用方法,但:
var str = "genshin";
console.log(str.length); // 7这是因为 JS 为它们封装了对应的包装类型,也可以显式地写成:
var str = new String("genshin");常见的包装类型有 String、Number、Boolean、Symbol、BigInt;null 和 undefined 没有包装类。
Number 类#
Number.MAX_SAFE_INTEGER/Number.MIN_SAFE_INTEGER:JS 中最大 / 最小的安全整数,即2^53 - 1。Number.toString(base):把数字转成字符串,并按 base 进制转换(2 到 36,默认 10)。直接对数字字面量操作时要注意写法,比如(10).toString(2)。Number.toFixed(digits):保留 digits 位小数(0 到 20)。Number.parseInt(string[, radix])/Number.parseFloat(string):把字符串解析成整数 / 浮点数,也有对应的全局方法parseInt、parseFloat。
Math 对象#
Math.PI:圆周率,约等于 3.14159。Math.floor:向下取整;Math.ceil:向上取整;Math.round:四舍五入。Math.random:生成 0 到 1 的随机数(包含 0,不包含 1)。Math.pow(x, y):返回 x 的 y 次幂,也可以用运算符**。
String 类#
字符串不可修改。
- 访问字符:
str[0](没找到返回undefined)、str.charAt(pos)(没找到返回空字符串)。 str.length:长度。str.toLowerCase()/str.toUpperCase():转小写 / 大写。str.indexOf(searchValue[, fromIndex]):从 fromIndex 开始查找,找不到返回 -1;lastIndexOf从后往前找。str.includes(searchString[, position]):从 position 开始查找,返回 true / false。str.startsWith(searchString[, position])/str.endsWith(searchString[, length]):判断是否以某段字符串开头 / 结尾。str.replace(substr, newSubstr):找到对应字符串并替换。- 截取字符串:
slice(start, end):从 start 到 end(不含 end),允许负值。substring(start, end):从 start 到 end(不含 end),负值代表 0。substr(start, length):从 start 开始取 length 长,允许 start 为负。
str.concat(str2[, ...strN]):拼接字符串。str.trim():删除首尾空白。str.split([separator[, limit]]):按 separator 分割(也可以是正则),limit 限制返回片段的数量。
数组 Array#
数组和对象一样是保存多个数据的数据结构,区别是它有序。
// 方法一
var names = ["why", "kobe", "james", "curry"];
var products = [
{ name: "鼠标", price: 98 },
{ name: "键盘", price: 100 },
{ name: "西瓜", price: 20 },
];
// 方法二
var arr1 = new Array();
var arr2 = new Array("abc", "cba", "nba");
console.log(names[0]); // 第一个元素
console.log(names[names.length - 1]); // 最后一个元素- 访问元素:用中括号
[],或者arr.at(i);i为负数时从数组尾部访问,比如arr.at(-1)是最后一个元素。注意arr[-1]这种写法取不到元素(结果是undefined),负数索引要用at。 - 修改元素:
arr[0] = "genshin"。 - 添加或删除:
- 直接索引添加会有空元素,比如
names[10] = "james",中间的位置会空出来。 delete names[0]可以删除,但会留下空槽。push/pop:末尾添加 / 删除。unshift/shift:首端添加 / 取出,其他元素整体后移 / 前移。
- 直接索引添加会有空元素,比如
splice(start, deleteCount, item):添加、删除、替换三合一。
arr.splice(1, 1); // 删除位置 1 的元素
arr.splice(1, 0, "a", "b"); // 从位置 1 开始添加 2 个元素
arr.splice(1, 2, "a", "b"); // 从位置 1 开始替换 2 个元素length属性:数组变化时自动更新;直接改arr.length,变长会补空元素,变短会直接删掉多出来的元素。- 遍历:
for (var i = 0; i < names.length; i++) {
console.log(names[i]);
}
for (var index in names) {
// 遍历键
console.log(index, names[index]);
}
for (var item of names) {
// 遍历值
console.log(item);
}- 其他方法:
arr.slice(start, end):截取数组。arr.concat(arr1, arr2...):拼接数组并返回。arr.join("字符"):把数组元素连接成字符串,不填默认用逗号。arr.indexOf(x, s)/arr.includes(x, s):从 s 位置开始找 x。find/findIndex:查找元素或元素索引。
var stu = students.find(function (item) {
if (item.id === 101) return true;
});sort([排序规则函数]):排序,默认按字符串比较;它会原地排序并返回同一个数组。reverse():把数组元素位置颠倒。- 高阶函数:
forEach:遍历数组,每个元素执行一次回调,没有返回值。map:返回一个新数组,新数组由每个元素调用函数后的返回值组成。filter:返回一个新数组,只包含调用函数返回true的元素。
let arr = [1, 2, 3, 4, 5, 6];
arr = arr.filter((item) => item % 2 === 0); // [2, 4, 6]reduce:把数组元素汇总成一个值。
arr.reduce((sum, item) => sum + item, 0);
// 第二个参数 0 是初始值,会作为第一次调用的 sumevery:数组里每个元素都符合要求才返回true。
arr.every((item) => item.isFlag);时间 Date#
创建 Date 对象:
new Date();
new Date(value);
new Date(dateString);
new Date(year, monthIndex[, day[, hours[, minutes[, seconds[, milliseconds]]]]]);日期的表示方式有两种:RFC 2822 标准和 ISO 8601 标准。
var date = new Date();
console.log(date); // RFC 2822 标准
console.log(date.toDateString());
console.log(date.toISOString()); // ISO 8601 标准获取信息的方法:
getFullYear():年份(4 位)。getMonth():月份,从 0 到 11。getDate():当月的日期,从 1 到 31(名字有点迷)。getHours()/getMinutes()/getSeconds()/getMilliseconds():时、分、秒、毫秒。getDay():一周中的第几天,从 0(星期日)到 6(星期六)。
设置信息的方法:setFullYear、setMonth、setDate、setHours、setMinutes、setSeconds、setMilliseconds、setTime。
获取时间戳:
new Date().getTime();
new Date().valueOf();
+new Date();
Date.now();Date.parse(str) 可以从字符串中读取日期,并输出对应的 Unix 时间戳。
DOM#
什么是 DOM 和 BOM#
- DOM:文档对象模型(Document Object Model),把页面所有内容表示为可以修改的对象。把 HTML 抽象成 DOM 对象时,它们会形成一个树结构,类型之间有继承关系。
- BOM:浏览器对象模型(Browser Object Model),由浏览器提供的、用于处理文档(document)之外所有内容的其他对象。
document 对象#
它是 DOM 的入口点,从 document 开始可以访问任何节点元素。
<html>=document.documentElement<body>=document.body<head>=document.head<!DOCTYPE html>=document.doctype
节点之间的导航#
拿到一个节点后,可以据此获取其他节点:
- 父节点:
parentNode - 前兄弟节点:
previousSibling,后兄弟节点:nextSibling - 子节点:
childNodes - 第一个子节点:
firstChild,最后一个子节点:lastChild
元素之间的导航#
拿到一个元素后,可以据此获取其他元素:
- 父元素:
parentElement - 前兄弟元素:
previousElementSibling,后兄弟元素:nextElementSibling - 子元素:
children - 第一个子元素:
firstElementChild,最后一个子元素:lastElementChild
两套导航的区别在于:节点(Node)那一套包含文本节点、注释节点,元素(Element)那一套只包含元素。
表格元素的导航#
<table>:table.rows是<tr>的集合;table.caption/tHead/tFoot分别引用<caption>、<thead>、<tfoot>;table.tBodies是<tbody>的集合。<thead>、<tfoot>、<tbody>:提供rows属性,比如tbody.rows。<tr>:tr.cells是行内<td>和<th>的集合;tr.sectionRowIndex是在所属<thead>/<tbody>/<tfoot>中的位置;tr.rowIndex是在整个表格中的编号。<td>和<th>:td.cellIndex是在所属<tr>中的编号。
表单元素的导航#
<form>可以通过document.forms获取。- 表单里的内容通过
form.elements获取。 - 也可以给表单子元素设置
name,再用名字取。
获取 DOM 元素#
querySelector/querySelectorAll:通过选择器获取。getElementById:通过 id 获取。getElementsByName:通过表单 name 获取。getElementsByTagName:通过标签获取。getElementsByClassName:通过 class 获取。
节点的常见属性#
- 节点类型(数字):
nodeType。

- 节点类型名称:
nodeName获取节点名字;tagName获取标签名,只适用于元素节点。 - 节点数据:
data/nodeValue获取非元素节点的文本内容。 innerHTML:获取元素内部的所有内容;textContent:只获取内部的文字内容;outerHTML:获取元素内部的所有内容加上自己。
元素的一些属性#
hidden
全局属性,用于隐藏元素:
box.hidden = true;attribute(元素的属性)
- 分类:标准的 attribute(
id、class、href、type、value等)和非标准的 attribute(自定义的abc、age、height等)。 - 常用方法:
hasAttribute(name)检查是否存在、getAttribute(name)获取值、setAttribute(name, value)设置值、removeAttribute(name)移除、attributes拿到特性对象的集合。 - 特征:大小写不敏感;值总是字符串。
property(节点中元素的属性)
- 标准的 attribute 会在 DOM 对象上创建对应的 property,可以通过
elem.property访问。 - property 和 attribute 操作会互相影响:改 property,用 attribute 读到的新值也跟着变,反之亦然。
- 大多数情况用 property 的方式设置和获取;只有 input 的 value 值习惯用 attribute 操作。
class 与 style
elem.className; // class attribute 对应的 property 叫 className,不叫 class
elem.classList.add("cls"); // 添加类
elem.classList.remove("cls"); // 移除类
elem.classList.toggle("cls"); // 不存在就添加,存在就移除
elem.classList.contains("cls"); // 返回 true / falseclassList是一个特殊的对象,可以用 for of 遍历。style:多词属性要用驼峰式(background-color写成backgroundColor);把值设为空字符串会使用 CSS 的默认样式;要写多条样式可以用cssText。
boxEl.style.cssText = "font-size: 30px; color: red;";- 内联样式可以通过
style.读到;写在 CSS 文件里的样式要用getComputedStyle。
getComputedStyle(boxEl).fontSize;data-自定义属性名:通过dataset读取。
<div id="abc" class="box" data-age="18" data-height="1.88"></div>
<script>
var boxEl = document.querySelector(".box");
console.log(boxEl.dataset.age);
console.log(boxEl.dataset.height);
</script>创建元素#
document.write:不利于处理复杂情况。- 通过
innerHTML手动添加。 document.createElement(tag):
var h2El = document.createElement("h2");
h2El.className = "title";
h2El.classList.add("active");
h2El.textContent = "我是标题";插入元素#
node.append(...nodes or strings):在 node 末尾插入。node.prepend(...):在 node 开头插入。node.before(...):在 node 前面插入。node.after(...):在 node 后面插入。node.replaceWith(...):把 node 替换成给定的节点或字符串。

移除与克隆#
- 移除元素:调用元素本身的
remove方法。 - 克隆元素:
elem.cloneNode(true/false),true表示深度克隆,会连子元素一起克隆。
元素的大小与滚动#
clientWidth/clientHeight:content + padding(不包含滚动条)。clientTop/clientLeft:上 / 左边框的宽度。offsetWidth/offsetHeight:元素完整的宽度 / 高度。offsetLeft/offsetTop:距离父元素的 x / y。scrollHeight:整个可滚动区域的高度。scrollTop:已经滚动部分的高度。
window 的大小与滚动#
innerWidth/innerHeight:window 窗口的宽高(包含滚动条)。outerWidth/outerHeight:整个窗口的宽高(包括调试工具、工具栏)。documentElement.clientWidth/clientHeight:html 的宽高(不包含滚动条)。scrollX/scrollY:X / Y 轴滚动的位置(别名是pageXOffset/pageYOffset)。scrollBy(x, y):相对当前位置滚动;scrollTo(pageX, pageY):滚动到绝对坐标。
事件的监听方式#
- 在 script 中直接监听(很少用)。
- 通过元素的
on属性监听,比如onclick。 - 通过 EventTarget 的
addEventListener监听(推荐)。
<button onclick="console.log('按钮1发生了点击~');">按钮1</button>
<script>
box.onclick = function () {
console.log("box 发生了点击~");
};
btn.addEventListener("click", function () {
console.log("btn 的事件监听~");
});
</script>常见的事件类型#
- 鼠标事件:
click、mouseover/mouseout、mousedown/mouseup、mousemove。 - 键盘事件:
keydown/keyup。 - 表单事件:
submit、focus。 - Document 事件:
DOMContentLoaded。 - CSS 事件:
transitionend。
事件冒泡与事件捕获#
点击一个元素时,点击的不仅仅是这个元素本身:默认情况下事件从最内层向外依次传递,这个顺序叫事件冒泡;反过来从外层到内层(body → span)叫事件捕获。
element.addEventListener("事件", function () {}, true); // 第三个参数 true 表示捕获阶段事件对象#
事件发生时,浏览器会创建一个 Event 对象,把它作为参数传给处理函数。
常见属性:
type:事件类型。target:当前事件发生的元素。currentTarget:当前处理事件的元素。eventPhase:事件所处的阶段。offsetX/offsetY:事件在元素内的位置。clientX/clientY:事件在客户端内的位置。pageX/pageY:事件相对于 document 的位置。screenX/screenY:事件相对于屏幕的位置。
常见方法:
preventDefault():取消事件的默认行为。stopPropagation():阻止事件进一步传递(冒泡和捕获都能阻止)。
在事件处理函数里,this 指向当前绑定事件的元素。
EventTarget#
- 所有节点、元素都继承自 EventTarget,Window 也是。
- EventTarget 是一个 DOM 接口,主要用于添加、删除、派发事件。
- 常见方法:
addEventListener(注册事件类型和处理函数)、removeEventListener(移除)、dispatchEvent(派发事件)。
事件委托#
事件冒泡衍生出的模式:子元素被点击时,父元素可以通过冒泡监听到;再通过 event.target 拿到真正被点的元素。
比如一个 ul 里放着多个 li,点击某个 li 让它变红:可以给每个 li 都加监听,也可以只在 ul 上监听一次,再通过 event.target 处理对应的 li——后者就是事件委托。
鼠标事件#
click:点击。contextmenu:右键打开上下文菜单时触发。dblclick:双击。mousedown/mouseup:按下 / 松开鼠标按钮。mousemove:鼠标移动。mouseover/mouseout:鼠标移入 / 移出(支持冒泡)。mouseenter/mouseleave:鼠标移入 / 移出(不支持冒泡)。mouseenter/mouseleave进入子元素时没有任何反应,因为子元素依然在该元素内。mouseover/mouseout进入子元素时会先触发父元素的mouseout,再触发子元素的mouseover,并且因为支持冒泡会继续传到父元素。
键盘事件#
keydown、keypress、keyup,执行顺序也是这个顺序(keypress已经废弃,了解即可)。- 通过
key和code区分按下的键:code是按键代码("KeyA"、"ArrowLeft"),对应键盘上的物理位置;key是字符("A"、"a"),非字符按键通常和 code 的值相同。
表单事件#
change:表单元素的内容改变时触发。input:元素获取用户输入时触发。focus/blur:获得 / 失去焦点时触发。reset/submit:表单重置 / 提交时触发。
文档加载事件#
DOMContentLoaded:浏览器已完全加载 HTML 并构建了 DOM 树,但<img>和样式表之类的外部资源可能还没加载完。load:不仅 HTML 加载完成,所有外部资源也加载完成。
BOM#
BOM 由浏览器提供,用来处理 document 之外的内容,可以看作 JS 脚本与浏览器窗口之间的桥梁,比如 navigator、location、history、screen 这些对象。
window#
- 包含大量属性:
localStorage、console、location、history、scrollX等。 - 包含大量方法:
alert、close、scrollTo、open等。 - 包含大量事件:
focus、blur、load、hashchange等。 - 还从 EventTarget 继承了
addEventListener、removeEventListener、dispatchEvent。
location#
常见属性:
href:当前窗口对应的完整 URL。protocol:协议。host:主机地址(带端口);hostname:主机地址(不带端口)。port:端口。pathname:路径。search:查询字符串。hash:哈希值。username/password:URL 中的用户名和密码,很多浏览器已经禁用。
常见方法:
assign:赋值一个新 URL 并跳转。replace:跳转到新 URL,但不会在浏览记录里留下之前的记录。reload:重新加载页面,可以传一个布尔值。
URLSearchParams 可以把字符串和参数对象互相转换,常见方法有 get(取值)、set(设置)、append(追加)、has(判断是否存在)。
history#
history 对象用来访问浏览器曾经的会话历史记录。
- 属性:
length(会话中的记录条数)、state(当前保留的状态值)。 - 方法:
back()(返回上一页,等价于go(-1))、forward()(前进下一页,等价于go(1))、go()(加载历史中的某一页)、pushState()(改变地址栏并压入一条历史记录,不会真的加载页面)、replaceState()(同上,但替换当前记录)。
JSON#
JSON 顶层支持的三种值#
- 简单值:数字、字符串(不支持单引号)、布尔类型、
null。 - 对象值:由 key、value 组成,key 必须是字符串且要加双引号,值可以是简单值、对象值、数组值。
- 数组值:值可以是简单值、对象值、数组值。
序列化#
JSON.stringify():把 JavaScript 类型转成对应的 JSON 字符串。JSON.parse():解析 JSON 字符串,转回对应的 JavaScript 类型。
写在最后#
这份笔记是 2024 年学 JavaScript 基础时整理的,来源包括 MDN 之类的官方文档和当时看的教程视频。这一篇记的内容最多,也最杂:语法部分看着简单,真正花时间的是 this、作用域和事件流这些「规则类」的东西。
现在回看,JS 基础最值得反复确认的其实不是 API 记不记得住,而是几个判断标准——== 和 === 什么时候结果不同、this 到底指向谁、事件到底是冒泡还是捕获。这些东西后来写框架代码时也一直在用。
下一篇是 JavaScript 进阶,讲原型、Promise、迭代器和异步这些。
