JavaScript

ffy Lv3

JavaScript是一种轻量级、解释型、面向对象的编程语言. 作为前端三件套之一以及TS的基础, JS语言细节十分难嚼. 本文在「料理的加护」下, 尽可能将JS处理得更加可口一些)

创建JS代码块

变量

var or let?

var先于let的产生, 后者是现代版本的JS中新的关键字.

使用var, 可以对一个先前已被声明且初始化的变量重新声明, 这不会带来报错, 代码依旧可以工作. 但是let并不适用.

1
2
3
4
5
6
7
8
9
10
myName = "Chris";

function logName() {
console.log(myName);
}

logName(); //输出"Chris"

var myName = "Aniya";
logName(); //输出"Aniya"

除此之外, 可以使用var前后声明相同的变量, 这并不会报错:

1
2
var myName = "Chris";
var myName = "Bob";

let只能声明一次:

1
2
let myName = "Chris";
myName = "Bob";

因此, 在代码编写中应尽量多使用let而非var, 这可以帮助我们排除无意中重新命名相同变量而导致的错误.

变量命名的规则

与C语言类似, 建议以 字母、数字、下划线 组成的标识符来命名变量.

  • 不可用_开头, 因为可能被JS设计为特殊的含义;
  • 不可用数字开头, 否则引发错误;
  • 大小写敏感;
  • 建议采用 小写驼峰命名法 ,即小写整个命名的第一个字母然后大写剩下单词的首字符;
  • 避免使用保留字, 比如var,let,for等.

变量类型

1
2
3
4
5
6
7
8
9
10
11
let myAge = 20 ;// 数字
let dolphinGoodbye = "So long and thanks for all the fish"; // 字符串
let test = 6 < 3; //boolean

//数组类型
let myNameArray = ["Chris", "Bob", "Jim"];
let myNumberArray = [10, 15, 40];

//对象类型
let dog = { name: "Spot", breed: "Dalmatian" };

对象类型的访问与结构体相似, dog.name;

在上面的几种变量类型中, 我们都采用let关键字声明变量, 这体现了JS是一种 动态类型语言 ,即无需指定变量包含的数据类型.

同时, 这也意味着我们可以像python一样对同一个变量先后赋值不同类型的值:

1
2
3
4
5
6
7
let myNumber = "500";
typeof myNumber;
// 输出 'string'

myNumber = 500;
typeof myNumber;
//输出'number'

函数

  • 解释器在执行代码之前,似乎将函数、变量、类或导入的声明移动到其作用域的顶部的过程.
1
2
3
4
5
6
exampleFunction();

function exampleFunction() {
console.log("函数内");
console.log(x);
}

由于 提升 的存在, 上述的函数调用不会出错.

默认参数

在编写函数时, 可以通过在参数名称后添加=, 再指定默认值, 这样当调用函数时, 如果没有传入该参数, 则使用默认值。

1
2
3
4
5
function greeding(name = "my friend") {
console.log(`Hello, ${name}!`);
}
hello(); //Hello, my friend!
hello("world"); //Hello, world!

事件处理函数的默认接受值是event:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<button>
onclick
</button>

<p>
nothing here
</p>

<script>
btn = document.querySelector("button")
para = document.querySelector("p")

btn.onclick = click;

function click(string){
console.log("clicked!")
para.textContent = string;
}
</script>
  1. 上述的btn在点击之后调用函数click, 该函数需要一个参数string, 由于btn.onclick = click;的绑定方式, 我们无法指定传参的值, 因此点击之后的para的内容显示为: [object PointerEvent];
  2. textContent是属性而非方法, 因此采用赋值实现;
1
2
3
4
btn.onclick = function click(string) {
console.log("clicked!")
para.textContent = "You have clicked the button!";
}

将上述的绑定方式如此改写, 可以在btn外对string进行赋值, 然后点击按钮可以传入指定参数供后续处理.

箭头函数

在了解箭头函数的作用之前, 需要先介绍:

1
2
3
4
5
6
7
8
function myFunction() {
alert("你好");
}

// 匿名函数
(function () {
alert("你好");
});

正如其名, 匿名函数没有函数名, 不能被调用, 但可以作为参数传入其他函数中.

如果我们希望在监听某个事件发生时调用简单的函数来处理, 则可以通过调用上述的匿名函数实现:

1
2
3
4
5
function logKey(event) {
console.log(`You pressed "${event.key}".`);
}

textBox.addEventListener("keydown", logKey);

这部分代码通过监听html元素的keydown事件, 调用函数输出按下的键盘按键. 我们可以通过匿名函数来简化书写:

1
2
3
textBox.addEventListener("keydown", function (event) {
console.log(`You pressed "${event.key}".`);
});

只需传入函数体, 而不需要函数名, 就可以实现监听事件并调用函数的功能.

箭头函数则是在此情况下更简洁的函数定义方式:

1
2
3
4
5
6
7
8
textBox.addEventListener("keydown", (event) => {
console.log(`You pressed "${event.key}".`);
});

//如果函数只接受一个参数, 也可以省略参数周围的括号
textBox.addEventListener("keydown", event => {
console.log(`You pressed "${event.key}".`);
});

如果只包含一行的return,则可以忽略{}return关键字:

1
2
3
4
5
const originals = [1, 2, 3];

const doubled = originals.map(item => item * 2);

console.log(doubled); // [2, 4, 6]

item => item * 2等价于:

1
2
3
function doubleItem(item) {
return item * 2;
}

一个实例

1
2
<input id="textBox" type="text" />
<div id="output"></div>
1
2
3
4
5
6
const textBox = document.querySelector("#textBox");
const output = document.querySelector("#output");

textBox.addEventListener("keydown", (event) => {
output.textContent = `You pressed "${event.key}".`;
});

通过监听输入框的keydown事件, 输出按下的键盘按键.

函数作用域和冲突

指当前的执行上下文, 在其中的值和表达式可以被访问.

  • 全局作用域: 脚本模式运行所有代码的默认作用域;
  • 模块作用域: 模块模式中运行代码的作用域;
  • 函数作用域: 由函数创建的作用域

和C语言相似, 在函数外部let定义的变量, 以及const定义的常量可以在函数内部访问.

如果HTML调用了多个外部JS文件, 其中具有相同的函数名, 那么只能访问的第一个函数, 第二个函数将被忽略:

1
2
3
4
5
6
<!-- Excerpt from my HTML -->
<script src="first.js"></script>
<script src="second.js"></script>
<script>
greeting();
</script>

如果两个JS文件都定义了greeting函数, 则只有第一个文件中的函数才会被调用.

数据类型

数字和操作符

大部分与C语言相同, 概括需要注意的差异:

  • JS当中只有一种数字类型 – number, 对于整型或者浮点数的初始化得到的量, 由typeof均得到number;
  • 算术运算符: 求幂为**;
  • 常量无法使用自增或自减,好像也是C语言的 忘了
  • ===表示严格等于, !==表示不等于;

    同时存在==!=来判断是否相等, 但是它们只是测试 是否相等, 会忽略数据类型的差异; 而上述的比较会同时比较数据类型. 因此推荐使用===!==来避免类型不一致的错误.

字符串

创建字符串

1
2
3
4
let myString = "A string";
const constString = myString;
console.log(constString);
//A string

可以使用单引号,双引号和反引号来包裹字符串, 但是必须确保字符串的开头和结尾使用相同的字符:

1
2
3
const single = '单引号';
const double = "双引号";
const backtick = `反引号`;

反引号包裹的字符串称为, 大多数情况下,它与其他两种字符串类似, 但是具有特殊的属性:

  • 可以嵌入 JavaScript;
  • 可以声明多行的模板字面量.

字符串的拼接

字符串的拼接有两种方法, 我们先介绍上述提到的模板字符串中的:

1
2
3
const name = "克里斯";
const greeting = `你好,${name}`;
console.log(greeting); // "你好,克里斯"

在模板字面量中用${}包装JS的变量或者表达式.

1
2
3
4
const one = "你好,";
const two = "请问最近如何?";
const joined = `${one}${two}`;
console.log(joined); // "你好,请问最近如何?"

连接2个变量.

1
2
3
4
5
6
7
const song = "青花瓷";
const score = 9;
const highestScore = 10;
const output = `我喜欢歌曲《${song}》。我给它打了 ${
(score / highestScore) * 100
} 分。`;
console.log(output); // "我喜欢歌曲《青花瓷》。我给它打了 90 分。"

在模板字面量的${}内部包含表达式.


除此之外,对于普通的字符串(使用单引号或者双引号得到的字符串), 我们可以使用`+`直接连接:
1
2
3
const greeting = "你好";
const name = "克里斯";
console.log(greeting + "," + name); // "你好,克里斯"

多行字符串

模板字符串会保留源代码中的换行符,因此可以编写跨越多行的字符串:

1
2
3
4
5
6
7
8
const newline = `终于有一天,
你知道了必须做的事情,而且开始……`;
console.log(newline);

/*
终于有一天,
你知道了必须做的事情,而且开始……
*/

如果希望用普通的字符串得到等效的输出, 必须在字符串中包含\n,而非直接跨行:

1
2
3
4
5
6
7
const newline = "终于有一天,\n你知道了必须做的事情,而且开始……";
console.log(newline);

/*
终于有一天,
你知道了必须做的事情,而且开始……
*/

显示引号

  1. 通过在符号前加上反斜杠\, 可以转义字符串中的特殊字符,包括字符串中的引号:
1
const bigmouth = 'I\'ve got no right to take my place…';
  1. 换用其他字符: 在字面量内用不同于包裹字符串的引号:
1
2
const goodQuotes1 = 'She said "I think so!"';
const goodQuotes2 = `She said "I'm not going in there!"`;

常用方法

对于字符串对象实例,其常用的方法:

  • .length: 获取字符串的长度;
  • []: 返回字符串中对应索引的字符, 索引同样从0开始;
  • .indexOf(""): 查找子字符串
    • input: 希望查找的子字符串;
    • output: 子字符串开始的下标(如果不存在则返回-1);
  • .slice(indedxStart, indexEnd): 截取字符串
    • input: 起始下标, 结束下标(不包含该下标). 如果不存在结束下标则提取之后剩余的全部字符;
    • output: 截取的子字符串;

更多的slice知识:

  1. 如果索引是个负数, 取index+str.length进行标准化;
  2. 如果indexStart大于str.length, 返回空字符串;
  3. 如果标准化负值之后, indexStart大于indexEnd, 也返回空字符串;
  • .toLowerCase() & .toUpperCase(): 转换字符串中的所有字符为小写或大写;
  • .replace(original, new): 替换字符串中original子字符串为new;

    此时不会直接改变原字符串的值, 而是返回一个修改之后的字符串. 因此, 如果想要将原来的值替换, 需要用上述方法得到的值去赋值原来的字符串.

Cases

利用 indexOfslice 方法, 获取新字符串:

  • input: "str3"三位长字符串+"..."(无关字符串)+";"+strLast(剩余字符串);
  • output: "str3"+";"+strLast
1
2
3
4
5
6
7
8
9
10
11
12
var stations = ['MAN675847583748sjt567654;Manchester Piccadilly',
'GNF576746573fhdg4737dh4;Greenfield',
'LIV5hg65hd737456236dch46dg4;Liverpool Lime Street',
'SYB4f65hf75f736463;Stalybridge',
'HUD5767ghtyfyr4536dh45dg45dg3;Huddersfield'];

for(var i = 0; i < stations.length; i++){
var input = stations[i];
var str3 = input.slice(0,3);
var strLast = input.slice(input.indexOf(";")+1); //indexOf获取;位置
var output = str3 + ";" + strLast;
}

通过 indexOf 根据子字符串筛选字符串数组:

  • input: 可能包含 Christmas 的字符串数组;
  • output: 包含 Christmas 的字符串数组;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var list = document.querySelector('.output ul');
list.innerHTML = '';
var greetings = ['Happy Birthday!',
'Merry Christmas my love',
'A happy Christmas to all the family',
'You\'re all I want for Christmas',
'Get well soon'];

for(var i = 0; i < greetings.length; i++) {
var input = greetings[i];
if(greetings[i].indexOf('Christmas') !== -1) {
var result = input;
var listItem = document.createElement('li');
listItem.textContent = result;
list.appendChild(listItem);
}
}

数字与字符串

相互转换

非常神奇, 在JS当中, 数字和字符串可以直接通过函数Number()String()进行转换, 与C语言不同.

1
2
3
4
5
6
const myString = "123";
const myNum = Number(myString);
console.log(typeof myNum);
// number
console.log(myNum);
// 123
1
2
3
4
5
6
const myNum2 = 123;
const myString2 = String(myNum2);
console.log(typeof myString2);
// string
console.log(myString2);
// "123"

对于浮点数同样成立.

前后拼接

使用+将字符串类型和数字类型的变量or常量直接拼接, 得到的是以空格相隔的字符串:

1
2
3
4
5
6
7
const name = "Front ";
const number = 242;
const combine = name + number;

console.log(combine); //Front 242

console.log(typeof(combine)); //string

数组

  1. 存储任意类型元素–字符串,数字,对象,变量,另一个数组;
  2. 可以 混合 元素类型:
1
let random = ["tree", 795, [0, 1, 2]];
  1. 像访问字符串一样, 利用索引访问数组元素;
  2. 包含数组的数组结构称为~

split()

  • 作用: 将一个字符串根据给定的字符分隔为字符串数组;
1
2
3
4
let myData = "Manchester,London,Liverpool,Birmingham,Leeds,Carlisle";
let myArray = myData.split(",");
console.log(myArray);
// ["Manchester", "London", "Liverpool", "Birmingham", "Leeds", "Carlisle"]

join()

split的反向操作, 给出分隔符号, 将数组的字符串拼接成一个字符串:

1
2
let myNewString = myArray.join(",");
myNewString;

toString()

join方法相似, 但是无法自定义分隔符, 默认为,:

1
2
let dogNames = ["Rocket", "Flash", "Bella", "Slugger"];
dogNames.toString(); //Rocket,Flash,Bella,Slugger

push & pop

push()方法可以将1或多个元素添加到数组的 末尾:

  1. 将会直接改写原来的数组,不需要重新赋值;
  2. 该方法具有返回值, 且返回的是更新之后的数组长度(包含元素的个数);
1
2
3
4
let myArray = [1, 2, 3];
let newLength = myArray.push(4, 5,"string");
console.log(myArray); // [1, 2, 3, 4, 5, "string"]
console.log(newLength); // 6

使用.pop()从数组中删除最后一个元素:

1
2
myArray.pop(); //"string"
console.log(myArray); // [1, 2, 3, 4, 5]
  1. 方法调用返回值就是删除的元素本身;
  2. 直接对原始数组操作并赋值, 不需要另外的赋值操作;

shift & unshift:
在功能上分别与pushpop相同, 但是作用于数组的开始位置.

条件语句

JS的条件语句与C语言十分相似, 在此仅给出官方文档的一些例子:

天气预报

1
2
3
4
5
6
7
8
9
10
<label for="weather">选择今天的天气:</label
><select id="weather">
<option value="">--作出选择--</option>
<option value="sunny">晴天</option>
<option value="rainy">雨天</option>
<option value="snowing">雪天</option>
<option value="overcast">阴天</option>
</select>

<p></p>

lable当中的for标签与select标签的id属性对应, 用于关联两个标签.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const select = document.querySelector("select");
const para = document.querySelector("p");

select.addEventListener("change", setWeather);

function setWeather() {
const choice = select.value;

switch (choice) {
case "sunny":
para.textContent = "阳光明媚。穿上短裤吧!去海滩,或公园,吃个冰淇淋。";
break;
case "rainy":
para.textContent = "外面下着雨;带上雨衣和雨伞,不要在外面呆太久。";
break;
case "snowing":
para.textContent =
"大雪纷飞,天寒地冻!最好呆在家里喝杯热巧克力,或者去堆个雪人。";
break;
case "overcast":
para.textContent =
"虽然没有下雨,但天空灰蒙蒙的,随时都可能变天,所以要带一件雨衣以防万一。";
break;
default:
para.textContent = "";
}
}
  1. 通过querySelector方法获取selectp标签;
  2. 然后为select标签添加事件监听器, 当内容改变时触发 change 事件, 同时调用setWeather函数;
  3. 进而通过 switch 语句处理不同天气的情况, 并设置相应的文字内容;
    在线网页示例:simple-switch

事件介绍

什么是?

  • 用户选择、点击或者光标悬停在某一元素;
  • 用户在键盘中按下某个按键;
  • 网页结束加载;

为了响应事件, 我们需要编写一份JS代码块用于在事件发生之后运行. 这样的代码块称之为~.

处理点击事件

以点击事件为例, 介绍html与js如何进行事件处理的交互:

1
<button> 改变颜色 </button>
1
2
3
4
5
6
7
8
9
10
11
const btn = document.querySelector("button");

function random(number){
return Math.floor(Math.random()*(number+1));

}

btn.addEventListener("click", ()=>{
const rndCol = `rgb(${random(255)},${random(255)},${random(255)})`;
document.body.style.backgroundColor = rndCol;
})
  1. Math.random()方法生成一个介于[0,1)之间的随机数;
  2. *(number+1)之后利用向下取整的方法Math.floor()将其转换为整数, 范围为[0,number];

    假如输入的number为4, 则random(4)的结果可能为0, 1, 2, 3, 4中的一个;
    假设输入的number为3.6, 则输出的结果还是0~4中的整数.

  3. rndCol = `rgb(${random(255)},${random(255)},${random(255)}) 采用的是在内部使用${}调用函数变量的方法.

addEventListener()

adEventListener方法已经在之前的例子中出现过, 现在具体介绍其作用和语法.

通过EventTarget.adddEventListener()的方法, 将指定的监听器注册到对象上, 具体的语法如下:

1
2
3
addEventListener(type, listener);
addEventListener(type, listener, options);
addEventListener(type, listener, useCapture);
  • type: 事件类型, 如click, mouseover, mouseout, keydown, keyup等;
  • listener: 事件处理函数, 该函数将在事件发生时被调用;
    • 包括 回调函数 以及 实现了 EventListener 接口的对象;
  • options: 可选参数, 用于配置事件监听器的行为;

    可以为单个事件添加多个事件监听器.

listener

简单来说, ~指的是当某个事件发生时被调用的一段代码.

  • 是一个函数, 但是只有等到特定的事件发生时才会执行.

实现了 EventListener 接口的对象:

  • 特点: 以对象作为listener, 对象中具有名为handleEvent()的方法;
  • 作用:
    • 将事件处理封装到一个对象当中, 可以更好地组织代码;
    • 便于在对象中保存更多的状态信息;
1
2
3
4
5
6
7
8
9
10
const listenerObject = {
count: 0,
handleEvent(event) {
this.count++;
console.log(`事件类型是:${event.type},已触发 ${this.count} 次`);
}
};

const button = document.querySelector('button');
button.addEventListener('click', listenerObject);

options

一个指定有关 listener 属性的可选参数对象.

Capture
  • 含义:

    • 一个布尔值,表示 listener 会在该类型的事件捕获阶段传播到该 EventTarget 时触发;
    • 默认为false, 表示只有在冒泡阶段才触发.
  • 区别:

    • captureuseCapture实际上指的都是 监听器是否在捕获阶段触发 的布尔值.

      捕获阶段: 从最外层的元素开始, 逐层向内捕获事件, 直到事件到达目标元素.

    • 后来DOM的规范更新时引入了options参数, 此后capture取代了useCapture的作用.
    • 如果addEventListener的第三个参数不指定对象, 只有布尔值, 那么默认是在设置useCapture

可以先查看事件传播的阶段来辅助理解不同的阶段.

Once
  • 含义:
    • 一个布尔值,表示 listener 在添加之后最多只调用一次;
    • 默认为false, 表示可以多次调用.
  • e.g.
1
2
3
child.addEventListener('click', () => {
console.log('子元素 - 目标阶段');
},{once: true});

once属性被设置为true, 当调用一次之后事件监听器会被自动清除. 因此只有第一次的点击才会console.

Passive
  • 含义:

    • 一个布尔值,设置为 true 时,表示 listener 永远不会调用 preventDefault();
  • 作用:

    • 明确不会在listener中不会调用preventDefault()方法, 即不会阻止浏览器的默认行为;
    • 此时, 浏览器可以直接渲染默认行为的结果, 无需等待listener的执行与默认行为的检查, 从而提高了性能.
  • Notice:

    • 如果设置passivetrue, 则listener当中不可出现preventDefault()方法, 否则会报错.

e.g.

1
2
3
4
document.addEventListener('wheel',()=>{
event.preventDefault();
console.log("scrolling");
},{passive: false});
  • wheel事件的默认行为是滚动页面;
  • event.preventDefault();表示会阻止鼠标滚动带来的页面滚动;
1
2
3
document.addEventListener('wheel',()=>{
console.log("scrolling");
},{passive: true});

明确不会阻止默认行为, 浏览器可以直接渲染页面的滚动效果, 因此提高了显示的效果.

1
2
3
4
document.addEventListener('wheel',()=>{
event.preventDefault();
console.log("scrolling");
},{passive: true});

passive的设置与listener内部矛盾, 将会报错.

Signal

用于有条件地移除事件监听器, 具体使用参见可被移除的监听器.

事件传播的阶段

  1. 捕获阶段: 事件从根节点开始向目标节点传播;

    e.g. 点击事件从document开始传播, 经过html,body直到目标元素.

  2. 目标阶段阶段: 事件到达目标元素;
  3. 冒泡阶段: 事件从目标元素开始沿着DOM树向上传播.

Case

1
2
3
4
<div id="parent">
parent
<div id="child">child</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const parent = document.querySelector('#parent');
const child = document.querySelector('#child');

parent.addEventListener('click', () => {
console.log('父元素 - 冒泡阶段');
});

parent.addEventListener('click', () => {
console.log('父元素 - 捕获阶段');
}, { capture: true });

child.addEventListener('click', () => {
console.log('子元素 - 目标阶段');
});

上述的child被包裹在parent内部.

  • 当点击parent时将会显示:
1
2
"父元素 - 捕获阶段"
"父元素 - 冒泡阶段"

由于设置了在捕获阶段就触发, 所以先触发了捕获阶段的监听器, 然后再触发冒泡阶段的监听器;

  • 当点击child时将会显示:
1
2
3
"父元素 - 捕获阶段"
"子元素 - 目标阶段"
"父元素 - 冒泡阶段"

child是整个事件流的目标元素, 所以触发时机介于二者之间.

Notice

  1. 如果将div换成button, 则点击child时可能只会显示 目标 阶段的输出.

    这是因为, 不同浏览器对于button元素的默认行为不同, 可能默认阻止了捕获阶段和冒泡阶段

  2. event.stopPropagation();加入该咒语代码可以在此停止事件的传播, 比如可以在上述的捕获阶段监听器加入该代码:

1
2
3
4
parent.addEventListener('click', () => {
console.log('父元素 - 捕获阶段');
event.stopPropagation();
}, { capture: true });

此时, 点击parent时, 只会触发捕获阶段的监听器, 不会触发冒泡阶段的监听器.

可被移除的监听器

1
2
3
4
5
6
7
8
<table id="outside">
<tr>
<td id="t1">one</td>
</tr>
<tr>
<td id="t2">two</td>
</tr>
</table>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 为 table 添加可被移除的事件监听器
const controller = new AbortController();
const el = document.getElementById("outside");
el.addEventListener("click", modifyText, { signal: controller.signal });

// 改变 t2 内容的函数
function modifyText() {
const t2 = document.getElementById("t2");
if (t2.firstChild.nodeValue === "three") {
t2.firstChild.nodeValue = "two";
} else {
t2.firstChild.nodeValue = "three";
controller.abort(); // 当值变为 "three" 后,移除监听器
}
}
  • AbortController是一个构造函数, 用于创建一个可被移除的事件监听器的控制器;
  • signal属性是一个AbortSignal对象, 用于控制监听器的移除;
  • controller.abort()方法用于移除监听器;
  • t2的内容变为”three”时, 移除监听器, 使得modifyText函数不再执行. 此后, 点击t2不会触发modifyText函数.

具体的构造步骤:

  1. 创建一个AbortController实例: const controller = new AbortController();
  2. 在事件监听器内的参数中添加signal: controller.signal选项;
  3. 在需要移除监听器的地方调用controller.abort()方法;

我们也可以直接使用removeEventListener()方法来移除事件监听器:

1
2
3
removeEventListener(type, listener);
removeEventListener(type, listener, options);
removeEventListener(type, listener, useCapture);
  • Notices:
    • 如果同一个对象上存在2个事件监听器, 且仅在useCapture参数存在差异, 那么需要先后2次调用removeEventListener()方法才能完全移除其事件监听器;
    • 如果无法匹配当前注册的事件监听器, 那么该函数将不会起任何作用;
    • type,listener参数必须完全匹配才能移除事件监听器;
    • 对于options参数:
      • 字段相同: 一定可以移除;
      • 字段不同: 需要与默认值false匹配才可以移除.
1
2
3
4
5
6
7
8
element.addEventListener("mousedown", handleMouseDown, { passive: true });

element.removeEventListener("mousedown", handleMouseDown, { passive: true }); // 成功
element.removeEventListener("mousedown", handleMouseDown, { capture: false }); // 成功
element.removeEventListener("mousedown", handleMouseDown, { capture: true }); // 失败
element.removeEventListener("mousedown", handleMouseDown, { passive: false }); // 成功
element.removeEventListener("mousedown", handleMouseDown, false); // 成功
element.removeEventListener("mousedown", handleMouseDown, true); // 失败

添加与移除的结合使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const body = document.querySelector("body");
const clickTarget = document.getElementById("click-target");
const mouseOverTarget = document.getElementById("mouse-over-target");

let toggle = false;
function makeBackgroundYellow() {
body.style.backgroundColor = toggle ? "white" : "yellow";

toggle = !toggle;
}

clickTarget.addEventListener("click", makeBackgroundYellow, false);

mouseOverTarget.addEventListener("mouseover", () => {
clickTarget.removeEventListener("click", makeBackgroundYellow, false);
});

使用匿名函数

在上述html例子下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 改变 t2 内容的函数
function modifyText(new_text) {
const t2 = document.getElementById("t2");
t2.firstChild.nodeValue = new_text;
}

// 用匿名函数为 table 添加事件监听器
const el = document.getElementById("outside");
el.addEventListener(
"click",
function () {
modifyText("four");
},
false,
);

通过匿名函数封装代码, 将参数传入函数modifyText, 使得函数可以被调用.

使用箭头函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 改变 t2 内容的函数
function modifyText(new_text) {
var t2 = document.getElementById("t2");
t2.firstChild.nodeValue = new_text;
}

// 用箭头函数为 table 添加事件监听器
const el = document.getElementById("outside");
el.addEventListener(
"click",
() => {
modifyText("four");
},
false,
);

通过=>{}形式的箭头函数简化代码书写.


比较匿名与箭头

匿名函数与箭头函数在此处的应用基本相同, 但是在this的指向上有所不同:

  • 匿名函数与其他普通的JS函数:this指向调用它的对象之作用域(如果没有直接调用关系, 默认为全局对象, 且严格模式下为undefined);
1
2
3
4
function sayHello() {
console.log(this); // 在非严格模式下,this 指向 window
}
sayHello();
  • 箭头函数的this继承自外部作用域, 即调用该方法的对象.
1
2
3
4
5
6
7
const obj = {
name: "ZJU",
greet: function () {
console.log(this.name); // this 指向 obj
},
};
obj.greet(); // 输出:ZJU
  • e.g.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const obj = {
name: "ZJU",
getNameWithAnonymous: function () {
return function () {
console.log(this.name);
};
},
getNameWithArrow: function () {
return () => {
console.log(this.name);
};
},
};

const anonymousFn = obj.getNameWithAnonymous();
anonymousFn(); // 输出:undefined

const arrowFn = obj.getNameWithArrow();
arrowFn(); // 输出:ZJU

进一步完善.

事件对象

在事件处理函数的内部, 以固定指定名称出现的参数, 例如event,e,evt. 它被自动传递给事件处理函数,以提供额外的功能和信息。

e.target始终是对 事件刚刚发生的元素 的引用

表达式和运算符

new()

用来创建对象实例的一个关键字.

  • 作用: 调用 一个 构造函数, 并返回一个由该构造函数创建的对象实例.

语法

1
2
3
4
5
new constructor
new constructor()
new constructor(arg1)
new constructor(arg1, arg2)
new constructor(arg1, arg2, /* …, */ argN)
  1. 如果没有指定参数, 默认为在不带参数的情况下调用构造函数. 即new foo 等价于 new foo();
  2. 构造函数内部的this将被绑定到新建的对象实例上;
  • e.g.
1
2
3
4
5
6
7
8
9
function Car(color, brand) {
this.color = color; // 将 color 赋值给新对象
this.brand = brand; // 将 brand 赋值给新对象
}

const myCar = new Car("red", "Toyota");

console.log(myCar.color); // 输出 "red"
console.log(myCar.brand); // 输出 "Toyota"

使用new()步骤:

  1. 定义构造函数;
  2. 使用new()并传入构造函数的参数;
  3. 将返回的对象实例赋值给一个变量;

新增属性

  • 为已经定义的对象实例直接新增属性, 但是不会影响其他相同类型的对象和构造函数本身:
1
car1.color = "black" //为car1新增color属性

  • 添加共享属性到构造函数中的prototype:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Car() {}
car1 = new Car();
car2 = new Car();

console.log(car1.color); // undefined

Car.prototype.color = "原色";
console.log(car1.color); // '原色'

car1.color = "黑色";
console.log(car1.color); // '黑色'

console.log(Object.getPrototypeOf(car1).color); // '原色'
console.log(Object.getPrototypeOf(car2).color); // '原色'
console.log(car1.color); // '黑色'
console.log(car2.color); // '原色'
  • 此处的构造函数名为Car, 因此通过Car.prototype可以访问到构造函数的原型对象;
  • getPrototypeOf 表示获取对象的原型对象, 因此此处均为最初定义的 原色.

new.target

函数通过new.target属性可以判断是否通过new关键字调用, 即构造.

  • 如果函数是正常调用, 则返回undefined;

  • 如果函数是通过new调用, 返回被调用的构造函数.

  • e.g.

1
2
3
4
5
6
7
8
9
10
11
function Car(color) {
if (!new.target) {
// 以函数的形式被调用。
return `${color}车`;
}
// 通过 new 被调用。
this.color = color;
}

const a = Car("红"); // a 是“红车”
const b = new Car("红"); // b 是 `Car { color: "红" }`

对象类型与实例

通过构造函数可以创建一个对象类型:

1
2
3
4
5
function Car(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}

通过使用new()方法, 由对象类型构造一个对象实例:

1
const myCar = new Car("鹰牌", "Talon TSi", 1993);

类与new

在JS当中, 类 必须 通过new调用.

可以优先阅读类相关的知识

  • e.g.
1
2
3
4
5
6
7
8
9
10
class Animal {
//构造函数
constructor(name) {
this.name = name;
}
//实例方法
greet() {
console.log(`你好,我的名字是${this.name}`);
}
}

对于上述的类, 必须使用如下的调用方式:

1
const animal = new Animal("Dog"); // 正常

而下面这样类似于普通函数的调用方式会抛出错误:

1
Animal("Cat"); // TypeError:  Class constructor Animal cannot be invoked without 'new'

在使用正确方法得到类的实例对象之后, 可以用访问属性的方式来调用实例方法:

1
animal.greet(); // 输出 "你好,我的名字是Dog"

下面给出与普通函数的区别:

1
2
3
4
5
6
7
function Car(model) {
this.model = model;
}

const car = new Car("Toyota"); // 正常
Car("Honda"); // 不抛出错误,但 this 会指向全局对象.
const anotherCar = Car("cat"); //此时全局对象下的model值为 "cat", 覆盖了上一行的定义.

总结:

  • 以构造函数形式呈现的普通函数, 可以被直接调用, 但是此时内部的参数赋值给了全局对象;
  • 如果以new方法构造得到对象实例, 依旧正常.

补充

默认行为

是指浏览器在某些事件发生时,自动执行的内置操作, 是浏览器的“默认反应”.

  • 比如存在以下的默认行为:
    • 滚动事件:触摸屏上滑动手指,页面会滚动;
    • 拖拽文件到浏览器:浏览器会尝试加载文件;
    • 点击链接 (<a href="...">):跳转到指定的 URL;

使用 event.preventDefault() 方法可以阻止事件的默认行为.

  • e.g: 阻止链接跳转
1
2
3
4
document.querySelector('a').addEventListener('click', function(event) {
event.preventDefault(); // 阻止点击链接时的默认行为
console.log('链接被点击,但没有跳转');
});
  • 作用:
    • 通过阻止默认行为, 可以实现自定义逻辑.

this

  • this可以视作函数的一个隐参数, 是在函数被执行时创建的绑定;
  • this 指向的是当前函数的调用者,而不是函数内部定义的变量.

  • e.g.
1
2
3
4
5
6
7
8
9
10
11
12
const obj = {
a: "a in the obj",
b: "b in the obj",
f: function() {
const b = "b in the function"; // 函数作用域
console.log(this.b); // 访问 this.b
}
};

const b = "b outside of the func";

obj.f();

此处的f

函数上下文中的this

  • this参数的值取决于函数如何被调用, 而不是函数如何被定义.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 对象可以作为第一个参数传递给 'call' 或 'apply',
// 并且 'this' 将被绑定到它。
const obj = { a: "Custom" };

// 使用 var 声明的变量成为 'globalThis' 的属性。
var a = "Global";

function whatsThis() {
return this.a; // 'this' 取决于函数如何被调用
}

whatsThis(); // 'Global'; 在非严格模式下,'this' 参数默认为 'globalThis'
obj.whatsThis = whatsThis;
obj.whatsThis(); // 'Custom'; 'this' 参数被绑定到 obj
  1. 同样是调用函数whatsThis(), 但是this参数被绑定到不同的对象上, 导致返回值不同;
  2. 在非严格模式下, this参数默认指向globalThis, 即全局对象;
  3. 对于典型函数, this指向函数访问的对象;
  • e.g.
1
2
3
4
5
6
7
8
9
10
11
const obj = {
b: "b in the obj",
f: function() {
const b = "b in the function"; // 函数作用域
console.log(this.b); // 访问 this.b
}
};

const b = "b outside of the func";

obj.f();

此处f作为obj对象的方法被调用, 因此普通函数的this指向obj.

  • e.g. 直接调用的普通函数this指向全局:
1
2
3
4
5
6
7
8
9
10
const obj = {
a: "a in the obj",
f: function() {
const funcA = function () { return this.a }; // 普通函数,this 由调用方式决定
console.log(funcA()); // 访问 this.a
}
};

var a = "a in the global";
obj.f(); // "a in the global"
  • 此处的funcA并没有类似于作为对象的属性调用(obj.funcA()), 因此其this指向全局作用域(window), 输出undefined, 而是直接调用的形式, 因此其this指向全局作用域.

对this传值

使用call()以及apply()方法可以将this绑定到其他对象上.

call()

  • 形式: func.call(thisArg, arg1, arg2, ...)
  • e.g:
1
2
3
4
5
6
7
8
9
function add(c, d) {
return this.a + this.b + c + d;
}

const o = { a: 1, b: 3 };

// 第一个参数被绑定到隐式的 'this' 参数;
// 剩余的参数被绑定到命名参数。
add.call(o, 5, 7); // 16

apply()

  • 形式: func.apply(thisArg, [argsArray])
  • e.g:
1
2
3
4
5
6
7
8
9
function add(c, d) {
return this.a + this.b + c + d;
}

const o = { a: 1, b: 3 };

// 第一个参数被绑定到隐式的 'this' 参数;
// 第二个参数是一个数组,其成员被绑定到命名参数。
add.apply(o, [10, 20]); // 34

bind()

  • 形式: f.bind(someObject);
  • 作用:
    • 创建一个新的函数(需要重新赋值), 具有与f相同的函数体和作用域;
    • 新函数的this永久地 绑定到someObject, 不随调用方式的变化而变化.
  • 限制:
    • bind无法多次生效. 即对函数fbind得到的g, 无法继续用bind得到期望的h;
  • e.g. 多次bind:
1
2
3
4
5
6
7
8
9
10
11
12
function f() {
return this.a;
}

const g = f.bind({ b: "azerty" });
console.log(g()); // undefined

const h = g.bind({ a: "yoo" }); // bind 只能生效一次!
console.log(h()); // undefined

const o = { a: 37, f, g, h };
console.log(o.a, o.f(), o.g(), o.h()); // 37 37 undefined undefined
  • 由于bind只能对一个原始函数作用, 因此由f得到的g无法继续由bind绑定this得到期望的h, 此处h的this依旧是{b: "azerty"}, 因此在输出对象a时显示undefined;
  • o.f()的调用是普通函数的调用, 因此其this继承自对象o, 输出37;
  • e.g. 对象
1
2
3
4
5
6
7
8
9
10
11
12
function f() {
return this.a + " " + this.c;
}

const g = f.bind({ b: "azerty" , c:"ccc"});
console.log(g()); // "undefined ccc"

const h = g.bind({ a: "yoo" }); // bind 只能生效一次!
console.log(h()); // "undefined ccc"

const o = { a: 37, f, g, h };
console.log(o.a, o.f(), o.g(), o.h()); // 37 37 azerty azerty
  • bind绑定的this是永久覆盖, 而非简单叠加;
  • 由于bind绑定的this不随者调用方式的变化而变化, 因此即使处于对象o当中, g,h依旧不会输出o中的a.

箭头函数中的this

使用 call()、apply() 或 bind() 调用箭头函数时,传入的 this 值会被忽略,但其他参数仍然会正常传递。

普通函数:

1
2
3
4
5
6
7
8
9
const a = "a in the global";
const foo = function () {return this.a};

const obj = {
a: "a in the obj",
f: foo
};

console.log(obj.f()); // "a in the obj"

call()apply()bind() 无法改变箭头函数的this(但是call与apply的其他参数可以正常传递:

1
2
3
4
5
6
7
8
9
const foo = ()=> this.a;

const obj = {
a: "a in the obj",
f: foo.bind({a:"a in the bind"}) // 显式绑定 this 到 obj, 但是无法生效
};

console.log(obj.f()); // undefined

换成普通函数则输出a in the obj.

  • 全局作用域
1
2
3
4
5
6
7
8
9
var a = "a in the global";
const foo1 = () => this.a;

const obj = {
a: "a in the obj",
f: ()=> a
};

console.log(obj.f());

作用域

指当前的执行上下文, 在其中的值和表达式可以被访问.

  • 全局作用域: 脚本模式运行所有代码的默认作用域;
  • 模块作用域: 模块模式中运行代码的作用域;
  • 函数作用域: 由函数创建的作用域
  • 块级作用域: 由letconst声明的变量的作用域.(对于var无效);
1
2
3
4
5
6
7
8
9
{
var x = 1;
}
console.log(x); // 1

{
const x = 1;
}
console.log(x); // undefined

Notices:

  • 对象本身并不会创建作用域, 只是一个键值对的集合;
  • 箭头函数也不会创建自己的作用域, 而是 继承 外层作用域中的this;

变量与作用域

  • var在全局作用域中声明时会成为 全局对象 (windowglobal)的属性;
  • letconst即使在全局作用域中声明, 也不会成为全局对象的属性;
1
2
3
4
5
var a = "1";
let b = "2";

window.a; // "1"
window.b; // undefined

因此, 建议在全局作用域中不要使用var声明变量, 而使用letconst声明变量. 从而避免导致意外的覆盖和冲突.

函数与作用域

普通函数

普通函数和匿名函数的作用域继承自其定义时的作用域.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const obj = {
a: "a in the obj",
insideObj: {
g: function() {
return this.a; // 普通函数,this 动态绑定到 insideObj
}
},
f: function() {
return this.a; // 普通函数,this 动态绑定到 obj
}
};

console.log(obj.f()); // "a in the obj"
console.log(obj.insideObj.g()); // undefined,因为 insideObj 中没有 a

箭头函数

e.g. 箭头函数继承外层作用域:

1
2
3
4
5
6
7
8
9
10
11
12
var a = "a in the global";

const obj = {
a: "a in the obj",
insideObj: {
g: () => this.a
},
f: () => this.a
};

console.log(obj.f()); // "a in the global"
console.log(obj.insideObj.g()); //"a in the global"

由于对象不会创建作用域, 因此此处的箭头函数的this继承了外层作用域(window)的this, 且var创建的变量存在于全局作用域中.

语法糖

一种让代码更简洁、更易读的语法形式.

  • 本质上没有增加语言的功能, 而是对已有功能的 包装 或者优化;
  • 可读性提升: 让代码更填 使得代码更加容易理解和书写;
  • 底层实现: 实质上依旧用基础的语法实现.

class 是 ES6 引入的语法糖, 它提供了面向对象编程的简洁语法. 本质上是对原型继承prototype的封装.

使用class的写法:

1
2
3
4
5
6
7
8
9
10
11
12
class Person {
constructor(name) {
this.name = name;
}

greet() {
console.log(`Hello, my name is ${this.name}`);
}
}

const person = new Person("Alice");
person.greet(); // 输出:Hello, my name is Alice

等价的原型写法:

1
2
3
4
5
6
7
8
9
10
function Person(name) {
this.name = name;
}

Person.prototype.greet = function() {
console.log(`Hello, my name is ${this.name}`);
};

const person = new Person("Alice");
person.greet(); // 输出:Hello, my name is Alice

箭头函数

箭头函数简化了函数定义的书写, 其本质上依旧是一个普通函数, 因此也是语法糖的一种.

  • e.g.
1
2
3
4
5
6
7
// 使用箭头函数
const add = (a, b) => a + b;

// 等价的普通函数
const add = function add(a, b) {
return a + b;
}

结构赋值

手动提取对象属性的语法糖.

  • 使用结构赋值:
1
2
3
const person = {name:"Zhuo", gender:"male"};

const {name, gender} = person;
  • 等价的原型写法:
1
2
3
4
const person = {name:"Zhuo", gender:"male"};

const name = person.name;
const gender = person.gender;

赋值规则

结构赋值时, 基于 属性名匹配 而非顺序.
因此, 对象结构的{}内部属性必须和 对象的属性名 相对应.

错误的示例:

1
2
3
4
5
const person = { name: "Alice", age: 25 };
const { a, b } = person;

console.log(a); // 输出:undefined
console.log(b); // 输出:undefined

重命名属性的写法:

1
2
3
4
5
const person = { name: "Alice", age: 25 };
const { name: a, age: b } = person;

console.log(a); // 输出:Alice
console.log(b); // 输出:25

手动赋值: 对于结构对象中不存在的属性, 可以采取普通赋值的方式与结构赋值相结合:

1
2
3
4
5
const person = { name: "Alice" };
const { name, age = 30 } = person;

console.log(name); // 输出:Alice
console.log(age); // 输出:30 (因为 person 中没有 age 属性,所以使用了默认值)

数组的结构赋值

上述讨论的结构赋值都是对 对象 的结构赋值, 对于数组同样可以结构赋值, 且赋值规则与对象相反—— 基于顺序赋值:

1
2
3
4
5
const arr = ["Alice", 25];
const [a, b] = arr;

console.log(a); // 输出:Alice
console.log(b); // 输出:25
  • 标题: JavaScript
  • 作者: ffy
  • 创建于 : 2025-01-09 14:06:30
  • 更新于 : 2025-05-08 19:54:46
  • 链接: https://ffy6511.github.io/2025/01/09/编程语言/JavaScript/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论