浏览器缓存

浏览器缓存

浏览器缓存

浏览器缓存机制是一种优化网站性能和用户体验的技术,它允许浏览器存储网页资源的副本,以便在后续的访问中可以直接从本地加载,而不需要重新从服务器下载。这种机制可以减少网络延迟、节省带宽,并提高页面加载速度。浏览器缓存可以分为以下几个部分:

  1. 浏览器缓存流程
    • 请求阶段:当浏览器请求一个资源时,会首先检查本地缓存是否有该资源的副本。
    • 匹配阶段:如果本地有缓存,浏览器会检查缓存的有效期和相关性。
    • 验证阶段:如果缓存已过期或需要验证,浏览器会向服务器发送请求,询问资源是否有更新。
    • 更新阶段:服务器响应请求,如果资源未更改,则返回304状态码,告诉浏览器可以使用本地缓存;如果资源已更改,则返回新资源和200状态码。
  2. 强缓存(HTTP 200 from Cache)
    • 通过 Cache-ControlExpires 头来控制。
    • Cache-Control 提供了多种指令,如 no-cacheno-storemax-age 等,用于控制资源的缓存行为。
    • Expires 指定资源过期的具体日期和时间,但不如 Cache-Control 灵活。
  3. 协商缓存(HTTP 304 Not Modified)
    • 当强缓存失效后,浏览器会发送带有条件请求的HTTP请求。
    • 使用 If-Modified-SinceLast-ModifiedIf-None-MatchETag 来实现。
    • If-Modified-SinceLast-Modified 基于时间戳,If-None-MatchETag 基于资源的特定标识符。
  4. ETag 和 If-None-Match
    • ETag 是服务器响应头中的一个字段,它提供了一个资源的唯一标识符。
    • If-None-Match 是浏览器请求头中的一个字段,携带上次请求中服务器返回的 ETag 值。
    • 如果 ETag 没有变化,服务器返回304状态码,告诉浏览器资源未更改,可以使用缓存副本。
  5. 缓存位置
    • 内存缓存:最快的缓存类型,但可能因为浏览器关闭而丢失。
    • 磁盘缓存:存储在硬盘上,容量更大,持久性更强。
  6. 缓存策略
    • 开发者可以通过设置HTTP头来控制缓存行为,选择合适的缓存策略可以显著提高网站性能。
  7. 清除缓存
    • 用户或开发者可以手动清除浏览器缓存,以确保获取最新的资源。
  8. 缓存与SEO
    • 合理使用缓存可以提高网站对搜索引擎的友好性,但也需要确保搜索引擎能够获取到最新的内容。
      浏览器缓存机制是一个复杂但高效的系统,它涉及到客户端和服务器端的多个组件,合理配置和使用缓存可以显著提升网站的性能和用户体验。

强缓存和协商缓存

强缓存和协商缓存是Web开发中用于提高资源加载效率和减少服务器负载的两种缓存机制。它们各自有不同的工作方式和适用场景:

强缓存(HTTP 200 From Cache)

强缓存是指浏览器在不与服务器通信的情况下,直接从本地缓存中读取资源的过程。如果资源有效并且没有过期,浏览器将使用本地缓存的副本,而不是从服务器重新下载资源。强缓存主要依赖于以下HTTP头:

  1. Cache-Control
    • Cache-Control 是最重要的HTTP缓存头之一,它提供了多种指令来控制资源的缓存行为。
    • max-age=<seconds>:指定资源能够被缓存的最大时间(以秒为单位)。
    • no-cache:指示必须先向服务器确认资源是否更新,才能从缓存读取资源。
    • no-store:指示不应该缓存请求或响应的任何部分。
    • public:响应可以被任何中间缓存存储。
    • private:响应是为单个用户准备的,只能被客户端缓存。
  2. Expires
    • Expires 指定资源过期的具体日期和时间。
    • 它是一个绝对时间,而 Cache-Controlmax-age 是一个相对时间。

协商缓存(HTTP 304 Not Modified)

当强缓存失效(即资源已过期或被配置为 no-cache)时,浏览器将使用协商缓存来决定是否需要从服务器重新获取资源。协商缓存依赖于服务器来验证资源是否自上次请求以来发生了变化:

  1. Last-Modified / If-Modified-Since
    • Last-Modified:服务器在响应中包含这个头,指示资源的最后修改时间。
    • If-Modified-Since:在随后的请求中,浏览器会包含这个头,并携带之前 Last-Modified 的值,询问服务器自那个时间以来资源是否被修改过。
  2. ETag / If-None-Match
    • ETag(Entity Tag):服务器在响应中包含这个头,提供一个资源的唯一标识符,通常是资源的哈希值。
    • If-None-Match:在随后的请求中,浏览器会包含这个头,并携带之前 ETag 的值,询问服务器资源是否发生了变化。

工作流程对比

  • 强缓存
    1. 浏览器请求资源。
    2. 服务器响应资源,并包含缓存控制头(如 Cache-ControlExpires)。
    3. 浏览器存储资源和缓存头。
    4. 后续请求时,浏览器检查本地缓存和缓存头,如果资源未过期且符合缓存规则,直接使用缓存。
  • 协商缓存
    1. 浏览器请求资源。
    2. 服务器响应资源,并包含缓存控制头和验证头(如 Last-ModifiedETag)。
    3. 浏览器存储资源和所有相关的头。
    4. 后续请求时,浏览器检查本地缓存,如果资源已过期或被标记为 no-cache,浏览器会发送带有验证头的请求。
    5. 服务器检查验证头,如果资源未更改,返回304状态码;如果资源已更改,返回新资源和200状态码。
      通过合理配置强缓存和协商缓存,可以显著提高网站的性能,减少不必要的网络请求,加快页面加载速度。

禁用浏览器缓存

禁用浏览器缓存可以通过以下几种方法实现:

  1. 开发者工具禁用缓存
    • 打开浏览器的开发者工具(通常可以通过按 F12Ctrl+Shift+I(Windows/Linux)或 Cmd+Opt+I(Mac)打开)。
    • 转到“网络”(Network)标签页。
    • 勾选“禁用缓存”(Disable cache)选项。这样,在开发者工具打开的情况下,浏览器会忽略缓存并从服务器重新加载资源。
  2. 使用Meta标签
    • 在HTML文档的 <head> 部分,可以添加以下meta标签来防止页面被缓存:
      1
      2
      3
      <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
      <meta http-equiv="Pragma" content="no-cache" />
      <meta http-equiv="Expires" content="0" />
    • 这些标签告诉浏览器不缓存页面内容,每次访问时都从服务器获取最新版本。
  3. 浏览器设置
    • 在某些浏览器中,你可以直接在设置中禁用缓存。例如,在Firefox中可以通过进入 about:config 页面,搜索并设置 network.http.use-cachefalse
  4. 使用Chrome扩展程序
    • 可以使用专用的Chrome扩展程序来禁用缓存。例如,Classic Cache Killer插件可以一键开启或关闭缓存。
  5. 清除浏览器缓存
    • 在Chrome浏览器中,可以通过以下步骤清除缓存:
      • 点击浏览器右上角的菜单按钮(三个竖点图标)。
      • 选择“更多工具”>“清除浏览数据”。
      • 在弹出的对话框中,选择要清除的时间范围(建议选择“全部时间”)。
      • 勾选“缓存图像和文件”选项。
      • 点击“清除数据”按钮。
  6. 修改Chrome启动参数
    • 通过更改Chrome启动参数,比如使用快捷方式,在路径末尾chrome.exe之后添加--disk-cache-size=1或者--disk-cache-dir=nul,可以达到禁用缓存的效果。
  7. Chrome Flags设置
    • 在Chrome浏览器的地址栏中输入 chrome://flags/ 进入浏览器设置页面。
    • 搜索“禁止缓存 (Disable caching of all the network requests)”选项,并将其设置为“启用 (Enabled)”状态。
    • 重启浏览器后,所有网页的缓存功能将被禁用。
      请注意,禁用缓存可能会导致访问速度变慢,因为每次访问页面都需要重新获取最新的数据。同时,禁用缓存还可能在一定程度上增加服务器的负担,因此需要根据实际需求进行设置。

缓存的存取

在JavaScript中,可以通过多种方式实现缓存的存取,常见的缓存方式包括浏览器缓存(如Cookie、localStorage、sessionStorage、IndexedDB等)和内存缓存。以下是这些缓存方式的使用方法和特点:

Cookie是存储在浏览器中的小型文本文件,通常用于存储用户会话信息等。

存储Cookie

1
document.cookie = "username=John Doe; expires=Thu, 18 Dec 2023 12:00:00 UTC; path=/";

读取Cookie

1
2
const x = document.cookie;
console.log(x); // 输出所有Cookie

特点

  • 限制:Cookie大小有限制(通常为4KB),且每次请求都会发送到服务器。
  • 适用场景:适合存储少量数据,如用户会话信息。

2. localStorage

localStorage是HTML5提供的Web存储机制,用于存储大量数据,且数据不会过期。

存储数据

1
localStorage.setItem("username", "John Doe");

读取数据

1
2
const username = localStorage.getItem("username");
console.log(username); // 输出:John Doe

删除数据

1
2
localStorage.removeItem("username"); // 删除指定键
localStorage.clear(); // 清空所有数据

特点

  • 容量:通常可存储5MB左右。
  • 适用场景:适合存储用户偏好设置、主题切换等。

3. sessionStorage

sessionStoragelocalStorage类似,但数据仅在当前会话中有效,关闭浏览器标签页后数据会被清除。

存储数据

1
sessionStorage.setItem("sessionKey", "sessionValue");

读取数据

1
2
const sessionValue = sessionStorage.getItem("sessionKey");
console.log(sessionValue); // 输出:sessionValue

特点

  • 容量:与localStorage类似,通常为5MB。
  • 适用场景:适合存储会话级别的数据,如登录状态。

4. IndexedDB

IndexedDB是一种低级API,用于存储大量结构化数据。它是一个非关系型数据库,支持事务操作。

存储数据

1
2
3
4
5
6
7
8
9
10
11
12
13
// 打开数据库
const request = indexedDB.open("myDatabase", 1);
request.onupgradeneeded = function(event) {
const db = event.target.result;
const objectStore = db.createObjectStore("myStore", { keyPath: "id" });
objectStore.add({ id: 1, name: "John Doe" });
};
request.onsuccess = function(event) {
const db = event.target.result;
const transaction = db.transaction(["myStore"], "readwrite");
const objectStore = transaction.objectStore("myStore");
objectStore.add({ id: 2, name: "Jane Doe" });
};

读取数据

1
2
3
4
5
6
7
8
9
10
const request = indexedDB.open("myDatabase", 1);
request.onsuccess = function(event) {
const db = event.target.result;
const transaction = db.transaction(["myStore"], "readonly");
const objectStore = transaction.objectStore("myStore");
const getRequest = objectStore.get(1);
getRequest.onsuccess = function(event) {
console.log(event.target.result); // 输出:{ id: 1, name: "John Doe" }
};
};

特点

  • 容量:存储容量较大,通常可达几十MB。
  • 适用场景:适合存储复杂数据结构,如离线应用。

5. Memory Cache(内存缓存)

内存缓存通常用于临时存储数据,数据存储在JavaScript的内存中,页面刷新后数据会丢失。

存储数据

1
2
const memoryCache = {};
memoryCache["key"] = "value";

读取数据

1
console.log(memoryCache["key"]); // 输出:value

特点

  • 容量:受限于浏览器内存。
  • 适用场景:适合临时存储数据,如页面级缓存。

6. Cache API

Cache API是HTML5提供的用于存储和管理HTTP请求和响应的API,主要用于实现离线功能和服务端缓存。

存储数据

1
2
3
caches.open("my-cache").then(cache => {
cache.put(new Request("https://example.com/image.jpg"), new Response(new Blob(["Hello, World!"])));
});

读取数据

1
2
3
4
5
caches.match("https://example.com/image.jpg").then(response => {
if (response) {
response.text().then(text => console.log(text)); // 输出:Hello, World!
}
});

特点

  • 适用场景:主要用于离线应用和服务端缓存。

总结

  • Cookie:适合存储少量数据,如用户会话信息。
  • localStorage:适合存储持久化数据,如用户偏好设置。
  • sessionStorage:适合存储会话级数据,如登录状态。
  • IndexedDB:适合存储复杂数据结构,如离线应用。
  • Memory Cache:适合临时存储数据,如页面级缓存。
  • Cache API:适合实现离线功能和服务端缓存。
    根据具体需求选择合适的缓存方式,可以提高应用的性能和用户体验。

02 function类型对象

02 🎈function类型对象

在 JS 中, function可以说是最核心的内容了。它本身是一种对象,另外,它还可以创建对象,而且可以对对象进行操作,所以这块内容可以说是 JS 中最复杂的内容 。
JS 中的 function 有三大作用 :

  • 作为对象来管理其中的属性
  • 作为方法处理具体业务
  • 创建对象
    本文首先介绍怎样创建function,然后分别介绍它的三种用法,最后介绍function的三种子类型

1 💎创建function

在 JS 中创建 function 的常用方法有两种:

1.1 函数声明

函数声明的结构如下

1
2
3
function 函数名(参数){
函数体
}

函数由4个部分组成

  • function关键词,固定不变
  • 函数名
  • 圆括号内存放函数参数
  • 花括号包含的函数体

1.2 函数表达式

函数表达式的结构与函数声明相比,是将函数声明中的函数名去掉,然后将创建的结果赋值给一个变量

1
2
3
var 变量 = function (参数){
函数体
}

函数表达式和函数声明出来的函数基本没有区别。

2个方式的关系

在 JS 中所有的数据只有两种存在形式,要么是对象的属性,要么是变量。
函数也不例外,无论是对象的属性还是变量都是名值对的结构,因此函数也应该是这种名值对的结构,由函数表达式可以很容易看明白这一点。
其实,通过函数声明方式创建函数时, JS 在背后自动帮用户做了这件事情,它首先创建了函数对象,然后又创建了和函数名同名的变量,并将创建出来的函数赋值给了这个变量 。

1
2
3
4
5
6
function(a){ 
console.log(a);
}
var f = F;
F = null;
f ('hello function');//输出 hello function

!Drawing 2024-04-13 23.28.52.excalidraw

2 💎用作对象

JS 中的函数本身也是对象,是对象就可以有自己的属性。函数对象的属性一般是使用点操作符来操作的,可以通过点给对象的属性进行赋值,如果属性不存在则直接创建,如果存在,则可以修改其内容 。
函数对象的属性也可以是直接量、 object 对象和 function 对象三种类型中的任意一种 。
如果是 function对象类型的属性,还可以通过点操作符来调用它执行相应逻辑。我们来看下面的例子 。

1
2
3
4
5
6
function func () {}; 
func.val = "go";
func.logVal = function () {
console.log(this.val);
}
func.logVal(); //go

05 点运算符与 this 关键字 该使用方式其实就是把 function 当做构造函数,可以参考 es6 新功能 class
function 对象也可以当作普通的object 对象来使用。

3 💎处理业务

在 JS 中,使用函数function对象用来处理业务是最常见的用法。JS 中真正对对象的操作大部分都是通过函数对象来执行的 。函数的创建方式前面已经介绍过,其中用来处理业务的是函数体。函数体主要包括变量、操作符和语句三大部分内容。

3.1 变量

js变量

3.2 语句

语句是用来执行具体功能的,可以分为单条语句和语句块 。 单条语句以分号结束,语句块用花括号包含,语句块可以包含多条语句 。
语句主要由变量(或属性)、操作符和关键字组成。有的语句使用操作符完成,还有的语句需要使用相应的关键字。下面将介绍使用关键字完成的语句。
js语句

3.3 变量作用域

这里所说的变量作用域主要是指使用 var 定义的变量的作用域,这种变量的作用域是 function 级的,这一点在前面讲 var 语句的时候已经提过了。 JS 中的 function 是可以嵌套使用的,嵌套的 function 中的变量的作用域又是怎样的呢?请先看个例子。

1
2
3
4
5
6
7
8
9
var v = O; 
function fl(){
var v = 1;
function f2(){
console.log(v);
}
f2();
}
fl(); //1

这个例子中,定义了全局变量 v ,在函数 f1 中定义了局部变量 v ,f2定义在 f1 函数中,当调用函数 f1 时,会在其内部调用函数 f2,f2中用到了变量 v ,这时 v 会使用 f1 函数中定义的 v 。
在调用嵌套函数时,引擎会根据嵌套的层次自动创建一个参数作用域链,然后将各层次函数所定义的变量从外到内依次存放到作用域链中。例如,在上述示例中,执行函数 f2时,会首先将全局对象(浏览器中指页面本身,也就是 Window 对象)放在最下层,然后放 f1 最后放 f2。

3.4 闭包

js闭包

💎4 创建对象

JS 中的 function 除了前面介绍的两种用法之外,还有一种非常重要的用法,那就是创建 object 实例对象。03 object类型对象详细介绍了 object 对象,本文主要介绍如何使用 function 来创建 object 对象以及创建时的一些细节问题。

4.1 创建方式

使用 function 对象创建 object 类型对象的方法非常简单,直接在 function 对象前面使用 new 关键字就可以了,例如下面的例子 。

1
2
3
4
5
function F() {
this.v = 1;
}
var obj = new F(); //创建F类型对象obj
console.log(obj.v); //1

这个例子中,首先定义了一个 function 类型的对象 F ,然后使用 F 创建了 object 类型的对象 obj ,最后在控制台打印出 obj 对象的 v 属性的值 。
使用 function (例如 F )创建 object 类型的对象(例如 obj ),只需要在 function 对象(F)前加 new 关键字就可以了。也就是说,对于一个 function 类型的对象,如果调用时前面没有 new 关键字,那么调用方法处理业务,如果前面有 new 关键字,那么用来创建对象。当然,创建对象时函数体也会被执行。
其实,经常使用的 Array,Date 等对象也都是 function 类型,可以使用 new 关键字来创建相应的 object 类型的对象实例。
为了区分主要用于处理业务的 function 和主要用于创建对象的 function ,一般会将主要用于创建对象的 function 的首字母大写而将主要用于处理业务的 function 的首字母小写 。 但这只是人为区分,实际使用时并没有什么影响。

4.2 创建过程

使用 function 创建 object 类型对象的过程可以简单地分为以下两步(可以这么理解,实际创建过程要复杂一些)。

  1. 创建 function 对应类型的空 object 类型对象 ;
  2. 将 function 的函数体作为新创建的 object 类型对象的方法来执行(主要目的是初始化 object 对象)
    例如下面的例子 。
1
2
3
4
5
6
function Car(color, displacement) { 
this.color= color ;
this.displacement = displacement;
}
var car = new Car("black","2.4T");
console.log(car.color,car.displacement); //black, 2.4T

这个例子中,首先创建了 function 类型的 Car ,然后使用它新建 object 类型的 car 实例对象。在新建 car 对象时首先会新建 Car 类型的空对象 car,然后再将 Car 函数作为新建对象的方法来调用,从而初始化新建的 car 实例对象,相当于下面的过程。

1
2
3
4
5
6
7
8
function Car(){} 
var car= new Car();
car.init = function(color, displacement) {
this.color= color ;
this.displacement = displacement;
}
car.init("black","2.4T");
console.log(car.color,car.displacement); //black, 2.4T

上述示例将原来 Car 对象中的函数体的内容放到新建的 car 的 init 方法中,在使用 Car 创建完 car 实例对象后,再调用 init 方法初始化,这种方式和前面例子中将初始化内容放到 Car 的函数体内的效果是完全相同的 。
需要特别注意的是,创建过程的第二步,也就是说,在使用 function 对象新建 object 对象时依然会执行 function 的函数体。通过下面的例子可以更加直观地看到这一点。

1
2
3
4
5
6
7
var name="和坤";
function Sikuquanshu() {
name="纪晓岚"
}
console.log(name);//和神
var skqs = new Sikuquanshu();
console.log(name);//纪晓岚

这个例子中存在一个全局变量 name ,原值为“和坤”,在函数 Sikuquanshu 内部将其改为“纪晓岚”,在上述代码中并没有直接执行此函数,但在使用它创建 skqs 对象时其函数体得到了执行,这从创建 skqs 对象前后打印出的内容就能看出来,全局变量 name 被修改了。在使用 function 对象创建实例对象时一定要注意这一点 。
理解了对象创建的过程就可以理解为什么在构造函数(例如 Car )中使用 this 可以将属性添加到新创建的对象上 。 因为这时的函数体就相当于新创建的对象的一个方法,方法中的 this 指的就是新创建的对象自身,给 this 赋值就是给新创建的对象赋值,因此在 function 对象中使用 this 就可以给新创建出来的对象添加属性,就像本书第一个例子中的 Car 方法的 this.color = color;语句,这条语句会给新创建的 car 对象添加 color 属性并将 color 参数的值赋给它。对于这一点,在后面讲到 object 的属性时还会做进一步介绍。
05 点运算符与 this 关键字
js对象在内存中怎样保存

4.3 💎prototype属性与继承

js中prototype属性与继承

5 三种子类型

前面介绍过 ES 中的 function 共有三种用法 :作为对象使用、处理业务以及创建 object 类型的实例对象 。 跟这三种用法相对应的有三种子类型,分别是对象的属性、变量(包括参数)和创建出来的 object 类型实例对象的属性。这三种子类型是相互独立的,而且也很容易区分 。

5.1 function 作为对象来使用

这种情况下, function 对象的子类型就是对象自己的属性,这时通过点操作符“.”(或者方括号操作符)使用,例如下面的例子。

1
2
3
4
function book(){} 
book.price = 161.0;
book.getPrice = function(){return this.price;}
console.log (book.getPrice()) ; //161

在这种情况下, function 是作为 object 类型的对象来使用的。上面的例子中首先定义了 function 类型的 book 对象,然后给它添加了 price 属性和 getPrice 方法,这时就可以直接使用点操作符来对其进行操作了。

5.2 function 用于处理业务

这种情况下, function 的子类型就是自己定义的局部变量(包括参数),这时的变量是在方法被调用时通过变量作用域链来管理的。变量作用域链的相关内容前面已经介绍过,这里就不再重述了。

5.3 function 用于创建对象

这种情况下,对应的子类型是使用 function 创建的实例对象的属性,主要包括在 function 中通过 this 添加的属性,以及创建完成之后实例对象自己添加的属性。另外,还可以调用 function 的 prototype 属性对象所包含的属性,例如前面用过的 Car 的例子。

1
2
3
4
5
6
function Car(color, displacement) { 
this.color= color ;
this.displacement = displacement;
}
var car = new Car("black","2.4T");
console.log(car.color,car.displacement); //black, 2.4T

这个例子中创建的 car 对象就包含有 color 和 displacement 两个属性,而且还可以调用 Car. prototype 的 logMessage 方法。当然,创建完之后还可以使用点操作符给创建的 car 对象添加或者修改属性,也可以使用 delete 删除其中的属性,例如下面的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Car(color, displacement) { 
this.color= color ;
this.displacement = displacement;
}
Car.prototype.logMessage = function(){
console.log(this.color,this.displacement);
}
var car = new Car("black","2.4T");
car.logColor = function(){
console.log(this.color);
}
car.logColor() //black
car.color = "red"
car.logColor() //red
delete car.color
car.logMessage() //undefined 2.4T

这个例子中,在创建完 car 对象后又给它添加了 logColor 方法,可以打印出 car 的 color 属性 。 添加完 logColor 方法后直接调用就可以打印出 car 原来的 color 属性值( black ) 。 然后, 将其修改为 red ,再打印就打印出了 red 。 最后,使用 delete 删除 car 的 color 属性,这时再调用 logColor 方法就会打印出 undefined 。

5.4 三种子类型的关系

function 的三种子类型是相互独立的,它们只能在自己所对应的环境中使用而不能相互调用,例如下面的例子 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Bird() {
var name = "kitty ";
this.type = "pigeon"
this.getName = function() {
return this.name;
}
}
Bird.color = "white";
Bird.getType = function() {
return this.type;
}
Bird.prototype.getColor = function() {
return this.color;
}
var bird = new Bird();
console.log(bird.getColor());//undefined
console.log(bird.getName());//undefined
console.log(Bird.getType());//undefined

这个例子中的最后三条语句都会打印出 undefined ,下面分析其中的原因。

  • Bird 作为对象时包含 color 和 getType 两个属性
  • 作为处理业务的函数时包含一个名为 name 的局部变量
  • 创建的实例对象 bird 具有 type 和 getName 两个属性,而且还可以调用 Bird.prototype 中的 getColor 属性, getColor 也可以看作 bird 的属性
    每种用法中所定义的方法只能调用相应用法所对应的属性,而不能交叉调用, getName 、 getColor 和 getType 三个方法都获取不到对应的值,所以它们都会输出 undefined 。
用法 子类型
对象 Bird color,getType
处理业务 Bird 方法 name
创建实例对象 bird type,getName,(getColor)
另外, getName 和 getColor 是 bird 的属性方法, getType 是 Bird 的属性方法,如果用 Bird 对象调用 getName 或 getColor 方法或者使用 bird 对象调用 getType 方法都会抛出找不到方法的错误 。
除了三种子类型不可以相互调用之外,还有一种情况也非常容易被误解,那就是对象的属性并没有继承的关系,例如下面的例子 。
1
2
3
4
5
6
7
8
function obj() {} 
obj.v = 1;
obj.func = {
logV: function() {
console.log(this.v);
}
}
obj.func.logV();

这个例子中的 obj 是作为对象使用的, obj 有一个属性 v 和一个对象属性 func, func 对象中又有一个 logV 方法, logV 方法用于打印对象的 v 属性。这里需要特别注意, logV 方法打印的是 func 对象的 v 属性,但是 func对象并没有 v 属性,所以最后会打印出 undefined 。
在这个例子中,虽然 obj 对象中包含 v 属性,但是由于属性不可以继承,所以 obj 的 func 属性对象中的方法不可以使用 obj 中的属性 v 。这一点各位读者一定要记住,并且不要和 prototype 的继承以及变量作用域链相混淆。
JS中公有属性私有属性和静态属性

03 object类型对象

03 object类型对象

ES 中一共有两种对象: function 类型对象和 object 类型对象。object类型对象是ES的基础,它主要是通过属性使用。本文将详细介绍object类型对象的相关内容。

1 创建 object 类型对象的三种方式

ES 中 object 类型的对象大致有三种创建方式 : 直接使用花括号创建、使用 function 创建以及使用 Object.create 方法创建 。

1.1 直接使用花括号创建

使用花括号创建对象时直接将属性写到花括号中就可以了,属性名和属性值使用冒号(:) 分割,不同属性之间使用逗号(,)分割(注意,最后一个属性后面没有逗号),属性的值可以是直接量,也可以是 object 类型对象或者 function 类型对象 。
object 对象调用属性有两种方法:直接使用点操作符调用;使用方括号调用 。 因为使用方括号没有使用点操作符方便,所以一般使用点操作符调用的比较多,不过当属性名为一个变量时则只能使用方括号来调用。

1.2 使用 function 创建

关于使用 function 创建 object 实例对象,02 function类型对象#4 创建对象讲过了,是使用 new 关键字来创建的,而且创建出来的对象还可以使用 function 的 prototype 属性对象中的属性 。

1.3 使用 Object.create 方法创建

还有一种创建 object 类型对象的方式,那就是使用 Object.create 来创建。 Object 是 ES 中内置的一个 function 类型的对象, create 是 Object 对象的一个属性方法,其作用是根据传入的参数创建 object 类型的对象 。 create 方法的调用语法如下 。

1
Object.create { prototype, [ propertiesObject ] )

其中,第一个参数 prototype 是创建的对象所对应的 prototype ,相当于使用 function 创建时 function 中的 prototype 属性对象,创建出来的 object 对象实例可以直接调用 。 第二个参数 propertiesObject 为属性描述对象,是可选参数,用于描述所创建对象的自身属性 。 属性描述对象是 object 类型对象,它里面的属性名会成为所创建对象的属性名,属性值为属性的描述对象,其中包含属性的特性(属性的特性我们后面会详细讲解,这里大家只要知道其中的 value 就是属性值就可以了) 。 我们来看下面的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var obj = Object.create(
//prototype
{
type: "by create"
},
// propertiesObject
{
color: {
value: "red",
enumerable: true
},
size: {
value: "37",
enumerable: true
}
}
)
console.log(obj);//{color: 'red', size: '37'}
console.log(obj.type); //by create
console.log(obj.color); //red
console.log(obj.size); //37
console.log(Object.getOwnPropertyNames(obj)); //[" color "," size")

这个例子中,使用 Object.create 创建了一个 obj 对象,第一个参数 prototype 中有一个属性 type,第二个参数 propertiesObject 有两个属性: color 和 size 。对于创建的 obj 对象来说,这三个属性都可以使用,但其自身其实只有两个属性。虽然 obj 可以调用 prototype 中的 type 属性,但是并不属于 obj ,使用 Object. getOwnPropertyNames 方法就可以获取 obj 自己所拥有的属性。
另外,需要注意的一点是,使用花括号和 function 创建的对象都可以调用 Object 的 prototype 属性对象中的属性。即这两种方法创建出来的对象首先会拥有公共的 prototype,然后使用 function 创建的对象还可以调用 function 的 prototype 中的属性,而且即使 function 的 prototype 为 null ,创建的对象也可以调用 Object 的 prototype 中的属性。但是,使用 Object. create 创建对象时,如果第一个参数为 null ,那么它所创建的对象将不可以调用 Object 的 prototype 中的属性。我们来看下面的例子。

1
2
3
4
5
6
7
8
9
10
11
//使用花括号创建对象 braceObj
var braceObj = {}
//使用 function 创建对象 functionOb〕
function F() {}
F.prototype = null;
var functionObj = new F();
//使用 create 方法创建对象 createObj
var createObj = Object.create(null);
console.log(braceObj.toString());//[object Object]
console.log(functionObj.toString());//[object Object]
console.log(createObj.toString());//抛出方法没找到异常

在这个例子中, toString 方法是 Object 的 prototype 属性中的一个方法,如果创建出来的对象可以调用 toString 方法,就说明可以调用 Object 的 prototype 属性对象中的方法,否则就是不可以调用。从这个例子中可以看出,使用花括号和 function 创建的对象都可以调用 tostring 方法,而且即使将 function 的 prototype 设置为 null ,创建出来的对象也可以调用,但使用 Object.create 创建出来的对象,如果其 prototype 参数为 null 就不可以调用了。
Object的prototype属性对象里有什么

2 对象的属性

对象是通过其属性来发挥作用的,因此对象的属性是对象的核心 。

2.1 三种属性类型

ES 中对象的属性其实有三种类型,前面使用的属性只是其中的一种 。 属性的三种类型分别为:命名数据属性( named data properties )、命名访问器属性( named accessor properties) 和内部属性( internal properties ) 。下面我们分别学习 。

命名数据属性Data Property

命名数据属性是我们平时使用最多的属性,由属性名和属性值组成。前面例子中所使用的都是这种属性。

命名访问器属性Accessor Property

命名访问器属性是使用 getter、 setter 或者其中之一来定义的属性。getter 和 setter 是对应的方法, setter 方法用于给属性赋值,而 getter 方法用于获取属性的值。如果只有 getter、 setter 其中之一,就只能进行单一的操作,例如,只有 getter 方法的属性就是只读属性,只有 setter 方法的属性就是只可以写入的属性。例如下面的例子。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function log(msg) {
console.log(msg)
}
var colorManager = {
_colorNum: 3,
_accessColors: ["red", "green", "blue"],
_color: "red",
//colorNum 为只读属性,只需定义 get 方法
get colorNum() {
return this._colorNum;
},
//accessColors 为只可写入的属性,只需定义 set 方法
set accessColors(colors) {
this._colorNum = colors.length;
this._accessColors = colors;
log("accessColors 被修改了");
},
//color 为可读写属性 , 同时定义了 get 、 set 方法
set color(color) {
if (this._accessColors.indexOf(color) < 0) {
//判断设置的 color 是否在允许的范围内
log("color 不在允许范围内")
return;
}
log("color 值被修改为" + color);
this._color = color;
},
get color() {
log("正在获取 color 值")
if (this._accessColors.indexOf(this._color) < 0) {
return null;
}
return this._color;
}
}
log(colorManager.color); //正在获取 color 值 -> red
colorManager.accessColors = ["white", "black", "red", "yellow", "orange"]; //accessColors 被修改了
log(colorManager.colorNum); //5
colorManager.color = "blue"; //color 不在允许范围内
colorManager.color = "orange"; //color 值被修改为 orange
log(colorManager.color); //正在获取 color 值 orange

在这个例子中,我们定义了一个 colorManager 对象。它有三个访问器属性: colorNum 、 accessColors 和 color 。其中, colorNum 表示可以使用的 color 的数量,只有 getter 方法是只读属性; accessColors 表示可以使用的 color 的数组,只有 setter 方法是只写属性; color 表示当前的 color 值,是可读写属性。当给相应的属性设置值的时候就会调用相应的 setter 方法,调用属性值的时候就会调用相应的 getter 方法。我们在各个访问器方法中除了修改(或读取) 属性值之外,还做了一些逻辑判断以及打印日志的相关工作。
从这个例子可以看到 getter 和 setter 方法本身并不可以保存属性的内容。通常另外定义一个以下画线开头的属性来保存访问器属性的值,而且为了方便,一般会将保存访问器属性值的属性的名字设置为访问器属性名前加下画线。例如,在上面例子中使用 _color 来保存 color 访问器属性的值,使用 _colorNum 来保存 colorNum 访问器属性的值。这并不是强制性的,保存值的属性的名称也可以用其他名称。但是,为了方便和代码容易理解最好还是按照这个规则来命名。
这里大家可以会有一个疑问,那就是在定义了保存访问器属性值的属性之后,如果直接操作这个属性,不就可以绕过访问器来操作其值了吗?例如,直接操作 _color 属性不就可以绕过访问器方法了吗?对于这个问题,下文会有解决办法。

内部属性

内部属性是对象的一种特殊属性 。 它没有自己的名字,当然也就不可以像前两种属性那样直接访问了 。 正是因为内部属性没有名字所以前面两种属性才叫作命名属性 。 内部属性使用两对方括号表示 。 例如, Extensible 表示 Extensible 内部属性 。
内部属性的作用是用来控制对象本身的行为。所有对象共有的内部属性共 12 个: Prototype、Class、Extensible、Get、GetOwnProperty、GetProperty、Put、CanPut、HasProperty、Delete、DefaultValue和 DefineOwnPrope
除了这 12 个之外,不同的对象可能还会有自己的内部属性例如 Function 类型对象的 Haslnstance 、 RegExp 类型对象的 Match 等。通用的 12 个内部属性中的前 3 个可用来指示对象本身的一些特性,后 9 个属性可对对象进行特定的操作。它们在进行相应的操作时会自动调用,这里就不详细介绍了。下面主要解释下前3个属性。

Prototype

Prototype 属性就是前面讲过的使用 function 创建对象时 function 中的 prototype 属性。在创建完的实例对象中,这个属性并不可以直接调用,但可以使用 Object 的 getPrototypeOf 方法来获取,例如下面的例子。

1
2
3
4
5
function Car() {} 
Car.prototype = {color :"black"};
var car = new Car();
console.log(typeof car.prototype); //undefined
console.log(Object.getPrototypeOf(car)); //Object{color ="black"}

这个例子中,使用 Car 新建了 car 对象, Car 的 prototype 属性对象中有一个 color 属性,这个对象就是 car 实例的 Prototype 属性,不能使用 car. prototype 获取,可使用 Object. getPrototypeOf( car )获取。在有些浏览器(例如 Firefox )中还可以使用 __proto__ 属性来获取,例如,这里使用 car.__proto__同样可以获取Prototype属性。但是,因为 __proto__ 属性不是通用属性,所以最好还是使用 Object 的 getPrototypeOf 方法来获取。

Class

Class 属性可用来区分不同对象的类型,不能直接访问, toString 方法默认会返回这个值。默认 Object. prototype. toString 方法返回的字符串是 [ object, Class ],即方括号里面两个值,第一个是固定的 object ,第二个是 Class,因此使用 toString 方法就可以获取对象的 Class 属性。但是,因为 ES 中的内置对象在 prototype 中重写了 toString 方法,所以内置对象的返回值可能不是这个形式。浏览器中的宿主对象并没有重写此方法,在浏览器中调用它们的 toString 方法可以获取 Class 的值,例如下面的例子。

1
2
3
4
function log(msg) { console.log(msg); }
log(window.toString());//[object Window]
log(document.toString()); //[object HTMLDocument]
log(navigator.toString()); //[object Navigator]

我们自己创建的 object 类型对象默认都属于 Object 类型,因此,它们的 toString 方法默认都会返回[ object Object ] 。另外,内置对象因为重写了 toString 方法,所以不会返回这种结构的返回值,例如,字符串对象会返回字符串自身,数组会返回数组元素连接成的字符串等。对于这种情况,我们可以使用 Object. prototype. toString 的 apply 属性方法来调用 Object 原生的 toString 方法,这样就会得到 [object,Class ] 这样的结果,例如下面的例子。

1
2
3
4
var str = "",
arr = [];
console.log(Object.prototype.toString.apply(str));//[object String]
console.log(Object.prototype.toString.apply(arr));//[object Array]

逆向调用apply和call

Extensible

Extensible 属性用来标示对象是否可扩展,即是否可以给对象添加新的命名属性,默认为 true ,也就是可以扩展。我们可以使用 Object 的 preventExtensions 方法将一个对象的 Extensible 值变为 false ,这样就不可以扩展了。另外,可以使用 Object 的 isExtensible 方法来获取 Extensible 的值。我们来看个例子。

1
2
3
4
5
6
7
8
var person = {
nationality: "中国"
};
console.log(Object.isExtensible(person)); //true
person.name="欧阳修"; //现在可以添加属性
Object.preventExtensions(person); //将Extensible设置为 false
console.log(Object.isExtensible(person)); //false
person.age = "108"; //抛出异常

这个例子中 ,我们定义了 person 对象,它的Extensible属性本来为 true ,我们可以给它添加命名属性 。当调用 preventExtensions 方法对其操作后Extensible属性就变为了 false ,这时就不能给它添加新的命名属性了 。 需要注意的是,一旦使用 preventExtensions 方法将Extensible的值设置为 false 后就无法改回 true 了 。

5 种创建属性的方式

本节主要指创建命名属性 。 对象的命名属性一共有 5 种创建方式 。

使用花括号创建

这种方式是在使用花括号创建对象时创建属性,例如下面的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
var obj = {
v:1.0,
getV:function(){
return this.v
},
_name:"object",
get name(){
this._name
},
set name(name){
this._name = name
}
}

这个例子中,使用花括号创建了 obj 对象,其中包含直接量属性( v ) 、 function 对象属性( getV )以及访问器属性( name ) 。注意,在定义访问器属性的 getter 和 setter 方法时没有冒号 。

使用点操作符创建

当使用点操作符给一个对象的属性赋值时,如果对象存在此属性则会修改属性的值,否则会添加相应的属性并赋予对应的值,例如下面的例子。

1
2
3
var person = {name :"张三"}
person.name = "李四"//修改原有属性的值
person.age = 88; //添加新属性

这个例子中,首先使用花括号定义了 person 对象,其中包含 name 属性,当给它的 name 属性赋予新值时会改变其 name 属性的值,而当给 age 属性赋值时,由于 person 原来没有 age 属性,所以会先添加 age 属性,然后将其值设置为 88 。
在 function 中使用 this 创建属性其实也是这种添加属性方式的一种特殊用法。因为在 function 创建 object 类型对象时,其中的 this 就代表创建出来的对象,而且刚创建出来的对象是没有自定义的命名属性的,所以使用 this 和点操作符就可以将属性添加到创建的对象中,例如下面的例子。

1
2
3
4
5
function Person() {
this.name = "孙悟空";
}
var person = new Person()
console.log(person.name); //孙悟空

在这个例子中,首先定义了 function 类型的 Person ,然后用其创建了 person 对象,创建完成后会自动调用 Person 方法体中的 this.name = "孙悟空"语句,这时,由于 this 所代表的 person 对象并没有 name 属性,所以会自动给它添加 name 属性,这也就是创建的 person 对象具有 name 属性的原因了。

Object 的 create 方法

我们在前面已经介绍过 Object 的 create 方法,它有两个参数,第一个参数中的属性为创建的对象的Prototype 属性,第二个参数为属性描述对象 。

Object 的 defineProperty 、defineProperties 方法

我们可以使用 Object 的 defineProperty 和 defineProperties 方法给对象添加属性。 defineProperty方法可添加单个属性, defineProperties 方法可以添加多个属性。 Object 的 defineProperty 方法一共有三个参数,第一个是要添加属性的对象,第二个是要添加属性的属性名,第三个是属性的描述。前两个参数都很简单,第三个我们会在后面详细讲解,先来看个例子。

1
2
3
4
5
6
7
var obj = {};
Object.defineProperty(obj, "color", {
enumerable: true,
value: "green"
})
console.log(Object.getOwnPropertyNames(obj)); //['color']
console.log(obj.color); //green

在这个例子中,我们使用 defineProperty 方法给 obj 对象添加了 color 属性。 Object 的 defineProperties 方法可以创建多个属性,它有两个参数,第一个参数是要添加属性的对象,第二个参数是属性描述对象,和 create 方法中的第二个参数一样,例如下面的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var obj = {};
Object.defineProperties(obj, {
name: {
enumerable: true,
writable: false,
value: "lucy"
},
color: {
enumerable: true,
value: "green",
}
})
console.log(Object.getOwnPropertyNames(obj)); //['name', 'color']
obj.name = "peter"
console.log(obj.name);//lucy

这个例子使用 Object 的 defineProperties 方法给 obj 对象添加了 name 和 color 两个属性 。 在这个例子中, 因为 name 属性的 writable 为 false ,所以 obj 的 name 属性是不可以修改的 。 当我们将其值修改为 peter 后, 打印出的还是原来的 lucy ,这说明修改并没有作用 。 而且, 使用 defineProperties 方 法添加属性时 writable 的默认值就是 false 。

通过 prototype 属性创建

使用 function 创建的 object 实例对象可以使用 function 对象的 prototype 属性对象中的属性,这一点我们在前面已经多次证实过。严格来说, function 对象的 prototype 中的属性并不会添加到创建的实例对象中,但创建的对象可以调用,这样就相当于可以将 prototype 中的属性添加到创建的对象中。因此,如果给 function 的 prototype 添加了属性,那么也就相当于给创建的对象添加了属性,而且在对象创建完成之后还可以再添加,例如下面的例子。

1
2
3
4
function Shop(){}
var shop = new Shop();
Shop.prototype.type = "网络销售"
console.log(shop.type); //网络销售

这个例子中,首先使用 Shop 创建了 shop 对象,然后给 Shop 的 prototype 添加了 type 属性,这时调用 shop.type 也可以获取属性值。在调用 shop. type 时,因为 shop 没有 type 属性, shop 就会实时到 Shop 的 prototype 中查找,而不是提前将 Shop 的 prototype 属性对象保存起来,所以创建完 shop 对象后再修改 Shop 的 prototype 属性,已修改的属性也可以被 shop 实例对象调用。这一点在前面已经介绍过。

属性的描述

属性的描述也可以称为属性的特性,类似于对象的内部属性,其主要作用就是描述属性自己的一些特征 。 它的表示方法和对象的内部属性一样,也使用两个方括号表示 。 对象的命名数据属性和命名访问器属性各有 4 个特性(没有内部属性),其中两个特性是命名数据属性和命名访问器属性所共有的 。 下面我们来分别学习 。

命名数据属性的 4 个特性

命名数据属性的 4 个特性分别为:Value Writable Enumerable Configurable
Value表示属性的值;
Writable 表示属性值是否可以修改
Enumerable 表示属性是否可枚举,如果为 false 则不会被 for-in 循环遍历到;
Configurable 表示属性是否可以被删除和属性的特性(除 Value 外)是否可修改。
属性的特性可以使用 Object 的 getOwnPropertyDescriptor 方法查询。如果想修改,那么可以使用 Object 的 defineProperty 和 defineProperties 方法,这两个方法所操作的属性如果存在就会对其进行修改,否则就会创建。我们来看下面这个例子。

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
28
29
30
31
32
function log(msg) {
console.log(msg);
}
var person = {
name: "peter"
};
log(Object.getOwnPropertyDescriptor(person, "name"));
//Object {value: 'peter', writable: true, enumerable: true, configurable: true}
//将 person 的 name 属性设置为不可修改
Object.defineProperty(person, "name", {
writable: false
})
person.name = "maker"; //修改无效
log(person.name); //peter
//添加 age 属性
Object.defineProperty(person,
"age", {
value: 18,
configurable: true
})
log(Object.getOwnPropertyDescriptor(person,"age")); //Object {value: 18, writable: false, enumerable: false, configurable: true}
log(Object.getOwnPropertyNames(person)) // ['name', 'age']
for (prop in person) {
//因为 age 的 enumerable 为 false ,所以这里不会打印出 age
log(prop + ":" + person[prop]);// name:peter
}
//将 person 的 age 属性改为可修改
Object.defineProperty(person, "age", {
writable: true
});
person.age = 21;
log(person.age); //21

这个例子中,我们定义了 person 对象,然后使用花括号定义了 name 属性,并使用 defineProperty 方法定义了 age 属性。使用Object.getOwnPropertyDescriptor 可以看出,使用花括号定义的属性默认 Value Writable Enumerable Configurable 都为 true ,而使用 Object 的 defineProperty 方法定义的属性,如果没有明确声明,那么 Value Writable Enumerable Configurable 都默认为 false 。 Writable 为 false 时不能修改属性的值;Enumerable 为 false 时, for-in 循环遍历不到此属性,但是,使用 Object. getOwnPropertyNames 方法仍然可以获取;Configurable 属性为 false 时不能使用 defineProperty 方法修改属性的特性。
Writable 为 false 而 Configurable 为 true 时,我们还可以使用 defineProperty方法修改属性的值,但是 Configurable 为 false 的时候就不可以修改了,例如下面的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var obj = {};
Object.defineProperty(obj, " name", {
value: " 乔峰",
configurable: true
});
obj.name = "萧峰"
console.log(obj.name)//萧峰
Object.defineProperty(obj, "name", {
value: "萧峰"
});
console.log(obj.name); //萧峰
Object.defineProperty(obj, "name", {
configurable: false
});
Object.defineProperty(obj, "name", {
value: "乔峰"
}); //抛出异常

这个例子中,使用 defineProperty 方法添加的 name 属性因为默认 Writable 为 false, 所以不能直接修改它的值,但是因为 Configurable 为 true ,所以可以使用 defineProperty 方法通过 Value 特性来修改。当我们使用 defineProperty方法将 Configurable 设置为 false 的时候,如果再使用defineProperty方法就会抛出异常。另外,当 Configurable 为 false 的时候,属性也不可以使用 delete 删除。
可以使用 propertyIsEnumerable 方法检查 enumerable 特性。因为这个方法是 Object. prototype 中的一个,所以一般对象都可以直接调用( create 创建的 prototype 为 null 的对象除外),例如下面的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var 圣人 = {
姓名: "孔子"
}
圣人.代表作 = "论语";
Object.defineProperty(圣人, "年龄", {
value: 888,
enumerable: true
});
Object.defineProperty(圣人, "国籍", {
value: "中国",
enumerable: false
});
Object.defineProperty(圣人, "语言", {
value: "汉语"
});
console.log(圣人);
console.log(圣人.propertyIsEnumerable("姓名")); //true
console.log(圣人.propertyIsEnumerable("代表作")); //true
console.log(圣人.propertyIsEnumerable("年龄")); //true
console.log(圣人.propertyIsEnumerable("国籍"));//false
console.log(圣人.propertyIsEnumerable("语言")); //false

从这个例子可以看出,使用花括号和点操作符创建的属性的 enumerable 特性默认为 true ,使用 defineProperty 方法创建的属性,如果没有明确声明,那么enumerable默认为 false 。其他两个属性ConfigurableWritable的默认值也是这样的。另外,这个例子中的对象名和属性名都使用了中文,对于现在的浏览器来说一般都是支持的,而且现在很多 C++、 Java 编译器也支持中文变量名。但是,因为 JS 是直接将源代码发送到客户端的浏览器中运行的,而我们并不能保证所有客户端的浏览器都可以支持中文变量名,所以在 JS 中最好还是使用英文的变量名。如果是 C ++或者 Java 等编译型的语言就无所谓了,因为它们是将编译后的结果发给用户使用的。

命名访问器属性的 4 个特性

命名访问器属性因为没有值,所以没有 Value 特性,同时也就没有 Writable 特性,但它比命名数据属性多了 GetSet 特性,它们分别代表访问器属性的 getter 和 setter 方法。因此,命名访问器属性也有 4 个特性:GetSetEnumerableConfigurable 。其中,后两个特性和命名数据属性的含义是相同的,主要介绍它的前两个特性。下面看个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function log(msg) {
console.log(msg);
}
var person = {
_name: "peter"
}
Object.defineProperty(person, "name", {
get: function() {
log("getting name");
return this._name;
},
set: function(newNarne) {
log("name is changed to" + newNarne);
this._name = newNarne;
}
})
log(Object.getOwnPropertyDescriptor(person, "name"));
person.name = "lucy";
log(person.name);

在这个例子中,使用 Object 的 defineProperty 方法给 person 对象添加了 name 访问器属性,其值保存在 name 命名数据属性中,当我们获取 name 的值或者给 name 设置新值的时候就会调用相应的 getter 和 setter 方法。我们可以使用 Object 的 getOwnPropertyDescriptor 方法来获取 name 属性的所有特性。另外,我们也可以在 function 中使用 Object 的 defineProperty 方法给其创建的对象实例添加属性,这时只要将对象写为 this 即可,而且这种方式还可以使用 function 的内部变量。例如,我们将上个例子中的 person 对象改为由 function 类型的 Person 来创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function log(msg) {
console.log(msg);
}
function Person() {
var name = "peter";
Object.defineProperty(this, "name", {
get: function() {
log("getting name");
return name;
},
set: function(newNarne) {
log("name is changed to " + newNarne);
name = newNarne
}
})
}
var person = new Person();
log(Object.getOwnPropertyDescriptor(person, "name"));//{enumerable: false, configurable: false, get: ƒ, set: ƒ}
person.name = "lucy"; //name is changed to lucy
log(person.name);//getting name, lucy

这个例子就在 function 中使用 defineProperty 方法创建了名为 name 的访问器属性,并在其中定义了getter 和 setter ,即GetSet 特性 。 在这个例子中,我们将它的值保存到 Person 的局部变量 name 中,这样就可以屏蔽通过实例对象直接调用访问器属性的值 。

04 直接量及其相关对象

04 直接量及其相关对象

直接量是指不需要创建对象就可以直接使用的变量 。 ES 中的直接量主要有三种类型: 表示字符串的 string 类型 、 表示数字的 number 类型和表示 true/false 的 boolean 类型 。 对于直接量,在使用时直接将值赋给变量就可以了,例如下面的代码 。

1
2
3
4
5
6
7
8
9
10
var str ="hello word"; 
console.log(typeof str};
var num = 210;
console.log(typeof num};
var numl = 325 . 7;
console.log(typeof numl);
var flag = false;
console.log(typeof flag};
flag = 376;
console.log(typeof flag);

当我们直接将值赋给变量后, ES 就会自动判断其类型,而且当参数值发生变化后(例如此例中的 flag ),其类型也会自动跟着发生变化,即 ES 是一种弱类型的语言。另外,对于数字类型来说,无论是整数还是小数都是 number 类型。

4.1 直接量的保存方式

之前在内存模型中介绍过,直接量直接使用两块内存来保存它们的名值对,而不像对象类型那样需要 3 块内存。明白了这一点我们就可以知道,直接量是各自保存各自的值,它们不会相互影响,例如下面的例子。

1
2
3
4
var m = 5;
var n = m;
m = 7;
console.log(n,m); //5 ,7

这个例子中,虽然将 m 赋值给 n ,但只是将 m 的值赋给 n ,当 m 发生变化时, n 并没有发生变化,这一点和对象类型是不同的。如果是对象类型,那么赋值的时候是将对象的地址赋给新值,当对象中的属性发生变化时两个对象都会发生变化,例如下面的例子。

1
2
3
4
var obj = {m:5}; 
var newObj = obj;
obj.m = 7;
console.log(newObj.m) // 7

在这个例子中, obj 和 newObj 使用的是同一个对象,当 obj 中的 m 属性发生变化时, newObj 中的 m 属性也会发生变化。

4.2 直接量的封包与解包

直接量是单个值,并不是对象,当然也就没有属性。但是在 ES 中,我们可以使用直接量来调用属性方法,例如下面的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function log(msg) {
console.log(msg);
}
var s = "hello";
log(s.toUpperCase()); //HELLO
log(s.substr(3, s.length)); //lo
var n = 325.764;
log(n.toPrecision(5)); //325.76
log(n.toExponential(5)); //3.25764e+2
n = 7596389;
log(n.toLocaleString()); //7,596,389
log(n.toLocaleString("zh-Hans-CN-u-nu-hanidec")); //七,五九六,三八九
var b = true;
log(b === "true"); //false
log(b.toString() === "true"); //true
log(b.toString() === true); //false

这个例子中,由 String 类型的变量 s 调用了 toUpperCase 、 substr 和 length 属性,分别用于将 s 的值变为大写、截取 s 的一部分及获取 s 的长度; number 类型的变量 n 调用了 toPrecision , toExponential 和 toLocaleString 方法,分别用于设置 n 的精度、将 n 转换为科学计数法,以及将 n 转换为本地数组表达格式; boolean 类型的 b 属性调用了 toString 方法,用于将 boolean 转换为 string 类型 。 既然直接量只是一个值而不是对象,那么它怎么可以调用属性方法呢?原来自有一种叫作自动封包/解包的功能 。 封包/解包对于熟悉 Java 的读者来说一定不会陌生(在 Java 中也称装箱/拆箱,它们的含义都一样),其作用是在程序执行过程中按照实际需要自动在直接量和其所对应的对象类型之间进行转化 。 将直接量转换为对应的对象进行处理叫作封包,反过来,将对象转换为直接量叫作解包 。 封包和解包都是 JS 引擎自动完成的,而且只是为了完成程序的执行而进行的暂时转换,并不会实际修改变量的类型。有了封包/解包我们就不需要考虑什么时候使用直接量什么时候使用对象了,而且也不需要担心变量类型会发生变化 。上面的例子就使用了封包功能,下面我们再来看一个使用到解包功能的例子 。

1
2
3
4
5
6
7
var m = new Number(5);
var n = m + 2;//m会自动解包为直接量后再计算
console.log(n); //7
console.log(typeof m);//object
console.log(typeof n);//number
console.log(m instanceof Number);//true
console.log(n instanceof Number);//false

这个例子中,定义了对象类型的 m 变量,当对其进行加法计算时 m 会自动解包为直接量再进行计算,但是计算之后 m 的类型并不会变化,还是 object 类型。实际使用中我们很少直接使用直接量所对应的包装对象,所以封包功能使用得非常多,但是解包功能相对使用得就比较少了。
js类型转换

4.3 直接量的包装对象

直接量所对应的对象叫作包装对象, string 、 number 、 boolean 所对应的包装对象分别是 String 对象、 Number 对象和 Boolean 对象,它们都是 function 类型的对象。本节我们就来学习这三个对象。一个对象最重要的就是它所包含的属性,而 function 对象的属性又分为两大类,一类是它自身的属性,另一类是它创建的 object 类型实例对象的属性,创建的实例对象的属性又分为实例自己的属性和 function 的 prototype 的属性。学习 function 类型对象最重要的是学习两个方面的内容: function 作为函数的功能和它对应的属性。对于包装类型的对象来说,作为函数使用时的功能都是将传入的参数转换为 function 所对应的直接量,例如,使用如 String("abc") 可以新建值为 abc 的字符串类型的直接量等,其实和不使用函数的效果是一样的,所以学习包装类型对象主要是学习它所对应的属性。包装对象的属性和普通对象的属性没有什么区别,也是一共包括三部分: function 对象自身拥有的属性、创建的实例对象所拥有的属性和 function 的 prototype 属性对象中的属性。下面我们就从这三个方面分别学习这三个包装对象。

4.3.1 String 对象

String 对象是 function 类型的对象,对应的是字符串类型,可用来创建字符串类型的object 对象,例如, new String (” abc ”) ;就可以创建一个值为 abc 的字符串对象。最重要的还是它所对应的三种属性。

String 自身的属性

String 类型自身只有两个属性,一个是 prototype ,另一个是 fromCharCode 。对于 prototype 我们就不再解释了,企 omCharCode 方法的作用是创建由 Unicode 值所对应的字符组成的字符串,需要一个或多个参数,例如下面的例子。 vars= String . fromCharCode(97 , 98 , 99); console.log(s) ; // abc 在这个例子中,因为 97 、 98 、 99 所对应的 Unicode 值分别为 a 、 b 、 c ,所以创建出来的字符串 s 就是 abc 。

  • String.prototype.constructor:返回创建该字符串实例的函数,即 String
  • String.prototype.length:返回字符串的长度,即字符串中的字符数。

String.prototype 中的属性

在 JavaScript 中,String.prototype 是一个对象,它包含所有字符串实例共享的属性和方法。以下是一些常见的 String.prototype 中的属性和方法:

  • String.prototype.charAt(index):返回指定索引处的字符。
    1
    'hello'.charAt(1); // 'e'
  • String.prototype.charCodeAt(index):返回指定索引处字符的 Unicode 编码。
    1
    'hello'.charCodeAt(1); // 101
  • String.prototype.codePointAt(pos):返回指定索引处字符的 Unicode 编码点。
    1
    'a'.codePointAt(0); // 97
  • String.prototype.concat(str1, ..., strN):将一个或多个字符串连接到当前字符串的末尾,并返回新的字符串。
    1
    'hello'.concat(' ', 'world'); // 'hello world'
  • String.prototype.endsWith(searchString, length):判断当前字符串是否以指定的子字符串结束。
    1
    'hello world'.endsWith('world'); // true
  • String.prototype.includes(searchString, position):判断当前字符串是否包含指定的子字符串。
    1
    'hello world'.includes('world'); // true
  • String.prototype.indexOf(searchValue, fromIndex):返回指定值首次出现的索引,如果不存在则返回 -1。
    1
    'hello world'.indexOf('world'); // 6
  • String.prototype.lastIndexOf(searchValue, fromIndex):返回指定值最后一次出现的索引,如果不存在则返回 -1。
    1
    'hello world'.lastIndexOf('l'); // 9
  • String.prototype.localeCompare(that):根据本地环境的排序规则,比较两个字符串。
    1
    'a'.localeCompare('b'); // -1
  • String.prototype.match(regexp):对字符串执行匹配搜索,并将匹配的结果作为数组返回。
    1
    'hello world'.match(/world/); // ['world', index: 6, input: 'hello world', groups: undefined]
  • String.prototype.matchAll(regexp):返回一个包含所有成功匹配结果的迭代器。
    1
    2
    3
    4
    let matches = 'hello world'.matchAll(/l/g);
    for (let match of matches) {
    console.log(match); // ['l', index: 2, input: 'hello world', groups: undefined]
    }
  • String.prototype.normalize(form):返回调用字符串的 Unicode 正规形式。
    1
    'a\u0301'.normalize(); // 'á'
  • String.prototype.padEnd(targetLength, padString):将当前字符串填充到指定长度。
    1
    'hello'.padEnd(10, ' '); // 'hello     '
  • String.prototype.padStart(targetLength, padString):将当前字符串从开头填充到指定长度。
    1
    'hello'.padStart(10, ' '); // '     hello'
  • String.prototype.repeat(count):返回一个新字符串,表示将当前字符串重复指定次数。
    1
    'hello'.repeat(2); // 'hellohello'
  • String.prototype.replace(searchValue, replaceValue):在字符串中查找匹配项,然后使用新的子字符串替换匹配项。
    1
    'hello world'.replace('world', 'everyone'); // 'hello everyone'
  • String.prototype.search(regexp):执行匹配搜索,返回匹配项的索引,如果未找到则返回 -1。
    1
    'hello world'.search(/world/); // 6
  • String.prototype.slice(beginIndex, endIndex):提取字符串的某个部分,并返回一个新字符串。
    1
    'hello world'.slice(6, 11); // 'world'
  • String.prototype.split(separator, limit):使用指定的分隔符字符串将一个 String 对象分割成子字符串数组。
    1
    'hello world'.split(' '); // ['hello', 'world']
  • String.prototype.startsWith(searchString, position):判断当前字符串是否以指定的子字符串开头。
    1
    'hello world'.startsWith('hello'); // true
  • String.prototype.substring(start, end):返回一个字符串在开始索引到结束索引之间的一个子集。
    1
    'hello world'.substring(6, 11); // 'world'
  • String.prototype.toLowerCase():将字符串转换为小写。
    1
    'HELLO WORLD'.toLowerCase(); // 'hello world'
  • String.prototype.toLocaleLowerCase(locales):根据本地环境将字符串转换为小写。
    1
    'HELLO WORLD'.toLocaleLowerCase(); // 'hello world'
  • String.prototype.toUpperCase():将字符串转换为大写。
    1
    'hello world'.toUpperCase(); // 'HELLO WORLD'
  • String.prototype.toLocaleUpperCase(locales):根据本地环境将字符串转换为大写。
    1
    'hello world'.toLocaleUpperCase(); // 'HELLO WORLD'
  • String.prototype.trim():去除字符串两端的空白字符。
    1
    '   hello world   '.trim(); // 'hello world'
  • String.prototype.trimStart():去除字符串开头的空白字符。
    1
    '   hello world'.trimStart(); // 'hello world'
  • String.prototype.trimEnd():去除字符串末尾的空白字符。
    1
    'hello world   '.trimEnd(); // 'hello world'
  • String.prototype.valueOf():返回字符串的原始值。
    1
    'hello world'.valueOf(); // 'hello world'
    这些方法和属性为字符串的处理和操作提供了强大的支持,使得字符串的处理更加灵活和高效。

String 创建的对象实例的属性

String 创建的实例对象一共有两个属性,一个是 length 属性,它代表字符串的长度;另外一个属性类似于数组,属性名为 0 到 length - 1,属性值为序号所对应的字符,例如下面的例子。

1
2
3
var s = "www.excelib.com";
console.log(s.length); //15
console.log(s[7]); //e

使用第二个属性我们就可以把字符串当作字符数组来使用了 。

4.3.2 Number 对象

Number 对象是 function 类型的对象,对应的是数字类型,可用来创建数字类型的。同 ect 对象,例如, new Number(123)就可以创建一个值为 123 的数字实例对象。最重要的依然是它所对应的三种属性。

Number 自身的属性

JavaScript 中的 Number 对象提供了许多有用的属性和方法。这些属性和方法可以帮助你进行各种数值操作和检查。以下是一些 Number 对象自身的属性:

  1. Number.MAX_VALUE
    • 表示 JavaScript 中最大的正数,大约是 1.7976931348623157e+308
    • 用于检查数值是否超过了 JavaScript 能表示的最大范围。
    1
    console.log(Number.MAX_VALUE); // 1.7976931348623157e+308
  2. Number.MIN_VALUE
    • 表示 JavaScript 中最小的正数,大约是 5e-324
    • 用于检查数值是否接近 JavaScript 能表示的最小正数。
    1
    console.log(Number.MIN_VALUE); // 5e-324
  3. Number.MAX_SAFE_INTEGER
    • 表示 JavaScript 中最大的安全整数,即 2^53 - 1,等于 9007199254740991
    • 安全整数是指在这个范围内,整数的表示是精确的。
    1
    console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
  4. Number.MIN_SAFE_INTEGER
    • 表示 JavaScript 中最小的安全整数,即 -2^53 + 1,等于 -9007199254740991
    • Number.MAX_SAFE_INTEGER 一起,用于检查整数是否在安全范围内。
    1
    console.log(Number.MIN_SAFE_INTEGER); // -9007199254740991
  5. Number.POSITIVE_INFINITY
    • 表示正无穷大。
    • 用于表示一个数值超过了 Number.MAX_VALUE
    1
    console.log(Number.POSITIVE_INFINITY); // Infinity
  6. Number.NEGATIVE_INFINITY
    • 表示负无穷大。
    • 用于表示一个数值小于 Number.MIN_VALUE 且为负数。
    1
    console.log(Number.NEGATIVE_INFINITY); // -Infinity
  7. Number.NaN
    • 表示“非数字”值。
    • 用于表示一个数值是无效的或不是数字。
    1
    console.log(Number.NaN); // NaN

    以下是一些使用这些属性的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 检查数值是否在安全整数范围内
function isSafeInteger(num) {
return num >= Number.MIN_SAFE_INTEGER && num <= Number.MAX_SAFE_INTEGER;
}
console.log(isSafeInteger(9007199254740991)); // true
console.log(isSafeInteger(9007199254740992)); // false
// 检查数值是否为无穷大
function isFiniteNumber(num) {
return num !== Number.POSITIVE_INFINITY && num !== Number.NEGATIVE_INFINITY;
}
console.log(isFiniteNumber(1.7976931348623158e+308)); // false
console.log(isFiniteNumber(1.7976931348623157e+308)); // true
// 检查数值是否为 NaN
function isNaNNumber(num) {
return num === Number.NaN;
}
console.log(isNaNNumber(Number.NaN)); // false(因为 NaN 不等于任何值,包括它自己)
console.log(isNaN(Number.NaN)); // true(使用 isNaN 函数检查)

这些属性在处理数值时非常有用,可以帮助你确保数值在合理的范围内,避免一些常见的数值问题。

Number.prototype 的属性

Number.prototype 是一个对象,它包含所有数字实例共享的属性和方法。以下是一些常见的 Number.prototype 中的属性和方法:

属性
  • Number.prototype.constructor:返回创建该数字实例的函数,即 Number
    1
    console.log((123).constructor === Number); // true
方法
  • Number.prototype.toExponential(fractionDigits):将数字转换为指数表示法。
    1
    2
    console.log((123).toExponential()); // "1.23e+2"
    console.log((123).toExponential(1)); // "1.2e+2"
  • Number.prototype.toFixed(fractionDigits):将数字格式化为指定小数位数的字符串。
    1
    2
    console.log((123.456).toFixed(2)); // "123.46"
    console.log((123.456).toFixed(0)); // "123"
  • Number.prototype.toPrecision(precision):将数字格式化为指定精度的字符串。
    1
    2
    console.log((123.456).toPrecision(5)); // "123.46"
    console.log((123.456).toPrecision(2)); // "1.2e+2"
  • Number.prototype.toString(radix):将数字转换为字符串,可以指定进制。
    1
    2
    3
    console.log((123).toString()); // "123"
    console.log((123).toString(16)); // "7b"
    console.log((123).toString(2)); // "1111011"
  • Number.prototype.valueOf():返回数字的原始值。
    1
    console.log((123).valueOf()); // 123
静态属性
  • Number.EPSILON:表示 1 与大于 1 的最小浮点数之间的差异,用于精度比较。
    1
    console.log(Number.EPSILON); // 2.220446049250313e-16
  • Number.MAX_SAFE_INTEGER:表示 JavaScript 中最大的安全整数(2^53 - 1)。
    1
    console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
  • Number.MIN_SAFE_INTEGER:表示 JavaScript 中最小的安全整数(-(2^53 - 1))。
    1
    console.log(Number.MIN_SAFE_INTEGER); // -9007199254740991
  • Number.MAX_VALUE:表示 JavaScript 中最大的数值。
    1
    console.log(Number.MAX_VALUE); // 1.7976931348623157e+308
  • Number.MIN_VALUE:表示 JavaScript 中最小的正值。
    1
    console.log(Number.MIN_VALUE); // 5e-324
  • Number.NaN:表示“非数字”值。
    1
    console.log(Number.NaN); // NaN
  • Number.NEGATIVE_INFINITY:表示负无穷大。
    1
    console.log(Number.NEGATIVE_INFINITY); // -Infinity
  • Number.POSITIVE_INFINITY:表示正无穷大。
    1
    console.log(Number.POSITIVE_INFINITY); // Infinity
静态方法
  • Number.isFinite(value):判断一个值是否为有限数。
    1
    2
    console.log(Number.isFinite(123)); // true
    console.log(Number.isFinite(Infinity)); // false
  • Number.isInteger(value):判断一个值是否为整数。
    1
    2
    console.log(Number.isInteger(123)); // true
    console.log(Number.isInteger(123.456)); // false
  • Number.isNaN(value):判断一个值是否为 NaN。
    1
    2
    console.log(Number.isNaN(NaN)); // true
    console.log(Number.isNaN(123)); // false
  • Number.isSafeInteger(value):判断一个值是否为安全整数。
    1
    2
    console.log(Number.isSafeInteger(123)); // true
    console.log(Number.isSafeInteger(9007199254740992)); // false
  • Number.parseFloat(value):将一个值解析为浮点数。
    1
    2
    console.log(Number.parseFloat('123.456')); // 123.456
    console.log(Number.parseFloat('abc')); // NaN
  • Number.parseInt(value):将一个值解析为整数。
    1
    2
    console.log(Number.parseInt('123.456')); // 123
    console.log(Number.parseInt('abc')); // NaN
    这些属性和方法为数字的处理和操作提供了强大的支持,使得数字的处理更加灵活和高效。

Number 创建的实例对象的属性

Number 创建的实例对象没有自己的命名属性。

4.3.3 Boolean 对象

Boolean 对象是 function 类型的对象,对应的是布尔类型,可用来创建布尔类型的。同 ect 实例对象。例如, new ‘ Boolean (true )就可以创建一个值为 true 的布尔类型实例对象。 Boolean 对象非常简单。

Boolean 自身的属性

Boolean 作为对象时自身只有一个 prototype 属性 。 prototype 我们已经非常熟悉了,这里 就不再重述了 。

Boolean. prototype 的属性

Boolean 的 prototype 一共有三个属性: constructor 、 to String 和 valueOf 。 constructor 指向 Boolean 本身, to String 和 valueOf 都返回实例对象的值,但它们的类型不一样, to String 返回 s位ing 类型,而 valueOf 返回 boolean 类型。下面来看个例子。

1
2
3
4
5
6
var b = new Boolean(true);
console.log(b.toString()); //true
console.log(b.valueOf());//true
console.log(typeof b.toString());//string
console.log(typeof b.valueOf());//boolean
console.log(typeof b);//object

Boolean 创建的实例对象的属性

Boolean 创建的对象实例自身不包含命名属性。

如何在浏览器申查看对象的属性

本节介绍的包装对象中的属性主要是标准中规定的属性,但不同的浏览器除了实现标准中的属性外,还可能会添加自己特有的属性,我们可以使用 Object 的 getOwnPropertyNames 方法来获取当前浏览器中对象自身的所有属性。对于 function 类型的对象,我们需要获取三种类型的属性:

  • function 自身的属性
  • function.prototype 包含的属性
  • 使用 function 创建的实例对象自身所包含的属性
    这三种属性都可以通过Object 的 getOwnPropertyNames 方法来获取。例如,可以通过下面的代码来获取 String 象的三种类型的属性。
1
2
3
4
5
6
console.log(Object.getOwnPropertyNames(String))
//获取 String 对象自身的属性
console.log(Object.getOwnPropertyNames(String.prototype))
//获取 String 的 prototype 的属性
console.log(Object.getOwnPropertyNames(new String()))
//获取 String 创建的对象实例的属性

!Pasted image 20250114181139.png
通过上述方法可以获取当前浏览器中某个具体 function 对象的相关属性 。 对于所有 function 类型的对象都可以使用这个方法 , 包括 Object 对象和浏览器中的 Window 对象 。 注意 Window 对象不可以用来创建新的实 例对象 。

05 点运算符与 this 关键字

05 点运算符与 This 关键字

点运算符

点运算符可用来操作对象的属性。这里的操作可以分为获取和赋值两种类型。在赋值的情况下,如果对象原来没有所操作的属性则会添加,如果有则会修改其值,例如下面的例子。

1
2
3
4
5
6
7
var person = {
name: "maker"
};
person.age = 15; //添加age属性
console.log(person.name);//获得name属性
person.name = "peter";//修改name属性
console.log(person);//{name: 'peter', age: 15}

this

在 JavaScript 中,this 是一个关键字,用于指向函数的调用上下文。this 的值在函数被调用时确定,而不是在函数被定义时确定。以下是 this 的几种常见用法和规则:

谁直接调用方法,this就指向谁

1. 在全局上下文中

  • 在全局作用域中
    • 在非严格模式下,this 指向全局对象(在浏览器中是 window,在 Node.js 中是 global)。
    • 在严格模式下,thisundefined
    1
    console.log(this); // 在浏览器中输出 window

2. 在函数调用中

  • 普通函数调用
    • 在非严格模式下,this 指向全局对象。
    • 在严格模式下,thisundefined
    1
    2
    3
    4
    function foo() {
    console.log(this);
    }
    foo(); // 在非严格模式下输出 window,在严格模式下输出 undefined
  • 作为对象的方法调用
    • this 指向调用该方法的对象。
    1
    2
    3
    4
    5
    6
    7
    const obj = {
    name: 'Kimi',
    sayName: function() {
    console.log(this.name);
    }
    };
    obj.sayName(); // 输出 'Kimi'

3. 在构造函数中

  • 作为构造函数调用
    • this 指向新创建的对象。
    1
    2
    3
    4
    5
    function Person(name) {
    this.name = name;
    }
    const person = new Person('Kimi');
    console.log(person.name); // 输出 'Kimi'

4. 在箭头函数中

  • 箭头函数没有自己的 this
    • 箭头函数中的 this 是从其定义时的上下文中捕获的。
    • 通常指向定义箭头函数时的 this 值。
    1
    2
    3
    4
    5
    6
    7
    const obj = {
    name: 'Kimi',
    sayName: () => {
    console.log(this.name); // 输出 'undefined',因为箭头函数捕获了全局上下文的 this
    }
    };
    obj.sayName();

5. 使用 callapplybind 方法

  • call 方法
    • 显式地设置 this 的值,并调用函数。
    1
    2
    3
    4
    5
    function greet() {
    console.log(`Hello, ${this.name}!`);
    }
    const user = { name: 'Kimi' };
    greet.call(user); // 输出 'Hello, Kimi!'
  • apply 方法
    • 类似于 call,但接受一个参数数组。
    1
    2
    3
    4
    function sum(a, b) {
    console.log(a + b);
    }
    sum.apply(null, [1, 2]); // 输出 3
  • bind 方法
    • 创建一个新函数,其 this 值被永久绑定到指定的对象。
    1
    2
    3
    4
    5
    6
    function greet() {
    console.log(`Hello, ${this.name}!`);
    }
    const user = { name: 'Kimi' };
    const boundGreet = greet.bind(user);
    boundGreet(); // 输出 'Hello, Kimi!'

注意事项

  • this 的值是动态的:它在函数被调用时确定,而不是在函数定义时确定。
  • 箭头函数不绑定 this:箭头函数中的 this 是从其定义时的上下文中捕获的,而不是在调用时确定的.
  • 使用 callapplybind 可以显式地控制 this 的值,这在某些情况下非常有用,例如在回调函数中保持上下文.
    理解 this 的行为对于编写可维护和可靠的 JavaScript 代码非常重要,特别是在面向对象编程和事件处理中.

call和apply的区别

callapply 都是 JavaScript 中用于调用函数的方法,它们的主要功能是显式地设置函数的 this 值。尽管它们的目的相似,但在调用函数时传递参数的方式上有所不同:

call 方法

  • 语法
    1
    func.call(thisArg, arg1, arg2, ...);
  • 功能
    • call 方法调用一个函数,并显式地设置函数的 this 值为 thisArg
    • 可以传递多个参数给函数,参数是按顺序传递的。
  • 示例
    1
    2
    3
    4
    5
    function greet(lang, punctuation) {
    console.log(`Hello ${this.name} in ${lang}${punctuation}`);
    }
    const user = { name: 'Kimi' };
    greet.call(user, 'English', '!'); // 输出 'Hello Kimi in English!'

apply 方法

  • 语法
    1
    func.apply(thisArg, [argsArray]);
  • 功能
    • apply 方法调用一个函数,并显式地设置函数的 this 值为 thisArg
    • 参数以数组的形式传递给函数。
  • 示例
    1
    2
    3
    4
    5
    function greet(lang, punctuation) {
    console.log(`Hello ${this.name} in ${lang}${punctuation}`);
    }
    const user = { name: 'Kimi' };
    greet.apply(user, ['English', '!']); // 输出 'Hello Kimi in English!'

区别

  • 参数传递方式
    • call 传递参数是按顺序传递的,每个参数都是单独列出的。
    • apply 传递参数是通过一个数组传递的,所有参数都在一个数组中。
  • 适用场景
    • 当参数数量固定且已知时,call 更为方便。
    • 当参数数量不确定或参数已经以数组形式存在时,apply 更为合适。

其他用途

  • 函数继承
    • 可以使用 callapply 来实现构造函数的继承。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function Parent(name) {
    this.name = name;
    }
    function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
    }
    const child = new Child('Kimi', 25);
    console.log(child.name); // 输出 'Kimi'
    console.log(child.age); // 输出 25
  • Math 函数调用
    • apply 可以用于调用 Math 对象的方法,特别是当参数是数组时。
    1
    2
    3
    const numbers = [1, 2, 3, 4, 5];
    const max = Math.max.apply(null, numbers);
    console.log(max); // 输出 5

    总之,callapply 在功能上是相似的,主要区别在于参数的传递方式,选择使用哪一个通常取决于具体的使用场景和参数的形式.

箭头函数的this

箭头函数(Arrow Functions)是 ES6 引入的一种新的函数语法,其特点是简洁的语法和对 this 的特殊处理。以下是关于箭头函数中 this 的一些关键点:

箭头函数的 this 特性

  • 词法作用域
    • 箭头函数不绑定自己的 this,而是捕获其所在上下文的 this 值作为自身的 this 值。这意味着箭头函数中的 this 是在其定义时确定的,而不是在调用时确定的。
  • 不能用作构造函数
    • 箭头函数不能用作构造函数,因此不能使用 new 关键字来创建实例。由于没有自己的 this,箭头函数也无法通过 callapplybind 方法来改变 this 的值.

示例

假设有一个对象 user,其中包含一个普通函数和一个箭头函数:

1
2
3
4
5
6
7
8
9
10
11
const user = {
name: 'Kimi',
regularFunction: function() {
console.log(this.name); // 输出 'Kimi'
},
arrowFunction: () => {
console.log(this.name); // 输出 'undefined',因为箭头函数捕获了全局上下文的 this
}
};
user.regularFunction(); // 输出 'Kimi'
user.arrowFunction(); // 输出 'undefined'

箭头函数的 this 应用场景

  • 回调函数
    • 在使用回调函数时,箭头函数可以保持 this 的上下文不变,避免了使用 bindself 变量来保存 this 的常见做法。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const timer = {
    seconds: 0,
    start() {
    setInterval(() => {
    this.seconds++;
    console.log(this.seconds);
    }, 1000);
    }
    };
    timer.start(); // 每秒输出递增的 seconds 值
  • 事件处理
    • 在事件处理中,箭头函数可以确保 this 指向预期的对象,而不需要额外的绑定操作。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const button = document.getElementById('myButton');
    const handler = {
    count: 0,
    onClick() {
    button.addEventListener('click', () => {
    this.count++;
    console.log(`Clicked ${this.count} times`);
    });
    }
    };
    handler.onClick();

注意事项

  • 全局上下文中使用箭头函数
    • 在全局上下文中,箭头函数的 this 指向全局对象(在浏览器中是 window,在 Node.js 中是 global)。
  • 模块作用域
    • 在 ES6 模块中,箭头函数的 this 指向模块的上下文,通常是 undefined(严格模式下)或全局对象(非严格模式下).
      通过理解箭头函数的 this 特性,可以更好地利用其简洁的语法和稳定的上下文绑定,编写更清晰和可靠的代码.

ds

在 JavaScript 中,this 是一个特殊的关键字,它的值取决于函数的调用方式,而不是定义的位置。它的指向是动态的,理解 this 的行为是掌握 JavaScript 的重要部分。以下是 this 在不同场景下的详细说明:

一、默认绑定(全局环境)

当函数在全局作用域中被调用时,this 默认指向全局对象:

  • 浏览器环境window(非严格模式)或 undefined(严格模式)。
  • Node.js 环境global(非严格模式)或 undefined(严格模式)。
1
2
3
4
function showThis() {
console.log(this);
}
showThis(); // 浏览器中输出 window(非严格模式)

严格模式下:

1
2
3
4
5
"use strict";
function showThis() {
console.log(this); // 输出 undefined
}
showThis();

二、隐式绑定(方法调用)

当函数作为对象的方法被调用时,this 指向调用该方法的对象

1
2
3
4
5
6
7
const obj = {
name: "Alice",
greet: function() {
console.log(this.name); // 输出 "Alice"
}
};
obj.greet(); // this 指向 obj

三、显式绑定(call、apply、bind)

通过 call()apply()bind() 可以强制指定 this 的值:

  1. callapply:立即调用函数,手动绑定 this
    1
    2
    3
    4
    5
    function sayHello() {
    console.log(`Hello, ${this.name}`);
    }
    const user = { name: "Bob" };
    sayHello.call(user); // 输出 "Hello, Bob"
  2. bind:返回一个新函数,永久绑定 this
    1
    2
    const boundFunc = sayHello.bind(user);
    boundFunc(); // 输出 "Hello, Bob"

四、构造函数中的 this

当使用 new 关键字调用构造函数时,this 指向新创建的实例对象:

1
2
3
4
5
function Person(name) {
this.name = name;
}
const alice = new Person("Alice");
console.log(alice.name); // 输出 "Alice"

五、箭头函数的 this

箭头函数没有自己的 this,它的 this 继承自外层作用域(词法作用域):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const obj = {
name: "Alice",
greet: () => {
console.log(this.name); // 输出 undefined(外层可能是全局)
}
};
obj.greet();
// 更典型的用法:保留外层 this
function Timer() {
this.seconds = 0;
setInterval(() => {
this.seconds++; // 正确指向 Timer 实例
}, 1000);
}

六、事件处理函数中的 this

在 DOM 事件处理函数中,this 指向触发事件的元素:

1
2
3
button.addEventListener("click", function() {
console.log(this); // 输出 button 元素
});

七、回调函数中的 this

回调函数的 this 可能丢失原绑定,常见于 setTimeoutsetInterval 或高阶函数:

1
2
3
4
5
6
7
8
9
10
11
12
const obj = {
name: "Alice",
greet: function() {
setTimeout(function() {
console.log(this.name); // 输出 undefined(this 指向 window)
}, 100);
}
};
// 解决方法:使用箭头函数或 bind
setTimeout(() => { ... }, 100);
// 或
setTimeout(this.greet.bind(this), 100);

八、类中的 this

在 ES6 类中,方法中的 this 默认指向实例,但方法被单独调用时可能丢失绑定:

1
2
3
4
5
6
7
8
9
10
11
class User {
constructor(name) {
this.name = name;
}
greet() {
console.log(this.name);
}
}
const user = new User("Bob");
const greet = user.greet;
greet(); // 报错:此时 this 为 undefined(严格模式)

九、其他注意事项

  1. 严格模式:在严格模式下,未绑定的 thisundefined
  2. 模块化环境:在 ES6 模块中,顶层的 thisundefined(而非 window)。

总结

this 的指向规则可以简化为:

  1. 谁调用,this 指向谁(隐式绑定)。
  2. 箭头函数继承外层 this
  3. newcall/apply/bind 可显式设置 this
    理解 this 的关键在于分析函数的调用方式,而非定义位置。在复杂场景中,推荐使用 bind 或箭头函数来明确绑定 this

06 类

06 类

    • 构造函数
    • 静态方法
    • 实例方法
    • 继承
    • super

class

在 JavaScript 中,class 是 ES6(ECMAScript 2015)引入的一个语法糖,用于更简洁地创建对象和实现面向对象的编程。尽管 JavaScript 本质上是基于原型继承的,class 语法提供了一种更接近传统面向对象语言的类的写法,使得代码更易于理解和维护。

基本语法

定义类

使用 class 关键字定义一个类,类的名称通常首字母大写,以区分普通变量和函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person {
// 构造函数,用于初始化类的实例
constructor(name, age) {
this.name = name;
this.age = age;
}
// 实例方法
greet() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
// 静态方法
static sayHello() {
console.log('Hello from Person class');
}
}

创建实例

使用 new 关键字创建类的实例。

1
2
3
4
const person1 = new Person('Kimi', 25);
person1.greet(); // Hello, my name is Kimi and I am 25 years old.
const person2 = new Person('Moonshot', 30);
person2.greet(); // Hello, my name is Moonshot and I am 30 years old.

调用静态方法

静态方法可以通过类名直接调用,不需要创建实例。

1
Person.sayHello(); // Hello from Person class

类的继承

ES6 的类支持继承,可以使用 extends 关键字继承一个父类,并使用 super 关键字调用父类的构造函数和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Animal {
constructor(name) {
this.name = name;
}
makeSound() {
console.log(`${this.name} makes a sound.`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // 调用父类的构造函数
this.breed = breed;
}
makeSound() {
console.log(`${this.name} barks.`);
}
static sayHello() {
console.log('Hello from Dog class');
}
}
const dog = new Dog('Buddy', 'Golden Retriever');
dog.makeSound(); // Buddy barks.
Dog.sayHello(); // Hello from Dog class

类的特性

  • 构造函数(Constructor):用于初始化类的实例。每个类只能有一个构造函数,如果有多个构造函数会抛出错误。
  • 实例方法:定义在类中的方法,可以通过类的实例调用。
  • 静态方法(Static Methods):定义在类上的方法,可以通过类名直接调用,不需要创建实例。
  • 继承(Inheritance):子类可以继承父类的属性和方法,并可以重写父类的方法。
  • super 关键字:用于调用父类的构造函数和方法,只能在子类的构造函数和方法中使用。

注意事项

  • 类的声明不会被提升,因此必须先声明类,然后才能使用它。
  • 类中的方法默认是不可枚举的,与对象字面量中的方法不同。
  • 类的静态方法不能通过实例调用,只能通过类名调用。

示例:一个完整的类和继承示例

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
28
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
static sayHello() {
console.log('Hello from Person class');
}
}
class Employee extends Person {
constructor(name, age, jobTitle) {
super(name, age); // 调用父类的构造函数
this.jobTitle = jobTitle;
}
greet() {
super.greet(); // 调用父类的 greet 方法
console.log(`I am a ${this.jobTitle}.`);
}
static sayHello() {
console.log('Hello from Employee class');
}
}
const employee = new Employee('Kimi', 25, 'Software Engineer');
employee.greet(); // Hello, my name is Kimi and I am 25 years old. I am a Software Engineer.
Employee.sayHello(); // Hello from Employee class

通过使用 class 语法,JavaScript 的面向对象编程变得更加简洁和直观,同时也提供了更强大的功能,如继承和静态方法等。

ts 和 es6 的 class 区别

TypeScript(TS)和 ES6(ECMAScript 2015)都引入了类(class)的概念,但它们在实现和功能上有一些关键区别。TypeScript 是 JavaScript 的一个超集,它在 ES6 的基础上添加了类型系统和其他一些特性。以下是 TypeScript 和 ES6 类的主要区别:

1. 类型系统

  • ES6:ES6 的类不支持类型注解,所有变量和方法都是动态类型的。
  • TypeScript:TypeScript 的类支持类型注解,可以在类的属性和方法参数上指定类型,这有助于在编译时进行类型检查,减少运行时错误。

示例

1
2
3
4
5
6
7
8
9
10
11
12
// ES6 类
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
}
const person = new Person('Kimi', 25);
person.greet(); // Hello, my name is Kimi and I am 25 years old.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// TypeScript 类
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greet(): void {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
}
const person = new Person('Kimi', 25);
person.greet(); // Hello, my name is Kimi and I am 25 years old.

2. 访问修饰符

  • ES6:ES6 的类不支持访问修饰符(如 publicprivateprotected)。
  • TypeScript:TypeScript 支持访问修饰符,可以明确指定类的属性和方法的访问权限。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
public name: string;
private age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
public greet(): void {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
private getAge(): number {
return this.age;
}
}
const person = new Person('Kimi', 25);
person.greet(); // Hello, my name is Kimi and I am 25 years old.
// person.getAge(); // 抛出错误:Property 'getAge' is private and only accessible within class 'Person'.

3. 静态类型检查

  • ES6:ES6 的类在运行时进行类型检查,没有编译时的类型检查。
  • TypeScript:TypeScript 在编译时进行类型检查,可以提前发现类型错误,减少运行时错误。

示例

1
2
3
4
5
6
7
8
9
10
11
12
// ES6 类
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
}
const person = new Person('Kimi', '25'); // 运行时不会报错,但可能会导致逻辑错误
person.greet(); // Hello, my name is Kimi and I am 25 years old.
1
2
3
4
5
6
7
8
9
10
11
12
13
// TypeScript 类
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greet(): void {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
}
const person = new Person('Kimi', '25'); // 编译时报错:Argument of type 'string' is not assignable to parameter of type 'number'.

4. 构造函数参数属性

  • ES6:ES6 的类不支持构造函数参数属性。
  • TypeScript:TypeScript 支持构造函数参数属性,可以在构造函数参数上使用访问修饰符,自动创建类的属性。

示例

1
2
3
4
5
6
7
8
9
class Person {
constructor(public name: string, private age: number) {
}
greet(): void {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
}
const person = new Person('Kimi', 25);
person.greet(); // Hello, my name is Kimi and I am 25 years old.

5. 类的继承

  • ES6:ES6 的类支持继承,但没有类型检查。
  • TypeScript:TypeScript 的类支持继承,并且在继承时进行类型检查,确保子类的类型兼容性。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ES6 类的继承
class Animal {
constructor(name) {
this.name = name;
}
makeSound() {
console.log(`${this.name} makes a sound.`);
}
}
class Dog extends Animal {
constructor(name) {
super(name);
}
makeSound() {
console.log(`${this.name} barks.`);
}
}
const dog = new Dog('Buddy');
dog.makeSound(); // Buddy barks.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// TypeScript 类的继承
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
makeSound(): void {
console.log(`${this.name} makes a sound.`);
}
}
class Dog extends Animal {
constructor(name: string) {
super(name);
}
makeSound(): void {
console.log(`${this.name} barks.`);
}
}
const dog = new Dog('Buddy');
dog.makeSound(); // Buddy barks.

6. 抽象类和接口

  • ES6:ES6 的类不支持抽象类和接口。
  • TypeScript:TypeScript 支持抽象类和接口,可以定义类的结构和方法签名,确保类的实现符合特定的规范。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 抽象类
abstract class Animal {
abstract makeSound(): void;
}
class Dog extends Animal {
makeSound(): void {
console.log('Bark');
}
}
// 接口
interface IAnimal {
makeSound(): void;
}
class Cat implements IAnimal {
makeSound(): void {
console.log('Meow');
}
}

总结

  • ES6:提供了类的基本功能,但没有类型系统和访问修饰符。
  • TypeScript:在 ES6 的基础上添加了类型系统、访问修饰符、构造函数参数属性、抽象类和接口等高级特性,提供了更强大的类型检查和代码组织能力。
    在实际开发中,如果你需要类型检查和更严格的代码规范,推荐使用 TypeScript。如果你的项目不需要类型检查,或者你更倾向于动态类型,可以使用 ES6 的类。

07 模块

07 模块

模块

在 JavaScript 中,模块化是一种将代码组织成独立、可重用的单元的方法,有助于提高代码的可维护性和可读性。ES6(ECMAScript 2015)引入了原生的模块支持,但在此之前,社区已经发展出了多种模块化模式,如 CommonJS 和 AMD。以下是 JavaScript 模块化的一些主要方式及其特点。

1. ES6 模块(ECMAScript 模块)

ES6 模块是 JavaScript 的官方模块系统,提供了 importexport 语法来导入和导出模块。

导出模块(export

  • 命名导出:可以导出多个值(变量、函数、类等)。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // mathUtils.js
    export const add = (a, b) => a + b;
    export const subtract = (a, b) => a - b;
    export class Calculator {
    constructor() {}
    add(a, b) {
    return a + b;
    }
    }
  • 默认导出:每个模块只能有一个默认导出。
    1
    2
    3
    4
    5
    6
    7
    // mainCalculator.js
    export default class MainCalculator {
    constructor() {}
    add(a, b) {
    return a + b;
    }
    }

导入模块(import

  • 导入命名导出:需要使用与导出时相同的名称。
    1
    2
    3
    4
    5
    6
    // app.js
    import { add, subtract, Calculator } from './mathUtils.js';
    console.log(add(2, 3)); // 5
    console.log(subtract(5, 2)); // 3
    const calc = new Calculator();
    console.log(calc.add(4, 5)); // 9
  • 导入默认导出:可以使用任意名称。
    1
    2
    3
    4
    // app.js
    import MainCalc from './mainCalculator.js';
    const mainCalc = new MainCalc();
    console.log(mainCalc.add(6, 7)); // 13

2. CommonJS 模块

CommonJS 模块系统主要用于 Node. js 环境,使用 requiremodule.exports 来导入和导出模块。

导出模块(module.exports

  • 导出单个值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // mathUtils.js
    const add = (a, b) => a + b;
    const subtract = (a, b) => a - b;
    class Calculator {
    constructor() {}
    add(a, b) {
    return a + b;
    }
    }
    module.exports = { add, subtract, Calculator };
  • 导出默认值
    1
    2
    3
    4
    5
    6
    7
    8
    // mainCalculator.js
    class MainCalculator {
    constructor() {}
    add(a, b) {
    return a + b;
    }
    }
    module.exports = MainCalculator;

导入模块(require

  • 导入模块
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // app.js
    const { add, subtract, Calculator } = require('./mathUtils.js');
    console.log(add(2, 3)); // 5
    console.log(subtract(5, 2)); // 3
    const calc = new Calculator();
    console.log(calc.add(4, 5)); // 9
    const MainCalc = require('./mainCalculator.js');
    const mainCalc = new MainCalc();
    console.log(mainCalc.add(6, 7)); // 13

3. AMD 模块(Asynchronous Module Definition)

AMD 模块系统主要用于浏览器环境,支持异步加载模块,使用 definerequire 来定义和加载模块。

定义模块(define

  • 定义模块
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // mathUtils.js
    define('mathUtils', [], function() {
    const add = (a, b) => a + b;
    const subtract = (a, b) => a - b;
    class Calculator {
    constructor() {}
    add(a, b) {
    return a + b;
    }
    }
    return { add, subtract, Calculator };
    });

加载模块(require

  • 加载模块
    1
    2
    3
    4
    5
    6
    7
    // app.js
    require(['mathUtils'], function(mathUtils) {
    console.log(mathUtils.add(2, 3)); // 5
    console.log(mathUtils.subtract(5, 2)); // 3
    const calc = new mathUtils.Calculator();
    console.log(calc.add(4, 5)); // 9
    });

4. UMD 模块(Universal Module Definition)

UMD 模块系统是一种通用模块定义方式,旨在兼容 AMD、CommonJS 和全局变量的模块加载方式。它可以在浏览器和 Node. js 环境中使用。

定义 UMD 模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// mathUtils.js
(function(factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define([], factory);
} else if (typeof module === 'object' && module.exports) {
// Node.js
module.exports = factory();
} else {
// Browser globals
window.mathUtils = factory();
}
}(function() {
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
class Calculator {
constructor() {}
add(a, b) {
return a + b;
}
}
return { add, subtract, Calculator };
}));

总结

  • ES6 模块:现代 JavaScript 的官方模块系统,支持 importexport 语法,适用于浏览器和 Node. js 环境。
  • CommonJS 模块:主要用于 Node. js 环境,支持 requiremodule.exports 语法。
  • AMD 模块:主要用于浏览器环境,支持异步加载模块,使用 definerequire 语法。
  • UMD 模块:通用模块定义方式,兼容 AMD、CommonJS 和全局变量的模块加载方式。
    在实际开发中,选择哪种模块系统取决于你的项目需求和运行环境。ES6 模块是现代 JavaScript 开发的首选,但在某些情况下,CommonJS 和 AMD 仍然有其用武之地。

ES6 模块和 CommonJS 模块有什么区别?

ES6 模块(ECMAScript 模块)和 CommonJS 模块是 JavaScript 中两种主要的模块系统,它们在语法、功能和使用场景上有一些关键区别。以下是它们的主要区别:

1. 语法

  • ES6 模块
    • 使用 importexport 语法。
    • 支持命名导出和默认导出。
    • 导入时可以使用任意名称导入默认导出,但命名导出必须使用相同的名称。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // mathUtils.js (ES6 模块)
    export const add = (a, b) => a + b;
    export const subtract = (a, b) => a - b;
    export default class Calculator {
    constructor() {}
    add(a, b) {
    return a + b;
    }
    }
    1
    2
    3
    4
    5
    6
    7
    // app.js (ES6 模块)
    import { add, subtract } from './mathUtils.js';
    import Calculator from './mathUtils.js';
    console.log(add(2, 3)); // 5
    console.log(subtract(5, 2)); // 3
    const calc = new Calculator();
    console.log(calc.add(4, 5)); // 9
  • CommonJS 模块
    • 使用 requiremodule.exports 语法。
    • 只支持默认导出,但可以通过对象形式导出多个值。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // mathUtils.js (CommonJS 模块)
    const add = (a, b) => a + b;
    const subtract = (a, b) => a - b;
    class Calculator {
    constructor() {}
    add(a, b) {
    return a + b;
    }
    }
    module.exports = { add, subtract, Calculator };
    1
    2
    3
    4
    5
    6
    // app.js (CommonJS 模块)
    const { add, subtract, Calculator } = require('./mathUtils.js');
    console.log(add(2, 3)); // 5
    console.log(subtract(5, 2)); // 3
    const calc = new Calculator();
    console.log(calc.add(4, 5)); // 9

2. 功能

  • ES6 模块
    • 静态分析importexport 语句是静态的,可以在编译时进行分析,这使得工具(如 Webpack)可以进行代码分割和树摇优化。
    • 支持异步加载:ES6 模块支持异步加载,可以通过 import() 返回一个 Promise 来动态加载模块。
    • 支持命名导出和默认导出:可以同时使用命名导出和默认导出。
  • CommonJS 模块
    • 动态加载require 是动态的,只能在运行时解析,不支持静态分析。
    • 只支持默认导出:虽然可以通过对象形式导出多个值,但没有命名导出的概念。
    • 同步加载require 是同步的,模块在调用时立即加载,不支持异步加载。

3. 使用场景

  • ES6 模块
    • 浏览器环境:ES6 模块原生支持浏览器环境,可以直接在 HTML 文件中使用 <script type="module"> 标签。
    • 现代 JavaScript 开发:适用于现代 JavaScript 项目,特别是使用构建工具(如 Webpack、Rollup)的项目。
    • 支持动态导入:适用于需要按需加载模块的场景,如代码分割和懒加载。
    1
    2
    3
    4
    <script type="module">
    import { add } from './mathUtils.js';
    console.log(add(2, 3)); // 5
    </script>
  • CommonJS 模块
    • Node. js 环境:CommonJS 是 Node. js 的默认模块系统,适用于服务器端 JavaScript 开发。
    • 同步加载:适用于不需要异步加载的场景,模块在启动时一次性加载。

4. 性能

  • ES6 模块
    • 静态分析优化:由于 importexport 是静态的,构建工具可以进行代码分割和树摇优化,减少最终打包文件的大小。
    • 异步加载:支持异步加载,可以按需加载模块,提高应用的加载性能。
  • CommonJS 模块
    • 同步加载:模块在启动时一次性加载,可能会导致初始加载时间较长。
    • 缓存机制:Node. js 有模块缓存机制,重复 require 同一个模块时会返回缓存的实例,提高性能。

5. 兼容性

  • ES6 模块
    • 现代浏览器:现代浏览器原生支持 ES6 模块,但需要使用 <script type="module"> 标签。
    • Node. js:Node. js 从 v13.2.0 开始支持 ES6 模块,但需要使用 .mjs 文件扩展名或在 package.json 中设置 "type": "module"
  • CommonJS 模块
    • Node. js:Node. js 默认支持 CommonJS 模块,使用 .js 文件扩展名。
    • 浏览器:浏览器不原生支持 CommonJS 模块,需要使用构建工具(如 Browserify)转换为浏览器兼容的格式。

总结

  • ES6 模块:适用于现代 JavaScript 开发,特别是需要静态分析、代码分割和异步加载的场景。原生支持浏览器环境,也适用于 Node. js 环境。
  • CommonJS 模块:适用于 Node. js 环境,特别是不需要异步加载的服务器端开发。不支持浏览器环境,需要构建工具转换。
    在实际开发中,选择哪种模块系统取决于你的项目需求和运行环境。对于现代前端项目,ES6 模块是首选;对于 Node. js 项目,CommonJS 模块仍然是主流选择,但 ES6 模块也在逐渐普及。

vue 使用的模块

Vue 主要使用 ES6 模块系统来实现模块化开发。ES6 模块提供了 importexport 语法,使得代码的组织和管理更加清晰和高效。以下是 Vue 中使用 ES6 模块的一些常见方式:

1. 单文件组件(Single File Components, SFC)

Vue 的单文件组件(.vue 文件)是模块化开发的核心。每个 .vue 文件包含 <template><script><style> 三个部分,分别定义组件的模板、脚本和样式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div>{{ message }}</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello Vue!'
};
}
}
</script>
<style>
/* CSS 样式 */
</style>

2. 组件导入

在 Vue 项目中,可以使用 import 语句导入其他组件或模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
// MyComponent.vue
<template>
<div>Count is: {{ count }}</div>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const count = ref(0);
return { count };
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
// App.vue
<template>
<MyComponent />
</template>
<script>
import MyComponent from './MyComponent.vue';
export default {
components: {
MyComponent
}
}
</script>

3. Vuex 模块化

Vuex 是 Vue 的状态管理库,支持模块化开发。可以通过创建多个模块来管理不同的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
// store/modules/cart.js
const state = {
items: []
};
const mutations = {
ADD_ITEM(state, item) {
state.items.push(item);
}
};
export default {
state,
mutations
};
1
2
3
4
5
6
7
8
// store/index.js
import { createStore } from 'vuex';
import cart from './modules/cart';
export default createStore({
modules: {
cart
}
});

4. Vue Router 模块化

Vue Router 用于管理 Vue 应用的路由,也可以通过模块化的方式组织路由配置。

1
2
3
4
5
// router/modules/home.js
const routes = [
{ path: '/', component: () => import('../views/Home.vue') }
];
export default routes;
1
2
3
4
5
6
7
8
9
10
11
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import homeRoutes from './modules/home';
const routes = [
...homeRoutes
];
const router = createRouter({
history: createWebHistory(),
routes
});
export default router;

5. 项目结构

Vue 项目通常采用以下结构,每个模块有独立的目录和文件:

1
2
3
4
5
6
7
8
src/
├── assets/ # 静态资源
├── components/ # 公共组件
├── views/ # 页面组件
├── store/ # Vuex 状态管理
├── router/ # Vue Router 路由管理
├── main.js # 入口文件
└── App.vue # 根组件

6. 构建工具

Vue 项目通常使用构建工具(如 Webpack、Vite)来处理模块化代码。这些工具可以将多个模块打包成一个或多个文件,优化加载性能。

1
2
3
4
5
6
// 使用 Vite
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()]
});

总结

Vue 通过 ES6 模块系统实现了模块化开发,支持单文件组件、Vuex 状态管理、Vue Router 路由管理等多种方式。这些模块化技术使得 Vue 项目更加清晰、易于维护和扩展。

esm和cmj的区别

ESM(EcmaScript Modules)和CMJ(CommonJS)是两种不同的JavaScript模块规范,它们在多个方面存在区别:

  1. 定义和引入方式
    • ESM使用importexport关键字来定义和引入模块。
    • CJS使用require来引入模块,使用module.exportsexports来导出模块。
  2. 加载机制
    • ESM的加载机制是静态的,即在编译阶段就确定模块依赖关系,这使得浏览器可以并行加载模块。
    • CJS的加载机制是动态的,在代码运行时解析模块路径,因此更灵活。
  3. 作用域和全局变量
    • ESM模块始终运行在严格模式下,并且每个模块都有自己的作用域,顶层变量外部不可见。
    • CJS模块共享一个全局作用域,可能引发作用域污染的问题。
  4. 输出的可变性
    • ESM的导出是不可变的,导出值本质上是一个绑定,修改导出值会影响到所有导入该值的模块。
    • CJS的导出是一个值的拷贝,修改导出值不会影响到导入该值的模块。
  5. this的指向
    • 在ESM中,this指向undefined
    • 在CJS中,this指向当前模块的exports对象。
  6. 文件路径和扩展名
    • ESM要求明确使用文件扩展名,例如.js.mjs
    • CJS通常不需要指定文件扩展名,因为require函数会自动处理。
  7. 异步加载和编译时加载
    • ESM支持异步加载,允许使用import()语法动态加载模块。
    • CJS是同步加载,不适合在浏览器环境中使用,因为同步加载会阻塞主线程。
  8. 支持的JavaScript环境
    • ESM是JavaScript的官方标准,在现代浏览器和Node.js环境中原生支持。
    • CJS是Node.js的默认模块系统,生态系统成熟,兼容性强。
  9. 顶级await
    • ESM支持在模块顶层使用await,而CJS不支持。
  10. __filename和__dirname
    • 在CJS中,模块的执行需要用函数包起来,并指定一些常用的值,如__filename__dirname
    • 在ESM中,不存在__filename__dirname这两个变量。
      这些区别使得ESM和CJS在不同的应用场景下各有优势,开发者可以根据项目需求和环境选择合适的模块规范。

命名空间导入

在 ES6 模块系统中,命名空间导入(Namespace Import) 是一种将整个模块的导出内容作为一个对象导入的方式。通过这种方式,你可以通过一个命名空间对象访问模块中的所有导出成员。以下是详细的解释和用法:

1. 基本语法

使用 import * as Namespace 语法将模块的所有导出内容绑定到一个命名空间对象:

1
2
3
4
5
// 导入整个模块内容到命名空间对象 'mathUtils'
import * as mathUtils from './math-utils.js';
// 使用命名空间访问导出成员
const sum = mathUtils.add(2, 3);
const product = mathUtils.multiply(4, 5);

2. 核心作用

  • 避免命名冲突:将模块内容封装到一个命名空间对象中,减少全局命名污染。
  • 统一管理导出内容:当模块导出多个成员时,通过命名空间对象集中访问。
  • 动态访问成员:可通过对象属性语法动态选择成员(如 mathUtils[funcName])。

3. 模块导出示例

假设 math-utils.js 导出多个函数:

1
2
3
4
// math-utils.js
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
export const PI = 3.14159;

4. 命名空间对象的结构

导入后的 mathUtils 对象结构如下:

1
2
3
4
5
{
add: function add(a, b) { ... },
multiply: function multiply(a, b) { ... },
PI: 3.14159
}

5. 适用场景

(1)模块导出大量成员

当模块有多个导出项时,命名空间导入可简化访问:

1
2
3
4
// 模块导出多个工具函数
import * as utils from './utils.js';
utils.formatDate();
utils.validateEmail();

(2)需要动态调用导出成员

通过字符串动态选择方法:

1
2
const operation = 'add';
const result = mathUtils[operation](2, 3); // 调用 mathUtils.add(2, 3)

(3)第三方库的全局工具集

某些库推荐以命名空间形式使用(如早期 Lodash):

1
2
import * as _ from 'lodash';
_.debounce(() => {}, 100);

6. 与其他导入方式的对比

导入方式 语法 特点
命名空间导入 import * as Namespace 所有导出绑定到一个对象,适合多成员模块
命名导入(Named Import) import { add, PI } 仅导入指定成员,代码更简洁,推荐常用方式
默认导入(Default Import) import ModuleDefault 仅导入默认导出(需模块有 export default

7. 注意事项

(1)不包含默认导出

若模块同时存在 export default 和命名导出,命名空间对象不包含默认导出

1
2
3
4
5
6
7
// 模块代码:math-utils.js
export default function log() { console.log('default'); }
export function add() { /* ... */ }
// 导入后
import * as mathUtils from './math-utils.js';
console.log(mathUtils.default); // 默认导出作为 'default' 属性
mathUtils.default(); // 调用默认导出函数

2)Tree-Shaking 限制

命名空间导入可能导致打包工具(如 Webpack)无法优化未使用的代码(无法 Tree-Shaking),因为所有导出都被视为已使用。
优化建议:优先使用命名导入(import { add })以明确依赖。

(3)循环引用问题

避免在模块间循环引用时通过命名空间动态访问成员,可能导致未定义错误。

8. 综合示例

模块文件:string-utils.js

1
2
3
4
5
6
export function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export function reverse(str) {
return str.split('').reverse().join('');
}

使用命名空间导入

1
2
3
4
import * as stringUtils from './string-utils.js';
const name = 'alice';
console.log(stringUtils.capitalize(name)); // "Alice"
console.log(stringUtils.reverse(name)); // "ecila"

总结

命名空间导入(import * as Namespace)是 ES6 模块中管理多成员模块的有效方式,尤其适用于需要动态访问或避免命名冲突的场景。然而,在追求代码简洁性和打包优化时,更推荐优先使用命名导入import { add })或默认导入import ModuleDefault)。根据实际需求灵活选择导入方式,可以让代码更清晰、高效。

08 DOM

08 DOM

document 对象

document 对象是 Web 开发中非常核心的一个对象,它代表了整个 HTML 文档,是 DOM(文档对象模型)树的根节点,提供了对文档中元素的访问和操作接口。以下是关于 document 对象的一些详细介绍:

1. 获取元素的方法

1.1 getElementById(id)

  • 描述:根据元素的 id 属性值获取对应的元素,返回一个元素对象。如果文档中没有匹配的元素,则返回 null
  • 示例
    1
    2
    let element = document.getElementById('myElement');
    console.log(element); // 返回 id 为 'myElement' 的元素对象

1.2 getElementsByClassName(classNames)

  • 描述:根据元素的 class 属性值获取一组元素,返回一个 HTMLCollection 对象,该对象是一个类数组对象,包含所有匹配的元素。如果文档中没有匹配的元素,则返回一个空的 HTMLCollection 对象。
  • 示例
    1
    2
    let elements = document.getElementsByClassName('myClass');
    console.log(elements); // 返回一个包含所有 class 为 'myClass' 的元素的 HTMLCollection 对象

1.3 getElementsByTagName(tagName)

  • 描述:根据元素的标签名获取一组元素,返回一个 HTMLCollection 对象,包含所有匹配的元素。如果文档中没有匹配的元素,则返回一个空的 HTMLCollection 对象。
  • 示例
    1
    2
    let elements = document.getElementsByTagName('div');
    console.log(elements); // 返回一个包含所有 div 元素的 HTMLCollection 对象

1.4 querySelector(selector)

  • 描述:使用 CSS 选择器来选择文档中的第一个匹配的元素,返回一个元素对象。如果文档中没有匹配的元素,则返回 null
  • 示例
    1
    2
    let element = document.querySelector('.myClass');
    console.log(element); // 返回第一个 class 为 'myClass' 的元素对象

1.5 querySelectorAll(selector)

  • 描述:使用 CSS 选择器来选择文档中的所有匹配的元素,返回一个 NodeList 对象,该对象是一个类数组对象,包含所有匹配的元素。如果文档中没有匹配的元素,则返回一个空的 NodeList 对象。
  • 示例
    1
    2
    let elements = document.querySelectorAll('.myClass');
    console.log(elements); // 返回一个包含所有 class 为 'myClass' 的元素的 NodeList 对象

2. 文档信息属性

2.1 document.title

  • 描述:获取或设置文档的标题,对应 HTML 中的 <title> 标签内容。
  • 示例
    1
    2
    console.log(document.title); // 获取文档标题
    document.title = '新标题'; // 设置文档标题

2.2 document.URL

  • 描述:获取当前文档的完整 URL。
  • 示例
    1
    console.log(document.URL); // 获取当前文档的完整 URL

2.3 document.domain

  • 描述:获取或设置当前文档的域名。设置 document.domain 可以用于跨子域的通信。
  • 示例
    1
    2
    console.log(document.domain); // 获取当前文档的域名
    document.domain = 'example.com'; // 设置文档域名

2.4 document.referrer

  • 描述:获取加载当前文档的前一个文档的 URL。
  • 示例
    1
    console.log(document.referrer); // 获取前一个文档的 URL

3. 文档操作方法

3.1 document.createElement(tagName)

  • 描述:创建一个新的元素节点,返回一个元素对象。
  • 示例
    1
    2
    let newElement = document.createElement('div');
    console.log(newElement); // 返回一个新的 div 元素对象

3.2 document.createTextNode(text)

  • 描述:创建一个新的文本节点,返回一个文本节点对象。
  • 示例
    1
    2
    let textNode = document.createTextNode('Hello, World!');
    console.log(textNode); // 返回一个新的文本节点对象

3.3 document.appendChild(node)

  • 描述:将一个节点添加到文档中的指定父节点的子节点列表的末尾。
  • 示例
    1
    2
    3
    let parentElement = document.getElementById('parent');
    let newElement = document.createElement('div');
    parentElement.appendChild(newElement); // 将 newElement 添加到 parentElement 的末尾

3.4 document.insertBefore(newNode, referenceNode)

  • 描述:将一个节点插入到文档中的指定父节点的子节点列表中,插入位置在 referenceNode 之前。
  • 示例
    1
    2
    3
    4
    let parentElement = document.getElementById('parent');
    let newElement = document.createElement('div');
    let referenceElement = document.getElementById('reference');
    parentElement.insertBefore(newElement, referenceElement); // 将 newElement 插入到 referenceElement 之前

3.5 document.removeChild(node)

  • 描述:从文档中删除指定的节点。
  • 示例
    1
    2
    3
    let parentElement = document.getElementById('parent');
    let childElement = document.getElementById('child');
    parentElement.removeChild(childElement); // 从 parentElement 中删除 childElement

3.6 document.replaceChild(newNode, oldNode)

  • 描述:用一个新的节点替换文档中的一个旧的节点。
  • 示例
    1
    2
    3
    4
    let parentElement = document.getElementById('parent');
    let newElement = document.createElement('div');
    let oldElement = document.getElementById('old');
    parentElement.replaceChild(newElement, oldElement); // 用 newElement 替换 oldElement

4. 事件相关方法

4.1 document.addEventListener(type, listener, options)

  • 描述:为文档添加一个事件监听器。
  • 示例
    1
    2
    3
    document.addEventListener('click', function() {
    console.log('文档被点击');
    });

4.2 document.removeEventListener(type, listener, options)

  • 描述:移除文档上之前添加的事件监听器。
  • 示例
    1
    2
    3
    4
    5
    function handleClick() {
    console.log('文档被点击');
    }
    document.addEventListener('click', handleClick);
    document.removeEventListener('click', handleClick);

5. 其他常用方法

5.1 document.write(text)

  • 描述:向浏览器的文档流中直接写内容,可以用于动态生成 HTML 内容。不过在现代开发中,推荐使用更现代的方法(如 innerHTMLtextContent 等)来操作 DOM,因为 document.write 有一些已知的问题和限制。
  • 示例
    1
    document.write('<div>Hello, World!</div>');

5.2 document.getElementById(id).innerHTML

  • 描述:获取或设置元素的 HTML 内容。
  • 示例
    1
    2
    3
    let element = document.getElementById('myElement');
    console.log(element.innerHTML); // 获取元素的 HTML 内容
    element.innerHTML = '<span>New Content</span>'; // 设置元素的 HTML 内容

5.3 document.getElementById(id).textContent

  • 描述:获取或设置元素的文本内容,与 innerHTML 不同,textContent 不会解析 HTML 标签。
  • 示例
    1
    2
    3
    let element = document.getElementById('myElement');
    console.log(element.textContent); // 获取元素的文本内容
    element.textContent = 'New Text Content'; // 设置元素的文本内容

6. 文档状态属性

6.1 document.readyState

  • 描述:返回文档的加载状态,可能的值有:
    • loading:文档正在加载。
    • interactive:文档已加载完毕,但子资源(如图片、样式表等)仍在加载。
    • complete:文档及其所有子资源都已加载完毕。
  • 示例
    1
    console.log(document.readyState); // 获取文档的加载状态

6.2 document.addEventListener('DOMContentLoaded', handler)

  • 描述:监听 DOMContentLoaded 事件,该事件在文档的初始 HTML 完全加载和解析完成后触发,不包括样式表、图片和子框架的加载。
  • 示例
    1
    2
    3
    document.addEventListener('DOMContentLoaded', function() {
    console.log('DOM 加载完成');
    });

7. 窗口和文档的关系

document 对象是 window 对象的一部分,可以通过 window.document 访问。window 对象代表浏览器窗口,而 document 对象代表窗口中的文档内容。它们之间的关系如下:

  • window 是全局对象,所有全局变量和函数都是 window 的属性。
  • documentwindow 的一个属性,代表文档内容。
  • windowdocument 之间有一些事件和方法是相互关联的,例如 window.onloaddocument.addEventListener('load', handler) 都可以用于监听页面加载完成的事件。

总结

document 对象是 Web 开发中非常重要的一个对象,它提供了对文档中元素的访问和操作接口,允许我们动态地创建、修改和删除元素,监听和处理各种事件,获取文档的状态信息等。掌握 document 对象的这些方法和属性,可以帮助我们更灵活地操作 DOM,实现丰富的交互效果。

10 BOM window对象

BOM的window对象

Window对象

window对象是浏览器环境中的全局对象,代表浏览器的窗口。在浏览器中,所有的JavaScript代码都是在window对象的作用域下运行的。以下是window对象的一些关键特性和常用属性与方法:

基本特性

  • 全局作用域:在浏览器中,window是全局作用域的代表。所有全局变量和函数都是window的属性。
    1
    2
    var a = 1;
    console.log(window.a); // 输出 1
  • 事件监听器:可以为window对象添加事件监听器,以响应各种事件,如窗口加载、窗口大小变化等。
    1
    2
    3
    window.addEventListener('load', function() {
    console.log('页面加载完成');
    });

常用属性

  • document:指向当前窗口的document对象,用于操作DOM。
    1
    2
    const title = window.document.title;
    console.log(title); // 输出当前页面的标题
  • location:包含当前窗口的URL信息,并提供方法来加载新的文档。
    1
    2
    console.log(window.location.href); // 输出当前页面的完整URL
    window.location.href = 'https://www.example.com'; // 跳转到新的页面
  • navigator:包含有关浏览器的信息,如浏览器名称、版本等。
    1
    console.log(window.navigator.userAgent); // 输出用户代理字符串
  • history:允许访问用户的历史记录,可以用于前进和后退操作。
    1
    2
    window.history.back(); // 后退到上一个页面
    window.history.go(-2); // 后退两个页面
  • screen:包含有关用户屏幕的信息,如屏幕宽度和高度。
    1
    2
    console.log(window.screen.width); // 输出屏幕宽度
    console.log(window.screen.height); // 输出屏幕高度

常用方法

  • setTimeoutclearTimeout:用于设置和清除定时器。
    1
    2
    3
    4
    5
    const timer = window.setTimeout(() => {
    console.log('Hello after 2 seconds');
    }, 2000);

    window.clearTimeout(timer); // 清除定时器
  • setIntervalclearInterval:用于设置和清除间隔定时器。
    1
    2
    3
    4
    5
    const interval = window.setInterval(() => {
    console.log('Hello every 2 seconds');
    }, 2000);

    window.clearInterval(interval); // 清除间隔定时器
  • alertconfirmprompt:用于显示对话框。
    1
    2
    3
    window.alert('Hello, World!'); // 显示警告框
    const isConfirmed = window.confirm('Are you sure?'); // 显示确认框
    const name = window.prompt('What is your name?'); // 显示输入框

其他特性

  • localStoragesessionStorage:用于在浏览器中存储数据。
    1
    2
    window.localStorage.setItem('key', 'value'); // 存储数据
    const value = window.localStorage.getItem('key'); // 获取数据
  • console:用于调试输出信息。
    1
    window.console.log('Hello, World!'); // 输出信息

window对象是浏览器JavaScript编程的基础,提供了丰富的API来操作浏览器窗口、文档、历史记录等。通过合理使用window对象的属性和方法,可以实现各种复杂的浏览器功能和交互.

09 DOM事件

09 DOM事件

要监听一个 <div> 元素的大小变化,可以通过以下方法实现:

方法 1:使用 ResizeObserver API(推荐)

ResizeObserver 是现代浏览器原生支持的 API,专门用于监听元素尺寸变化(包括 widthheightpaddingborder 等)。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1. 创建 ResizeObserver 实例
const observer = new ResizeObserver((entries) => {
for (let entry of entries) {
// 获取元素的最新尺寸
const width = entry.contentRect.width;
const height = entry.contentRect.height;
console.log(`元素尺寸变化:宽度 ${width}px,高度 ${height}px`);
}
});
// 2. 获取目标元素
const divElement = document.querySelector('#yourDivId');
// 3. 开始监听
observer.observe(divElement);
// 4. 停止监听(可选)
// observer.unobserve(divElement);

优点

  • 精准监听元素尺寸变化。
  • 支持监听多个元素。
  • 无需依赖 CSS 属性(如动画或过渡)。

浏览器兼容性

  • 支持 Chrome 64+、Firefox 69+、Edge 79+、Safari 13.1+。
  • 如需兼容旧浏览器,需使用 polyfill

方法 2:通过 window.resize 事件(间接监听)

如果元素尺寸变化是由窗口大小变化触发的(例如 CSS 响应式布局),可以通过监听 windowresize 事件间接判断。

代码示例

1
2
3
4
window.addEventListener('resize', () => {
const div = document.querySelector('#yourDivId');
console.log('窗口大小变化,div 尺寸:', div.offsetWidth, div.offsetHeight);
});

缺点

  • 仅能监听到由窗口大小变化触发的尺寸变化。
  • 无法监听到因内容、JavaScript 直接修改样式等导致的尺寸变化。

方法 3:使用 MutationObserver(不推荐)

如果元素尺寸变化是由属性(如 styleclass)修改触发的,可以尝试用 MutationObserver 监听属性变化。

代码示例

1
2
3
4
5
6
7
8
9
10
11
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'style') {
const width = mutation.target.offsetWidth;
const height = mutation.target.offsetHeight;
console.log('元素样式变化,尺寸:', width, height);
}
});
});
const divElement = document.querySelector('#yourDivId');
observer.observe(divElement, { attributes: true });

缺点

  • 无法监听到由内容变化(如子元素增减)导致的尺寸变化。
  • 性能开销较大。

方法 4:CSS 动画监听(Hack 方法)

通过 CSS 动画和 transitionend 事件间接监听尺寸变化(需配合 CSS 过渡)。

代码示例

1
2
3
#yourDivId {
transition: width 0.1s, height 0.1s;
}
1
2
3
4
5
6
const divElement = document.querySelector('#yourDivId');
divElement.addEventListener('transitionend', (e) => {
if (e.propertyName === 'width' || e.propertyName === 'height') {
console.log('元素尺寸变化:', e.target.offsetWidth, e.target.offsetHeight);
}
});

缺点

  • 需要强制添加 CSS 过渡。
  • 仅能监听到通过 CSS 触发的尺寸变化。

总结

方法 适用场景 精准度 兼容性
ResizeObserver 任意尺寸变化(推荐) 现代浏览器
window.resize 窗口变化触发的尺寸变化 所有浏览器
MutationObserver 属性修改触发的尺寸变化 现代浏览器
CSS 动画监听 CSS 过渡触发的尺寸变化(不推荐) 所有浏览器
推荐直接使用 ResizeObserver,其他方法可作为兼容性兜底方案。

事件类型 事件名称 是否冒泡
鼠标事件 click
dblclick
mousedown
mouseup
mousemove
mouseover
mouseout
mouseenter
mouseleave
键盘事件 keydown
keyup
keypress (已废弃)
表单事件 submit
change
input
focus
blur
focusin
focusout
invalid
窗口/文档事件 load
unload
resize
scroll
hashchange
DOMContentLoaded
beforeunload
visibilitychange
触摸事件 touchstart
touchend
touchmove
touchcancel
媒体事件 play
pause
ended
timeupdate
过渡/动画事件 transitionend
animationstart
animationend
animationiteration
剪贴板事件 copy
cut
paste
拖放事件 dragstart
drag
dragend
dragenter
dragover
dragleave
drop
其他事件 contextmenu

DOM事件

DOM事件是Web开发中非常重要的一个方面,它允许我们为页面上的元素添加交互性。以下是关于DOM事件的一些详细介绍:

1. 事件类型

1.1 用户界面事件

  • load:当文档加载完成时触发,常用于页面初始化操作。
    1
    2
    3
    window.addEventListener('load', function() {
    console.log('页面加载完成');
    });
  • DOMContentLoaded:当初始的HTML文档被完全加载和解析完毕时触发,不包括样式表、图片和子框架加载时间。
    1
    2
    3
    document.addEventListener('DOMContentLoaded', function() {
    console.log('DOM加载完成');
    });
  • resize:当窗口或框架被调整大小时触发。
    1
    2
    3
    window.addEventListener('resize', function() {
    console.log('窗口大小改变');
    });
  • scroll:当用户滚动文档或元素时触发。
    1
    2
    3
    window.addEventListener('scroll', function() {
    console.log('页面滚动');
    });

1.2 鼠标事件

  • click:鼠标点击事件,常用于按钮点击等操作。
    1
    2
    3
    document.getElementById('myButton').addEventListener('click', function() {
    console.log('按钮被点击');
    });
  • dblclick:鼠标双击事件。
    1
    2
    3
    document.getElementById('myElement').addEventListener('dblclick', function() {
    console.log('元素被双击');
    });
  • mousedown:鼠标按钮被按下时触发。
    1
    2
    3
    document.getElementById('myElement').addEventListener('mousedown', function(event) {
    console.log('鼠标按下,按钮:' + event.button);
    });
  • mouseup:鼠标按钮被释放时触发。
    1
    2
    3
    document.getElementById('myElement').addEventListener('mouseup', function(event) {
    console.log('鼠标释放,按钮:' + event.button);
    });
  • mousemove:鼠标在元素上移动时触发,事件频率较高,使用时需注意性能优化。
    1
    2
    3
    document.getElementById('myElement').addEventListener('mousemove', function(event) {
    console.log('鼠标移动,坐标:(' + event.clientX + ',' + event.clientY + ')');
    });
  • mouseover:鼠标指针移动到元素上时触发。
    1
    2
    3
    document.getElementById('myElement').addEventListener('mouseover', function() {
    console.log('鼠标移入元素');
    });
  • mouseout:鼠标指针离开元素时触发。
    1
    2
    3
    document.getElementById('myElement').addEventListener('mouseout', function() {
    console.log('鼠标移出元素');
    });
  • mouseenter:鼠标指针进入元素时触发,与mouseover不同,它不会在鼠标从子元素移动到父元素时触发。
    1
    2
    3
    document.getElementById('myElement').addEventListener('mouseenter', function() {
    console.log('鼠标进入元素');
    });
  • mouseleave:鼠标指针离开元素时触发,与mouseout不同,它不会在鼠标从父元素移动到子元素时触发。
    1
    2
    3
    document.getElementById('myElement').addEventListener('mouseleave', function() {
    console.log('鼠标离开元素');
    });

1.3 键盘事件

  • keydown:某个键盘按键被按下时触发。
    1
    2
    3
    document.addEventListener('keydown', function(event) {
    console.log('按键按下,键码:' + event.keyCode);
    });
  • keyup:某个键盘按键被释放时触发。
    1
    2
    3
    document.addEventListener('keyup', function(event) {
    console.log('按键释放,键码:' + event.keyCode);
    });
  • keypress:某个键盘按键被按下并产生字符值时触发,不过在现代浏览器中已不推荐使用,可使用keydownkeyup代替。
    1
    2
    3
    document.addEventListener('keypress', function(event) {
    console.log('字符键按下,字符:' + String.fromCharCode(event.charCode));
    });

1.4 表单事件

  • submit:表单提交时触发,常用于表单验证等操作。
    1
    2
    3
    4
    document.getElementById('myForm').addEventListener('submit', function(event) {
    event.preventDefault(); // 阻止表单默认提交行为
    console.log('表单提交');
    });
  • change:表单域内容改变后触发,适用于inputselecttextarea等元素。
    1
    2
    3
    document.getElementById('myInput').addEventListener('change', function() {
    console.log('输入框内容改变');
    });
  • input:表单域内容正在改变时触发,与change事件不同,它在输入过程中实时触发。
    1
    2
    3
    document.getElementById('myInput').addEventListener('input', function() {
    console.log('输入框内容正在改变');
    });
  • focus:元素获得焦点时触发。
    1
    2
    3
    document.getElementById('myInput').addEventListener('focus', function() {
    console.log('输入框获得焦点');
    });
  • blur:元素失去焦点时触发。
    1
    2
    3
    document.getElementById('myInput').addEventListener('blur', function() {
    console.log('输入框失去焦点');
    });

2. 事件流

DOM事件的传播遵循特定的路径,称为事件流。事件流分为三个阶段:

  • 捕获阶段:事件从window对象开始,逐级向下传递,直到到达目标元素的父元素。
  • 目标阶段:事件到达目标元素。
  • 冒泡阶段:事件从目标元素开始,逐级向上传递,直到window对象。

3. 事件监听器

通过addEventListener方法可以为元素添加事件监听器,它接受三个参数:

  • 事件类型:字符串,表示事件的名称,如'click''mouseover'等。
  • 事件处理函数:当事件触发时要执行的函数。
  • 布尔值或选项对象:用于指定事件监听器的行为。如果为true,表示在捕获阶段触发事件处理函数;如果为false(默认值),表示在冒泡阶段触发事件处理函数。也可以传入一个选项对象,如{capture: true}表示捕获阶段触发,{once: true}表示事件处理函数只执行一次后自动移除等。
    1
    2
    3
    4
    5
    6
    document.getElementById('myElement').addEventListener('click', function() {
    console.log('点击事件');
    }, false); // 指定在冒泡阶段触发
    document.getElementById('myElement').addEventListener('click', function() {
    console.log('捕获阶段点击事件');
    }, true); // 指定在捕获阶段触发

4. 事件对象

当事件触发时,会创建一个事件对象,并作为参数传递给事件处理函数。事件对象包含了许多有用的属性和方法,例如:

  • target:触发事件的元素。
  • currentTarget:绑定事件监听器的元素。
  • type:事件的类型,如'click''mouseover'等。
  • eventPhase:事件当前所处的阶段,1表示捕获阶段,2表示目标阶段,3表示冒泡阶段。
  • preventDefault():阻止事件的默认行为。
  • stopPropagation():停止事件的进一步传播,即阻止事件冒泡或捕获。
  • stopImmediatePropagation():停止事件的进一步传播,并阻止同一个事件的其他监听器被调用。
    1
    2
    3
    4
    5
    6
    document.getElementById('myElement').addEventListener('click', function(event) {
    console.log('触发事件的元素:' + event.target);
    console.log('绑定事件监听器的元素:' + event.currentTarget);
    event.preventDefault(); // 阻止默认行为
    event.stopPropagation(); // 停止事件传播
    });

5. 移除事件监听器

使用removeEventListener方法可以移除之前添加的事件监听器,它需要传入与addEventListener相同的参数,即事件类型和事件处理函数。

1
2
3
4
5
6
function handleClick() {
console.log('点击事件');
}
document.getElementById('myElement').addEventListener('click', handleClick);
// 移除事件监听器
document.getElementById('myElement').removeEventListener('click', handleClick);

掌握DOM事件的相关知识,可以帮助我们为网页添加丰富的交互效果,提升用户体验。

事件冒泡

事件冒泡(Event Bubbling)是浏览器中事件传播的一种机制,指的是当一个事件在 DOM 树中发生时,该事件会从事件的目标元素(事件的起始点)开始,逐级向上传播到根元素的过程。以下是关于事件冒泡的一些关键点和示例:

工作原理

  • 传播路径
    • 当一个事件在某个元素上触发时,该事件首先在该元素上触发,然后逐级向上传播到其父元素,直到达到根元素(通常是 <html> 标签)。
  • 默认行为
    • 大多数鼠标事件(如 clickmouseover)和一些键盘事件(如 keydown)默认具有冒泡行为。
    • 有些事件默认不冒泡,例如 focusblurload 等。

示例

假设有一个嵌套的 HTML 结构:

1
2
3
4
5
<div id="outer">
<div id="inner">
<button id="button">Click me</button>
</div>
</div>

在 JavaScript 中为这些元素添加事件监听器:

1
2
3
4
5
6
7
8
9
document.getElementById('button').addEventListener('click', function() {
console.log('Button clicked');
});
document.getElementById('inner').addEventListener('click', function() {
console.log('Inner div clicked');
});
document.getElementById('outer').addEventListener('click', function() {
console.log('Outer div clicked');
});

当点击按钮时,控制台输出会依次显示:

1
2
3
Button clicked
Inner div clicked
Outer div clicked

阻止事件冒泡

  • event.stopPropagation()
    • 可以在事件处理函数中调用 event.stopPropagation() 方法来阻止事件继续冒泡。
    1
    2
    3
    4
    document.getElementById('button').addEventListener('click', function(event) {
    console.log('Button clicked');
    event.stopPropagation(); // 阻止事件冒泡
    });
  • 返回 false
    • 在使用 jQuery 或某些旧的浏览器事件模型时,返回 false 也可以阻止事件冒泡(同时还会阻止默认行为).
    1
    2
    3
    4
    $('#button').click(function() {
    console.log('Button clicked');
    return false; // 阻止事件冒泡和默认行为
    });

事件捕获

  • 事件捕获与冒泡
    • 事件冒泡是事件传播的“冒泡阶段”,而事件捕获是事件传播的“捕获阶段”。
    • 在捕获阶段,事件从根元素开始向目标元素传播。
    • 可以通过在 addEventListener 的第三个参数中设置 true 来使用事件捕获。
    1
    2
    3
    document.getElementById('outer').addEventListener('click', function() {
    console.log('Outer div captured');
    }, true);

应用场景

  • 事件委托
    • 利用事件冒泡,可以在父元素上设置事件监听器来处理子元素的事件,从而减少事件监听器的数量,提高性能。
    1
    2
    3
    4
    5
    document.getElementById('outer').addEventListener('click', function(event) {
    if (event.target.tagName === 'BUTTON') {
    console.log('Button clicked');
    }
    });

    事件冒泡是浏览器事件处理机制的重要组成部分,理解其工作原理和如何控制事件传播对于编写高效的事件处理代码非常重要.

冒泡的事件

在浏览器中,大多数鼠标事件和一些键盘事件默认具有冒泡行为。以下是一些常见的可以冒泡的事件:

鼠标事件

  • click:鼠标点击事件。
  • dblclick:鼠标双击事件。
  • mousedown:鼠标按下事件。
  • mouseup:鼠标释放事件。
  • mousemove:鼠标移动事件。
  • mouseover:鼠标进入元素事件(注意:mouseover 事件在进入目标元素的子元素时也会触发,因此它具有冒泡特性).
  • mouseout:鼠标离开元素事件(同样具有冒泡特性).
  • mouseentermouseleave:这两个事件不冒泡,但它们的行为类似于 mouseovermouseout,但只在进入或离开目标元素时触发,不进入子元素.
  • contextmenu:右键点击事件(显示上下文菜单).

键盘事件

  • keydown:按下键盘上的任意键时触发。
  • keyup:释放键盘上的任意键时触发。
  • keypress:按下键盘上的字符键时触发(在现代浏览器中已不推荐使用,keydownkeyup 可以替代).

表单事件

  • submit:表单提交事件。
  • reset:表单重置事件.
  • change:表单元素的值发生变化时触发(对于 <input><select><textarea> 等元素).
  • input:表单元素的值发生变化时触发,与 change 类似,但 input 事件在每次输入时都会触发,而 change 事件在元素失去焦点时触发.

触摸事件

  • touchstart:触摸开始事件。
  • touchend:触摸结束事件。
  • touchmove:触摸移动事件。
  • touchcancel:触摸取消事件.

自定义事件

  • 自定义事件(通过 dispatchEvent 创建的事件)默认可以冒泡,除非在创建事件时显式设置 bubbles 属性为 false

不冒泡的事件

  • focusblur:焦点事件不冒泡。
  • load:资源加载完成事件不冒泡。
  • unload:资源卸载事件不冒泡.
  • error:错误事件不冒泡.
  • resetsubmit:虽然 resetsubmit 事件可以冒泡,但在某些情况下,它们的行为可能会有所不同,具体取决于浏览器的实现和上下文.
    理解哪些事件可以冒泡以及如何控制事件传播对于编写有效的事件处理代码非常重要,特别是在复杂的 DOM 结构中,事件冒泡可以用于事件委托等技术,从而提高性能和可维护性.

不能冒泡

在浏览器中,有一些事件默认不冒泡。以下是一些常见的不冒泡事件:

焦点事件

  • focus:当元素获得焦点时触发。例如,当用户点击或使用键盘导航使输入框获得焦点时。
  • blur:当元素失去焦点时触发。例如,当用户点击其他元素或切换到其他窗口时,当前元素会触发 blur 事件.

资源加载事件

  • load:当文档或资源完成加载时触发。例如,当整个页面加载完成时,window 对象会触发 load 事件.
  • error:当资源加载失败时触发。例如,如果图片无法加载,img 元素会触发 error 事件.

表单事件

  • reset:当表单被重置时触发。例如,当用户点击表单中的重置按钮时。
  • submit:虽然 submit 事件在某些情况下可以冒泡(例如在表单元素上),但在 form 元素上直接触发时通常不冒泡。具体行为可能因浏览器而异.

其他事件

  • beforeunload:当窗口即将卸载时触发,通常用于提示用户确认是否离开页面.
  • hashchange:当 URL 的哈希值发生变化时触发,例如在单页应用中用于路由变化.
  • resize:当窗口大小改变时触发.
  • scroll:当用户滚动页面时触发.
  • select:当用户选择文本时触发,例如在输入框中选择文本时.
  • textInput:当用户输入文本时触发,类似于 input 事件,但更具体地关注文本输入.

注意事项

  • 事件冒泡与捕获
    • 事件冒泡是事件传播的“冒泡阶段”,而事件捕获是事件传播的“捕获阶段”。
    • 虽然一些事件不冒泡,但它们仍然可以被捕捉到,通过在 addEventListener 中设置第三个参数为 true 来使用事件捕获.
    1
    2
    3
    window.addEventListener('load', function() {
    console.log('Page loaded');
    }, true); // 使用捕获阶段监听 load 事件
  • 自定义事件
    • 自定义事件可以设置是否冒泡。在创建自定义事件时,可以通过 new Eventbubbles 属性来控制其是否冒泡.
    1
    2
    const customEvent = new Event('customEvent', { bubbles: false });
    element.dispatchEvent(customEvent);

    了解哪些事件不冒泡有助于更好地设计事件处理策略,特别是在复杂的 DOM 结构中,避免不必要的事件传播和处理逻辑.

Vue

多个修饰符

你可以将 .stop 与其他修饰符组合使用,例如 .prevent(阻止默认行为)。

1
<button @click.stop.prevent="handleButtonClick">Click me</button>

阻止默认事件:
e.preventDefault()
e.returnValue = false  (IE)
阻止冒泡:
e.stopPropagation()
e.cancelBubble = true (IE)