JavaScript基础与核心

1,070 阅读1小时+

JavaScript基础

篇幅很长,涵盖 JavaScript 多半知识点,耐心看完一定会有很大的收获!

简介

JavaScript 是一种运行在客户端(浏览器)和服务器端(NodeJS)的解释型弱类型动态编程语言。可以用于制作网页特效、表单验证、数据交互和服务器端编程

  • 解释型:代码无需编译直接运行,需要一个翻译器,一边翻译一边执行

  • 弱类型:声明变量时无需指定类型,并在赋值或运算时数据类型可以随意转换

  • 动态语言:在代码执行的过程中可以动态添加对象的属性

JavaScript 由ECMAScriptWeb APIs两大部分组成

  • ECMAScript:JavaScript 基础语法核心知识,可以理解为官方标准版 JS 语法

  • Web APIs:包括DOM(文档对象模型)、BOM(浏览器对象模型)

image-20230504222258848.png

通常,我们通过<script>标签来引入 JavaScript 代码,这也是最理想的引入方式。当然,也可以直接写在页面的<script>标签内,或者直接写在标签内部。在项目中,我们通过模块化的方式来引入 JavaScript 代码

页面解析顺序

页面的解析顺序指的是:浏览器如何加载和处理 HTML、CSS 和 JavaScript,以及这些资源是如何影响页面渲染

详细过程如下:

  1. 解析HTML文档:浏览器首先解析HTML文档,构建DOM(文档对象模型)
  2. 解析CSS样式:包括内联样式、内部样式表(<style>)和外部样式表(<link>引入)解析 CSS 后,浏览器构建 CSSOM(CSS对象模型)表示了页面的样式信息
  3. 构建渲染树(Render Tree):将 DOM 和 CSSOM 结合起来,浏览器构建渲染树
  4. 布局(Layout)或重排(Reflow):浏览器使用渲染树来计算每个元素在页面上的大小和位置
  5. 绘制(Painting):浏览器将渲染树中的每个节点绘制到屏幕上
  6. 解析和执行JavaScript:在解析 HTML 和 CSS 的过程中,如果遇到 JS,浏览器会采取不同的策略:
    • 内部 JS:
      • 没指定asyncdefer属性:浏览器会阻塞解析 HTML,直到脚本执行完毕
      • 指定defer属性:允许 HTML 继续解析,脚本的执行则延迟到文档解析完成之后,按脚本在文档中的顺序依次执行,适用于不依赖于文档内容的初始化脚本
      • 指定async属性:脚本加载与 HTML 解析并行进行,加载完毕后,脚本会在 HTML 解析的下一个适当时间点执行,不影响后续 HTML 的解析,但可能导致脚本执行顺序不确定,适用于相互独立的脚本文件
    • 外部 JS:
      • 没指定asyncdefer属性:浏览器会暂停解析 HTML,下载 JS 文件,然后执行
      • 指定defer属性:浏览器会并行下载脚本而不阻塞 HTML 解析,所有defer脚本会在 DOM 构建完成后,按照脚本在文档中的顺序执行
      • 指定async属性,脚本异步下载,不阻碍 HTML 解析,下载完成后立即执行,可能打断 HTML 解析。此模式下,脚本的执行顺序无法保证,适合彼此之间无依赖关系的脚本
  7. 事件处理和交互:页面加载完成后,浏览器会处理用户交互,如点击、滚动等。JavaScript 可以添加事件监听器来响应这些交互

理想情况下,页面的结构和逻辑应该是分离的。HTML 负责定义页面的结构和内容,而 JavaScript 负责添加交互性和动态行为。这种分离使得代码更易于维护和重用

详细工作原理参考:浏览器工作原理

语法基础

注释

  • 单行注释(快捷键 Ctrl + /
// 单行注释  
  • 多行注释(快捷键 Shift + Alt + A
/*
   多行注释  
*/

结束符

使用英文分号;代表语句结束。在实际开发中,大多数情况下可以省略,浏览器可以自动推断语句结束位置,回车会被解析为;

少数需要手动加分号的情况有:以括号、方括号、正则开头的斜杆、加号、减号作为行首时。这种情况十分不常见,一般来说,分号可加可不加,根据团队规范或个人习惯而定

输入输出

这里的输入输出,不是其他语言中的 I/O,只单指与浏览器进行交互

// 输入
prompt('输入内容')

// 输出
document.write('要输出的内容') // 向body内输出内容,可以解析标签
document.writeln('要输出的内容')
alert('要输出的内容') // 页面弹出警告对话框
console.log('要输出的内容') // 控制台打印

alert()prompt() 它们会跳过页面渲染先被执行

字面量

字面量是指在代码中直接表示值的特殊语法结构。字面量是 JavaScript 中创建数据的一种简单方式,它允许你直接在代码中定义一些基本的数据结构,而不是通过调用函数或构造器。如:

  • 3000:数字字面量
  • 'hello world': 字符串字面量
  • true: 布尔字面量
  • {}:对象字面量
  • []:数组字面量
  • /ab+c/i:正则表达式字面量
  • function fn() {}() => {}:函数字面量

字面量永远不能出现在等号的左边,等号的左边只能是变量

变量

JavaScript 中,变量指的是一个可以改变的量。可以把变量看成存储数据的容器,本质上是程序在内存中申请的一块用来存放数据的小空间

image-20240115195407655.png

变量名只能包含字母、数字、下划线、$,且不能以数字开头,此外不能使用关键字且严格区分大小写。在命名变量的过程中,要尽可能的让变量具有意义,一个变量包含多个单词时,使用小驼峰命名法(如userName

与很多语言一样,JS 也可以同时声明多个变量并赋值,不过 JS 具有变量没有类型而值有类型的特点(弱类型)声明变量使用let、var,无需声明类型

var a // es6之后不推荐使用var
let b

// 可以多变量声明,一般都是分开声明
let age = 18,uname = 'moss'

其他变量相关知识点,如变量提升、块级作用域等,见JavaScript进阶部分

常量

使用const声明的变量称为常量,当某个变量永远不会改变的时候,就可以使用const来声明

  • 常量无法修改,强制修改会报错

  • 常量必须要初始化,定义的时候必须要进行赋值

  • 对于值常量(固定值),常量名推荐全大写,单词间以_分割,如:UPPER_SNAKE_CASE

  • const 声明并不会真的保护数据不被改变。 为了确保数据不被改变,JavaScript 提供了一个函数 Object.freeze,任何更改对象的尝试都将被拒绝,如果脚本在严格模式下运行,将抛出错误

"use strict"
const obj = {
  name: "FreeCodeCamp",
  review: "Awesome"
};
Object.freeze(obj);
obj.review = "bad"; // 报错
obj.newProp = "Test"; // 报错
console.log(obj); // { name: "FreeCodeCamp", review: "Awesome" }

严格模式

严格模式可以帮开发者及早发现错误,使代码更安全规范,推荐在代码中一直保持严格模式运行

基本差异

  • 变量必须使用关键词声明,未声明的变量不允许赋值
"use strict"
url = 'www.baidu.com' // url is not defined
  • 关键词不允许做变量使用
"use strict"
var public = 'hello' // Unexpected strict mode reserved word
  • 变量参数不允许重复定义
"use strict"
function fn(name, name) {}
  • 可以单独为函数设置严格模式
function strict(){
  "use strict"
  return "严格模式"
}
function notStrict() {
  return "正常模式"
}
  • 为了在多文件合并时,防止全局设置严格模式对其他没使用严格模式文件的影响,将脚本放在一个执行函数中
(function () {
  "use strict"
  function fn(name, name) {}
})();

解构差异

  • 如果解构赋值中的变量未事先声明,会抛出ReferenceError
// 严格模式
'use strict';
const [a, b] = [1, 2] // 如果没有声明过a和b,会抛出ReferenceError
  • 在函数参数中,解构赋值不会影响外层作用域的同名变量
// 严格模式
function example({x}) {
  x = 2 // 这里会抛出TypeError,因为参数不能被赋值
}

流程控制

在JavaScript中,共有三种流程控制方式:顺序结构、选择结构、循环结构

  • 顺序结构:代码按照从上到下、从左到右的顺序执行

image-20230510070920011.png

  • 选择结构:根据条件判断来决定使用哪一段代码。包括单向选择、双向选择、多向选择。无论是哪一种,都只执行其中的一个分支

image-20230510071223590.png

  • 循环结构:根据条件来判断是否重复执行某一段程序。若条件为 true,则继续循环;若条件为 false,则退出循环

image-20230510071314677.png

分支语句

if分支语句

  • if语句分为:单分支、双分支、多分支
  • 括号内的结果不是布尔类型时,会发生隐式类型转换,转换为布尔值
  • if语句中只有一行时,可以省略{}
// 单分支
if (条件) {
  // 满足条件要执行的代码
}

// 双分支
if (条件) {
  // 满足条件要执行的代码
} else {
  // 不满足条件要执行的代码
}

// 多分支
if (条件1) {
  // 代码1
} else if (条件2) {
  // 代码2       
} else if (条件3) {
  // 代码3
} else {
  // 代码n
}
// 先判断条件1,若满足条件1就执行代码1,其他不执行
// 若条件1不满足则向下判断条件2,满足条件2执行代码2,其他不执行
// 若依然不满足继续往下判断,依次类推
// 若以上条件都不满足,执行else里的代码n

Switch语句

  • 找到跟括号里数据全等的 case 值,并执行里面对应的代码,若没有全等 === 的则执行default内的代码
  • switch case语句一般用于等值判断,不适合于区间判断
  • switch case一般需要配合break关键字使用,没有break会造成case穿透
switch (数据) {
  case1:
    代码1
    break;
  case2:
    代码2
    break;
  default:
    代码n
    break;
}

循环语句

while循环

// 符合条件,一直循环,否则跳出循环体
while (循环条件) {
  要重复执行的代码(循环体)
}

while 循环三要素:

  • 循环变量初始化
  • 循环条件(判断条件是否成立,成立执行循环体代码,不成立,循环结束)
  • 变量更新(自增或自减)
//变量初始化
let i = 1
//循环条件
while (i <= 3) {
   document.write(`我会循环三次<br>`)
   //变量更新
   i++ 
}

do...while 循环:不论什么情况,都会执行 do循环里的代码,然后当 while条件被评估为 true的时,继续运行循环

for循环

  • 初始化语句只会在执行循环开始之前执行一次,通常用于定义和设置循环变量
  • 循环条件语句会在每一轮循环的开始前执行,条件为 true时执行循环。这意味着,如果条件在一开始就为 false,这个循环将不会执行
  • 终止循环表达式在每次循环迭代结束, 在下一个条件检查之前时执行,通常用来递增或递减循环计数
for (初始化语句; 循环条件语句; 终止循环表达式) {
  //循环体
}

循环退出:continue(中止本次循环,开始下次循环)、break(中止整个循环)

无限循环:while(true)for(;;)。都需使用break退出循环

异常处理

异常处理是指预估代码执行过程中可能发生的错误,最大程度避免错误的发生

主要包含三个部分:

  • throw:抛出异常信息,程序终止执行用于在try中手动抛出错误,立即触发对应的catch
  • Error:内置对象,配合throw使用,能够设置更详细的错误信息
  • try...catch...finally:捕获错误信息。try代码块为可能发生错误的代码、catch代码块对应错误发生后执行的代码、finally代码块为最终且一定执行的代码
function divide(a, b) {
  try {
    // 可能发生错误的代码
    if (b === 0) {
      // 如果除数为0,抛出异常
      throw new Error("Division by zero is not allowed.")
    }
    return a / b
  } catch (e) {
    // 捕获错误信息并处理
    console.error("An error occurred: ", e.message)
    // 可以选择返回一个默认值或执行其他错误恢复操作
    return null
  } finally {
    // 无论是否发生错误,都会执行的代码
    console.log("Execution of the divide function is complete.")
  }
}

// 使用这个函数
var result = divide(10, 2) // 正常情况,期望得到 5
console.log(result) // 输出: 5

result = divide(10, 0) // 异常情况,除数为0
console.log(result) // 输出: null

try...catch 并不是异步操作,不影响 JavaScript 的单线程执行模型。异步操作中使用 try...catch 需要注意错误可能在Promise.then().catch()中被捕获,或在使用async/await时通过try...catch包裹异步函数内部的代码来捕获错误

可以用debugger来打断点调试,不过一般用的比较少,了解即可

基本数据类型

类型简介

JS 数据类型整体分为两大类:基本数据类型、引用数据类型

  • 基本数据类型:
    • number 数字型
    • string 字符串型
    • boolean 布尔型
    • symbol 标志型(不常用)
    • undefined 未定义型
    • null 空类型
  • 引用数据类型:
    • Object
    • Function
    • Array

image-20230531221423577.png

引用数据类型还有:DateRegExpMapSetErrorPromise

基本数据类型与引用数据类型在内存中的存储不同(操作系统层面):

  • 栈: 访问速度快,基本数据类型存放到
  • 堆: 存储容量大,引用数据类型存放到

image-20240507152733359.png

数字型

数字是最基本的数据类型,JavaScript 中的数字型不区分整型和浮点型。JS 是弱数据类型,变量类型只有在赋值之后,才能确认

  • 数学运算符也叫算术运算符,主要包括加、减、乘、除、取余(求模)
  • +:求和、 -:求差、*:求积、/:求商、 %:取模(取余数)

数字包装类型

number 有专门的构造函数Number,称为包装类型,除了字面量初始化数字型变量,也可以使用Number()构造函数

let num1 = 99
let num2 = new Number(88) // 这里是对象

一般使用前者较多,num1照样可以调用Number构造函数原型链上的方法,这个过程称为自动装箱,如:

const num = 99
const result = num.toFixed(2)
console.log(result) // "99.00"

在这个示例中,尽管num是一个原始的数字类型,你仍然可以调用toFixed()方法。这是因为:

  • JavaScript 引擎为数字 99 创建了一个Number包装对象
  • 在这个Number对象上调用了toFixed(2)方法
  • toFixed()方法返回了一个字符串 "99.00",该字符串被赋值给了变量 result
  • Number包装对象被销毁

包装类型是临时的,它们只在需要的时候被创建,一旦方法调用结束就会被销毁

NaN

NaN代表非数字(Not-a-Number)是一个特殊的值,用于表示一个操作数或操作数的结果不是合法数字。NaN 是粘性的,任何对 NaN 的操作都会返回 NaN

以下是一些可能导致NaN的情况:

  • 无效的算术操作: 当对一个或多个非数字值进行算术运算时,结果会是NaN
"hello" + 10 // NaN
Number({}) * 10 // NaN
  • 使用NaN作为操作数: 任何涉及NaN的操作都会产生NaN
10 * NaN // NaN
  • 错误的类型转换: 尝试将非数字字符串转换为数字,如果字符串格式不正确,会得到NaN
Number("123abc") // NaN
  • 科学计数法表示的过大数值: 当数值过大,超出了 JS 能表示的数字范围时,可能会返回NaN
Number("1e500") // NaN

由于NaN与任何值都不相等(包括自己)不能使用=====来检测NaN,正确的方式是使用全局的isNaN()函数

isNaN("hello") // true
isNaN(123) // false
isNaN(0 / 0) // true

字符串型

通过单引号 (' ')、双引号 (" ") 或反引号 ( `` ) 包裹的数据都叫字符串,JavaScript 中的单引号和双引号没有本质上的区别,推荐使用单引号

特点如下:

  • 单引号与双引号可以互相嵌套,但是不能自已嵌套自已,必要时可以使用转义符\来输出单引号或双引号
  • 通过 + 运算符,可以实现字符串的拼接
  • 字符串的值是不可变的,这意味着一旦字符串被创建就不能被改变,不过字符串是可以整体赋新值
  • 字符串取单字符可以使用charAt()[]

模板字符串

反引号 (``) 包裹,又称为模板字符串,用于拼接字符串和变量(ES6新增),内容拼接变量时用${}包住变量

  • 模板字符串可以直接包含换行符
const text = `
  Hello,
  How are you?
  Hope you are doing great.
`
  • 表达式可以嵌入到模板字符串中,表达式会在运行的过程中求值
const a = 5
const b = 10
const result = `The sum of ${a} and ${b} is ${a + b}.`
console.log(result) // The sum of 5 and 10 is 15.

标签函数

标签函数是一个特殊的函数调用形式,主要用于处理模板字符串。当在模板字符串之前放置一个函数名时,这个函数就会作为标签函数被调用

// 第一个参数是数组,包含了模板字符串中所有静态的部分(即不包含插值表达式的部分)
// 从第二个参数开始,是模板字符串中所有插值表达式求值后的值
function capitalize(strings, ...values) {
  return strings.map((str, i) => 
    str + (i < values.length ? ` ${values[i].charAt(0).toUpperCase() + values[i].slice(1)}` : '')
  ).join('')
}
const world = 'world'
const greeting = capitalize`Hello, ${world}`
console.log(greeting) // Hello, World
  • 模板字符串的.raw属性提供了模板字符串的原始形式,它将转义序列视为普通文本
const html = String.raw`<div>\n  <p>Hello, World!</p>\n</div>`
console.log(html) // <div>\n  <p>Hello, World!</p>\n</div>

字符串包装类型

string类型也有其对应的包装类型String,这也是为什么string类型能使用字符串方法的原因,同样涉及到自动装箱,与number类型类似

其属性及方法主要有:

  • length:获取字符串的长度
  • split('分隔符'):将字符串拆分成数组
  • substring(需要截取的第一个字符的索引[,结束的索引号]):字符串截取
  • startsWith(检测字符串[,检测位置索引号]):检测是否以某字符开头
  • endsWith(搜索的字符串[,检测终止位置索引号]):检测是否以某字符结尾
  • includes(搜索的字符串[,检测位置索引号]):判断一个字符串是否包含在另一个字符串中
  • trim():字符串的两端清除空格,返回一个新的字符串,而不修改原始字符串
  • toUpperCase()用于将字母转换成大写
  • toLowerCase():用于将就转换成小写
  • indexOf():检测是否包含某字符
  • replace(regexp):替换字符串,支持正则匹配
  • match(regexp):查找字符串,支持正则匹配

symbol

Symbol的值是唯一的,独一无二的,主要用于防止属性名冲突,如向第三方对象中添加属性

const symbol1 = Symbol()
const symbol2 = Symbol(42)
const symbol3 = Symbol('foo')
console.log(typeof symbol1) // "symbol"
console.log(symbol2 === 42) // false
console.log(symbol3.toString()) // "Symbol(foo)"
console.log(Symbol('foo') === Symbol('foo')) // false
  • 可以使用description获取传入的描述参数
const symbol = Symbol('foo')
console.log(symbol.description) // "foo"

for、keyFor

Symbol.for(key):接受一个字符串作为参数,并搜索一个具有该字符串描述的全局 Symbol 注册表

  • 如果找到了,就返回这个 Symbol
  • 如果没有找到,就创建一个新的 Symbol 并将其添加到全局注册表中,然后返回这个新创建的 Symbol

这意味着,当你使用相同的字符串作为参数调用 Symbol.for 时,你总是会得到相同的 Symbol 对象

let mySymbol = Symbol.for('mySymbol')
let anotherMySymbol = Symbol.for('mySymbol')
console.log(mySymbol === anotherMySymbol) // true

Symbol.keyFor(sym):接受一个 Symbol 对象作为参数,并返回该 Symbol 对象的全局描述字符串

  • 如果提供的不是 Symbol 对象,或者该 Symbol 对象没有在全局注册表中创建,则返回 undefined

这个方法通常用于反向查找 Symbol 对应的字符串键

let mySymbol = Symbol.for('mySymbol')
let anotherMySymbol = Symbol.for('mySymbol')

console.log(Symbol.keyFor(mySymbol)) // 'mySymbol'
console.log(Symbol.keyFor(anotherMySymbol)) // 'mySymbol'

遍历Symbol

Symbol 不能使用 for-infor-of 遍历,可以使用 Object.getOwnPropertySymbols(obj)Reflect.ownKeys(obj)获取所有Symbol属性。此外,这也意味着,如果对象属性不想被遍历,可以使用Symbol来保护

const myObject = {
  // 普通属性
  visibleProperty: 'This is visible',
  // 使用 Symbol 创建的属性
  [Symbol('hiddenProperty1')]: 'This is hidden1',
  [Symbol('hiddenProperty2')]: 'This is hidden2'
}


for (const key in myObject) {
  console.log(key + ' = ' + myObject[key])
}
// 只输出: visibleProperty = This is visible

for (const value of Object.values(myObject)) {
  console.log(value)
}
// 只输出: This is visible

const symbols = Object.getOwnPropertySymbols(myObject)
console.log(symbols) // [ Symbol(hiddenProperty1), Symbol(hiddenProperty2) ]

// 打印 Symbol 属性的值
symbols.forEach(sym => {
  console.log(sym.description + ' = ' + myObject[sym])
})
// 输出: hiddenProperty1 = This is hidden1
// 输出: hiddenProperty2 = This is hidden2

布尔类型

布尔类型只有两个固定的值:truefalse。与string、number类似,boolean也有包装类型Boolean

未定义与空类型

undefinednull都是表示没有值的特殊值,但它们的含义和用途不同:

undefined:

  • 当声明一个变量但未赋值则默认值为undefined
  • 函数没有返回值时,其返回值也是undefined
  • typeof undefined的结果是"undefined"

null:

  • null表示赋予变量空值,用来表示一个变量希望被赋予一个空或者不存在的值
  • null常用于初始化一个变量,表示它在逻辑上是一个空或者无效的引用
  • typeof null的结果是"object"(历史遗留问题,不建议依赖这个特性)

不同点:

  • undefined == null(true)、undefined === null(false)
  • undefined强转数字类型为NaNnull强制数字类型为0
  • undefined通常用于表示变量已声明但尚未初始化
  • null通常用于表示变量应该引用一个不存在的或者为空的对象

数据类型检测

通过typeof关键字检测数据类型,返回表示数据类型的字符串

typeof用法格式返回的字符串的值含义
typeof xundefined该值未定义
typeof(x)boolean该值为布尔类型
string该值为字符串类型
number该值为数值类型
object该值为对象(引用数据类型)或者 null
function该值为函数类型

运算符与表达式

表达式

表达式指的是由运算符组成的式子,JS 引擎会将其计算出一个结果

//表达式一定会有运算结果
console.log(1 + 2)
let num = 1 + 2

运算符

算数运算与赋值运算符
算数运算符含义赋值运算符含义
+求和=直接赋值
-求差+=a = a + b
*求积*=a = a * b
/求商/=a = a / b
%取模(取余数)%=a = a % b
连接符(+)

用于字符串的拼接,+号只要遇到字符串,就是连接符(数字相加,字符相连)

document.write('Hello' + 'World') // HelloWorld
let first = 'Hello'
let second = 'World'
document.write(first + second) // HelloWorld
一元/三元运算符

一元运算符:++(自增)、-(自减)

  • 前置
let num = 1;
console.log(++num + 1 + ++num);//6
console.log(num);//3
  • 后置
let num = 1;
console.log(num++ + 1 + num++);//4
console.log(num);//3

三元运算符:条件 ? 满足条件执行的代码 : 不满足条件执行的代码

// 数字补零
num = num < 10 ? '0' + num : num
比较运算符
比较运算符含义
>左边是否大于右边
<左边是否小于右边
>=左边是否大于或等于右边
<=左边是否小于或等于右边
==左右两边值是否相等(强转)
===左右两边是否类型和值都相等(无强转)
!==左右两边是否不全等
逻辑运算符
符号名称特点
&&逻辑与符号两边都为true结果才为true
||逻辑或符号两边有一个为truetrue
!逻辑非取反

逻辑与、逻辑或可以作为短路运算符来使用:

  • 逻辑中断:存在于逻辑运算符&&||中,左边如果满足一定条件会中断代码执行,也称为逻辑短路
  • 当逻辑表达式结果不是布尔值时,会隐式转换为布尔值,但是最后运算结果,返回的不是布尔值,而是进行隐式转换之前的结果

逻辑运算符使用规则如下:

  • false && anything&&左边结果为false则中断, 否则返回右边代码的值
  • true || anything||左边结果为true则中断,否则返回右边代码的值
  • &&常常用来代替if(flag){}代码块
console.log(1 || 2) // 1,或运算,左边为true,逻辑中断,不用计算右边,直接输出或运算符左边的原结果,即1
console.log(1 || 0) // 1,或运算,左边为true,逻辑中断,不用计算右边,直接输出或运算符左边的原结果,即1
console.log(0 || 1) // 1,或运算,左边为false,右边计算为true,输出或运算符右边的原结果,即1
console.log(0 || 0) // 0,或运算,左边为false,右边计算为false,输出或运算符右边的原结果,即0
console.log(1 && 2) // 2,与运算,左边为true,右边计算为true,输出与运算符右边的原结果,即2
console.log(1 && 0) // 0,与运算,左边为true,右边计算为false,输出与运算符右边的原结果,即0
console.log(0 && 1) // 0,与运算,左边为false,逻辑中断,不用计算右边,直接输出与运算符左边的原结果,即0

//实际例子举例
let age = 10
console.log(true && age++) // 10,++在右边,不参与运算
console.log(true && ++age) // 12,++在左边,参与运算
console.log(age) // 12
运算符优先级
优先级运算符顺序
1小括号()
2一元运算符++--!
3算数运算符* / %+ -
4关系运算符> >=<<=
5相等运算符==!====!==
6逻辑运算符&&后 |
7赋值运算符=
8逗号运算符,

JavaScript运算符优先级详情见:JavaScript运算符优先级

类型转换

JavaScript 是弱数据类型,只有变量赋值时,才确定为何种数据类型

显示转换

转为数字型
方法含义
Number(数据)返回数字类型。字符串有非数字则为NaNnull转换为0undefinedNaN
parseInt(数据)只保留整数。如果为数字开头字符串则保留整数数字,非数字开头返回NaN
parseFloat(数据)可以保留小数。如果数字开头的字符串,可以保留小数

parseInt(string, radix)详解:

参数说明
string要解析的字符串
radix1) 可选。表示要解析的数字的基数。该值介于2 ~ 36之间

2) 如果省略该参数或其值为 0,则数字将以10为基础来解析。如果它以0x0X开头,将以16为基数;以1 ~ 9的数字开头,parseInt() 将把它解析为十进制的整数

3) 如果该参数< 2 or> 36,则 parseInt() 返回NaN

示例如下:

parseInt('689090',8)  //6  只解析小于8的
parseInt('8f89090',8)  //NaN  8不在8进制范围内
parseInt('8000',0)    //8000 以10为基数解析

//经典案例
["1", "2", "3"].map(parseInt) // [1, NaN, NaN]
//其实就是["1", "2", "3"].map((item, index)=>{return parseInt(item, index)})
转为字符型
方法含义
String(数据)返回字符串类型
toString(进制数)可以有进制转换,返回字符串类型
转为布尔型
  • Boolean(数据):返回true或者false
  • 空字符、0、-0、undefined、null、false、NaN转换为布尔值后为false,其余为true

隐式转换

某些运算符被执行时,系统内部自动将数据类型进行转换,这种转换称为隐式转换,如:

  • + 号作为运算符时,两边只要有一个是字符串,都会把另外一个转成字符串
  • 除了+以外的算术运算符,如 - * / 等都会把数据转成数字类型
  • +号作为正号解析可以转换成数字型
  • 逻辑非 ! 转换为布尔类型

JS 隐式转换详情见:JavaScript中的隐式转换

数组

定义

数组(Array)是一种可以按顺序保存数据的数据类型,每个数组是Array的实例对象,所以能直接调用其方法

创建数组有三种方法:构造函数、Array静态方法、字面量

// 字面量
const arr = [1, 2, 3, 4, 5]

// 构造函数
const arr = new Array (1, 2, 3, 4, 5) // [1, 2, 3, 4, 5]
const arr = new Array (5) // 长度为5的空数组

// Array 静态方法
const arr = Array.of(1, 2, 3) // [1, 2, 3]

多维数组定义

// 使用 fill + map,实现多维数组初始化
const arr = new Array(6).fill([]).map(() => [])
// 字面量定义多维数组
const arr = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9],
  [[10, 11, 12], 13, 14]
]
// 多维数组取值
const subarray = arr[3] // [[10, 11, 12], 13, 14]
const nestedSubarray = arr[3][0] // [10, 11, 12]
const element = arr[3][0][1] // 11

数组遍历

JavaScript 遍历数组的方法有很多,以下为常用的方法:

  • for循环
const array = [1, 2, 3, 4, 5]
for (let i = 0; i < array.length; i++) {
  console.log(array[i])
}
  • for...in循环
const array = [1, 2, 3, 4, 5]
for (let key in array) {
  console.log(array[key])
}
  • for...of循环
const array = [1, 2, 3, 4, 5]
for (let item of array) {
  console.log(item)
}
  • forEach()
const array = [1, 2, 3, 4, 5]
array.forEach(function(item) {
  console.log(item)
})

数组方法

更多数组方法细节归纳查看JavaScript对象常用方法

六大核心

  • push(ele1, ele2, ...):将一个或多个元素添加到数组的末尾,返回该数组的新长度(修改原数组)

  • unshift(ele1, ele2, ...):将一个或多个元素添加到数组的开头,返回该数组的新长度(修改原数组)

  • pop():从数组中删除最后一个元素,返回删除的元素(修改原数组)

  • shift():从数组中删除第一个元素,返回删除的元素(修改原数组)

  • splice()删除指定元素,也可以添加指定元素(修改原数组)

// arr.splice(起始位置, 删除几个元素)
arr.splice(start, deleteCount)

// arr.splice(起始位置, 删除几个元素, 替换删除的元素)
arr.splice(start, deleteCount, item1, item2)
  • sort():对原数组进行排序,改变原数组
const arr = [88, 78, 100, 34, 99]
// 不写参数, 会把数组元素当做字符串来排, 按ASCII码进行排序
arr.sort() // [100, 34, 78, 88, 99]

// 升序排序
arr.sort(function (a, b) {
  return a - b
})
console.log(arr)  // [34, 78, 88, 99, 100]

// 降序排序
arr.sort(function (a, b) {
  return b - a
})
console.log(arr)  // [100, 99, 88, 78, 34]

静态方法

  • Array.of():创建数组
const arr = Array.of(1, 2, 3) // [1, 2, 3]
  • Array.from():类数组转换为数组,类数组指包含 length 属性或可迭代的对象
let str = 'hello'
Array.from(str) // [ 'h', 'e', 'l', 'l', 'o' ]

const arr = {
  0: 'hello',
  '1': 'world',
  length: 2
}
Array.from(arr) // [ 'hello', 'world' ]
  • Array.isArray():检测变量是否为数组类型
Array.isArray([1, 2, 3]) // true
Array.isArray(9) // false

实例方法

  • map():数组重新映射,返回渲染后的新数组
  • filter:数组过滤,返回过滤后的新数组
  • reduce:数组元素迭代变化(可累加、可保留上一层遍历的结果),自定义返回值
  • slice:数组截取,取值可为负数,返回截取后的新数组
  • concat:数组合并 (连接),返回新数组

迭代器方法

  • keys():通过迭代对象获取索引,返回迭代器对象
const array = [1, 2, 3, 4, 5]
for (let key of array.keys()) {
  console.log(array[key])
}
  • values():通过迭代对象获取值,返回迭代器对象
const array = [1, 2, 3, 4, 5]
for (let value of array.values()) {
  console.log(value)
}
  • entries():返回数组所有键值对,返回迭代器对象
const array = [1, 2, 3, 4, 5]
for (let [index, item] of array.entries()) {
  console.log(item)
}

数组解构

数组解构是将数组的单元值快速批量赋值给一系列变量的简洁语法

  • 赋值运算符左侧的[]用于批量声明变量,右侧数组的单元值将被赋值给左侧的变量

  • 变量的顺序对应数组单元值的位置,依次进行赋值操作

let [first, second] = ['hello', 'world']
console.log(first, second) // 'hello' 'world'

// 可以使用逗号分隔符,来获取任意一个想要的值
const [a, b, , , c] = [1, 2, 3, 4, 5, 6]
console.log(a, b, c) // 1 2 5
  • 使用展开运算符 (...) 解构
const [a, b, ...arr] = [1, 2, 3, 4, 5, 7]
console.log(a, b) // 1 2
console.log(arr) // [3, 4, 5, 7]

//省略前面两参数
const [, , ...arr] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
console.log(arr) // [3, 4, 5, 6, 7, 8, 9, 10]
  • 可以利用这一特性来交换变量
let a = 5, b = 8;
[a, b] = [b, a];

JS中两个特殊情况需要加分号

  • 小括号开头
;(function(){})()
;(function(){})()
  • 中括号开头
let a = 5, b = 8; // 此处必须加分号
[a, b] = [b, a]

分号加到上一行的末尾或者本行的开头均可

函数

定义

function是被设计为执行特定任务的代码块

  • 函数像其他正常值一样,可以赋值给变量、传递给另一个函数,或从其它函数返回,这种函数叫做头等函数。 在 JavaScript 中,所有函数都是头等函数(first class
  • 将函数作为参数或返回值的函数叫作高阶函数
  • 作为参数的函数称为回调函数
  • 在函数中返回一个对象的函数称为工厂函数

一个完整的函数由function关键字函数名参数列表函数体组成,示例如下:

function sum(a, b) {
  return a + b
}

使用函数时,直接通过函数名()来调用,声明的函数必须调用才能真正被执行

sum(1, 2) // 3

函数使用中涉及到的参数有两种:

  • 形参:声明函数时括号内的参数
  • 实参:调用函数时括号内的参数

函数形参中,还涉及参数默认值的概念:即不传值时,参数默认有值而非undefined

// es6新增
function sum(x = 0, y = 0) {
    return x + y
}
// es5
function sum(x, y) {
    x = x || 0
    y = y || 0
    return x + y
}

函数体中,由return关键字来返回结果(函数会立即中断执行),返回值包括三种:

  • 正常值
  • 函数
  • undefined(无return关键字)
// 返回正常值
function add(a, b) {
  return a + b // 返回正常值数据,即两个数的和
}

// 返回函数
function createAdder(x) {
  return function(y) {
    return x + y // 返回另一个函数,该函数接受一个参数并返回与x的和
  }
}

// 无return关键字, 默认返回undefined
function doSomething() {
  // 函数体中没有 return 语句
  console.log('Doing something...')
}

aguments与rest

函数形参又分为两种:动态参数剩余参数

动态参数(arguments

  • arguments是一个伪数组,只存在于函数中(不包括箭头函数)
  • arguments用于动态获取函数的实参
function sum() {
  let sum = 0
  for (let i = 0; i < arguments.length; i++) {
    sum += arguments[i]
  }
  return sum
}
// arguments只接受多个参数,其他传参方法无效
console.log(sum(1, 4, 7, 2, 5, 8, 3, 6, 9)) // 45

剩余参数(rest

  • 剩余参数允许我们将一个不定数量的参数表示为一个数组

  • ...是语法符号,置于最末函数形参之前,用于获取剩余的实参,是一个真数组

function getSum(...other) {
  console.log(other) // [1,2,3]
}
getSum(1, 2, 3)

// 拓展写法
function getSum(a, b, ...other) {
  console.log(a) // 1
  console.log(b) // 2
  console.log(other) // [3, 4, 5, 6]
}
getSum(1, 2, 3, 4, 5, 6)

length与name

在 JavaScript 中,函数实例也有一个length属性,返回函数的参数长度,即函数定义中正式参数的数量

function greet(name, greeting) {
  console.log(greeting + ', ' + name + '!')
}

console.log(greet.length) // 2

有几个特殊情况:

  • 宿主对象:在某些情况下,如使用浏览器的宿主对象(如DOM元素的事件处理函数),length属性可能不会按照预期工作,因为这些对象不是通过 JavaScript 函数构造的
  • 默认参数:如果函数参数中有默认值,length属性将不包括那些参数
function greet(name, greeting = 'Hello') {
  console.log(greeting + ', ' + name + '!')
}
// length不包括有默认值的参数
console.log(greet.length) // 1 
  • 剩余参数:如果函数使用了剩余参数,length属性将不包括这个参数
function greetAll(...names) {
  names.forEach(name => console.log('Hello, ' + name + '!'))
}

console.log(greetAll.length) // 0

函数的name属性 (只读) 表示函数在创建时指定的名称。如果函数是匿名函数,则名称可以是 anonymous''(空字符串)

const func1 = function () { }

const object = {
  func2: function () { },
}

console.log(func1.name) // "func1"
console.log(object.func2.name) // "func2"

函数作用域

函数作用域是指在函数内部定义的变量(包括函数参数和内部声明的变量)只能在这个函数内部访问和使用。当函数执行完毕,这些变量将被销毁,并且无法从函数外部访问

  • 函数内变量访问原则:在能够访问到的情况下先局部, 局部没有再找全局(就近原则)
  • 如果函数内部,变量没有声明,直接赋值,则为定义为全局变量

函数作用域结合实际例子更好理解(后文有更详细的作用域介绍)

  • 先访问局部,再访问全局
let a = 1
function fn1() {
  let a = 2
  let b = '22'
  fn2()
  function fn2() {
    let a = 3
    fn3()
    function fn3() {
      let a = 4
      console.log(a) //4
      console.log(b) //'22'
    }
  }
}
fn1()
  • 函数内部,变量没有声明,直接赋值,则为全局变量
let num = 1
console.log(num)  // 1
function fn(a) {
  console.log(num)  // 3
  num = 2 // 给全局变量num赋值为2
  console.log(num)  // 2
}
num = 3
console.log(num)  // 3
fn(num)
console.log(num)  // 2

匿名函数

匿名函数指的是没有具体名字的函数。匿名函数使用方式分为两种:函数表达式、立即执行函数

  • 函数表达式
let fn = function () {
  // 函数体
}
fn() // 调用
  • 立即执行函数

    • 避免全局变量之间的污染,无需调用,立即执行
    • 通过定义一个匿名函数,创建了一个新的函数作用域,相当于创建了一个私有的命名空间,该命名空间的变量和方法,不会破坏污染全局的命名空间
//形式一
(function () { console.log(11) })();
//形式二
(function () { console.log(11) }()); 

箭头函数

箭头函数是更简短的函数写法并且不绑定this,箭头函数的语法比函数表达式更简洁

  • 箭头函数更适用于那些本来需要匿名函数的地方
  • 箭头函数没有arguments动态参数,但有剩余参数...args
const getMousePosition = (x, y) => ({ x, y }) // 意思是 x:x, y:y

基本语法

  • 基本写法
const fn = () => {
    // 函数体
}
  • 只有一个参数可以省略小括号
array.forEach(item => {
    // 函数体
})
  • 若函数体只有一行代码,可以写到一行上,默认带有return
array.forEach(item => console.log(item))
  • 语法加括号的函数体返回对象字面量表达式
const fn = () => ({name: 'zs', age: 18})

箭头函数this

  • 普通函数的this指向调用者
  • 箭头函数不会创建自己的this,它只会从自己的作用域链的上一层沿用this
  • 使用箭头函数前,需要考虑函数中this指向。在回调函数使用箭头函数时,this为全局的window,因此 DOM 事件回调函数不推荐使用箭头函数
// 示例一
const obj = {
  name: 'seven',
  age: 18,
  sayHi: function () {
    console.log(this) // obj
    const fn = () => {
      console.log(this) // obj
    }
    fn()
  }
}
obj.sayHi()

// 示例二
btn.addEventListener('click', () => {
  console.log(this) // window
})

// 示例三
btn.addEventListener('click', function () {
  setTimeout(() => {
    this.style.color = 'yellow' // btn
  })
})

对象

定义

对象可以用来描述具体的某个事物,本质上是键值对的集合,一系列被映射到唯一标识符的数据就是对象。习惯上,唯一标识符叫做属性(property)或者键(key),数据叫做值(value

JavaScript中的所有引用类型都可以被认为是“对象”(底层是对象或者基于对象)

  • 对象声明
const obj = {}
const obj = new Object()
  • 一个完整的对象由属性和方法组成
// 传统写法
const obj = {
  uname: '张三',
  age: 18,
  gender: '男',
  sayHi: function () {
    console.log('hi~')
  }
}

// es6简写
const uname = 'andy'
const age = 18
const gender = '女'
const obj = {
  uname, // 此处的uname等同于uname:uname(同名可以简写)
  age,
  gender,
  sayHi() {
    console.log('hi~')
  }
}

如果你的对象有非字符串属性的话,JavaScript 会自动将它们转为字符串。可以说,对象内部的属性名都是字符串

对象使用

image-20240605161319852.png

声明对象,并添加了若干属性后,可以使用.[]获得对象中属性对应的值,称之为属性访问

  • .语法:获取属性对应的值,后接属性名,不可以为变量。属性名为数字时不可访问
  • []语法:获取属性对应的值,可以解析变量,属性名为数字时也可以访问,还可以访问特殊属性名
const fakeArr = {
  0: 'apple',
  1: 'banana',
  2: 'orange'
}

console.log(fakeArr[0]) // apple
console.log(fakeArr.0) // 报错

const person = {
  'un-name': '张三',
  age: 18,
  gender: '男'
}
console.log(person['un-name']) // "张三"
console.log(person.un-name) // 报错

遍历对象

对象内是无序键值对,没有下标,且没有length属性,无法确定长度

image-20240605164648923.png

对象解构

对象解构是将对象属性和方法快速批量赋值给一系列变量的简洁语法

  • 赋值运算符左侧的 {} 用于批量声明变量,右侧对象的属性值将被赋值给左侧的变量
  • 对象属性的值将被赋值给与属性名相同的变量,不要和其他变量名冲突
  • 对象中找不到与变量名一致的属性时变量值为undefined
  • 解构的变量可以重新命名
const user = {
  uname: '小明',
  age: 18
}
const { name: uname, age } = user // 对象内的属性 : 要赋值的变量名
  • 多级对象解构
const pig = {
  name: '佩奇',
  family: {
    mother: '猪妈妈',
    father: '猪爸爸',
    brother: '乔治'
  },
  age: 6
}
//按照解构依次剥离即可, 这里的family不是解构出来的
const { name, family: { mother, father, brother } } = pig
  • 如果是给事先声明的变量赋值,则赋值表达式必须包含在一对括号中,不常用了解即可
let personName, personAge
let person = {
  name: 'Matt',
  age: 27
}; // 此处的分号不能省
({ name: personName, age: personAge } = person)
console.log(personName, personAge) // Matt, 27

属性特征

查看特征

  • 使用 Object.getOwnPropertyDescriptor查看对象属性的描述
  • 使用 Object.getOwnPropertyDescriptors查看对象所有属性的描述

属性包括以下四种特性:

特性说明默认值
configurable能否使用delete、能否修改属性特性、或能否修改访问器属性true
enumerable对象属性是否可通过for-in循环,或Object.keys()读取true
writable对象属性是否可修改true
value对象属性的默认值undefined

使用示例如下:

const user = {
  name: "Jay",
  sex: "male",
  age: 18
}

console.log(Object.getOwnPropertyDescriptor(user, "name")) 
// { value: 'Jay', writable: true, enumerable: true, configurable: true }
console.log(Object.getOwnPropertyDescriptors(user))
/* 
{
  name: { value: 'Jay', writable: true, enumerable: true, configurable: true },
  sex: { value: 'male', writable: true, enumerable: true, configurable: true },
  age: { value: 18, writable: true, enumerable: true, configurable: true }
}
*/

设置特征

  • 使用Object.defineProperty 方法修改属性特性
  • 使用 Object.defineProperties 可以一次设置多个属性
const user = {
  name: "Jay",
  sex: "male",
  age: 18
}

Object.defineProperty(user, "name", {
  value: "Zhou",
  writable: false,
  enumerable: false,
  configurable: false
})

// 不允许修改
user.name = "Jane"
console.log(user.name) // Zhou

// 不能遍历
console.log(Object.keys(user)) // ['sex', 'age']

//不允许删除
delete user.name
console.log(user) // {sex: 'male', age: 18, name: 'Zhou'}

//不允许配置
Object.defineProperties(user, {
  name: { value: "Jucy", writable: false },
  age: { value: 20 }
}) // Uncaught TypeError: Cannot redefine property: name

禁止添加

  • Object.preventExtensions 禁止向对象添加属性

  • Object.isExtensible 判断是否能向对象中添加属性

const user = {
  name: "Jay",
  sex: "male"
}

Object.preventExtensions(user)
user.age = 25
console.log(user) // {name: "Jay", sex: "male"}
console.log(Object.isExtensible(user)) // false

封闭对象

  • Object.seal()方法封闭一个对象,阻止添加新属性并将所有现有属性标记为 configurable: false
  • Object.isSealed 如果对象是密封的则返回 true,属性都具有 configurable: false
const user = {
  name: "Jay",
  sex: "male"
}

Object.seal(user)
console.log(Object.isSealed(user)) // true
console.log(Object.getOwnPropertyDescriptors(user))
/*
{
  name: {
    value: 'Jay',
    writable: true,
    enumerable: true,
    configurable: false
  },
  sex: {
    value: 'male',
    writable: true,
    enumerable: true,
    configurable: false
  }
}
*/ 

冻结对象

  • Object.freeze 冻结对象后不允许添加、删除、修改属性,writable、configurable都标记为false
  • Object.isFrozen()方法判断一个对象是否被冻结
const user = {
  name: "Jay",
  sex: "male"
}

Object.freeze(user)
console.log(Object.isFrozen(user)) // true
console.log(Object.getOwnPropertyDescriptors(user))
/*
{
  name: { value: 'Jay', writable: false, enumerable: true, configurable: false },
  sex: { value: 'male', writable: false, enumerable: true, configurable: false }
}
*/ 

属性访问器

getter:get 语法将对象属性绑定到查询该属性时被调用的函数

setter:当尝试设置属性时,set 语法将对象属性绑定到要调用的函数,可在类中使用

gettersetter是 JS 提供的存取器特性,使用函数来管理属性,使用场景如下:

  • 用于避免错误的赋值
  • 需要动态监测值的改变
  • 属性只能在访问器和普通属性任选其一,不能同时存在
const obj = {
  _name: "Jay",
  // 使用set关键字定义一个setter
  set name(value) {
    if (typeof value === 'string') {
      this._name = value
    } else {
      console.log("Name must be a string.")
    }
  },
  // 使用get关键字定义一个getter
  get name() {
    return this._name
  }
}

obj.name = "Zoe" 
console.log(obj.name) // "Zoe"
// 不会改变 _name 的值
obj.name = 123 // "Name must be a string." 

也可以使用Object.defineProperty()在对象上定义gettersetter

const obj = {}

Object.defineProperty(obj, 'name', {
  // 设置属性的 getter
  get: function () {
    return this._name
  },
  // 设置属性的 setter
  set: function (value) {
    if (typeof value === 'string') {
      this._name = value
    } else {
      console.log("Name must be a string.")
    }
  },
  // 其他属性特性
  enumerable: true,
  configurable: true
})

obj.name = "Alice"
console.log(obj.name) // Alice

proxy代理

setter/getter是对单个对象属性的控制,而代理是对整个对象的控制,实现了对基本操作的拦截和自定义

ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例

const proxy = new Proxy(target, handler)
// target表示所要拦截的目标对象
// handler参数为定制拦截对象

Proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法

const user = { name: "Jay" }
const proxy = new Proxy(user, {
  get(obj, property) {
    return obj[property]
  },
  set(obj, property, value) {
    console.log('proxy调用了')
    obj[property] = value
    return true
  }
})
proxy.age = 10
console.log(user) // { name: "Jay", age: 10 }

要使Proxy起作用,必须针对Proxy实例进行操作,而不是针对目标对象进行操作

user.sex = 'male' // 直接修改了原对象,proxy不起作用

proxy具体用法参考:《ECMAScript 6 入门

Set与Map

Set

Set 类似于值唯一的数组,集合内部数据顺序以插入顺序而定。其基本使用如下图所示:

image-20240604171033960.png

数组的mapfilter方法也可以间接用于Set

let set = new Set([1, 2, 3])
set1 = new Set([...set].filter(x => (x % 2) == 0)) // Set{ 2 }
set2 = new Set([...set].map(x => x * 2)) // Set{ 2, 4, 6 }

使用 Set 可以很容易地实现并集(Union)、交集(Intersect)和差集(Difference)

let a = new Set([1, 2, 3])
let b = new Set([4, 3, 2])

// 并集
let union = new Set([...a, ...b]) // Set {1, 2, 3, 4}
// 交集
let intersect = new Set([...a].filter(x => b.has(x))) // set {2, 3}
// (a 相对于 b 的)差集
let difference = new Set([...a].filter(x => !b.has(x))) // Set {1}

WeakSet

WeakSet结构与Set类似,有几点区别:

  • WeakSet的成员只能是对象Symbol,而不能是其他类型的值
  • WeakSet中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用
  • WeakSet不可迭代,不能用迭代器方法遍历
  • WeakSet没有clear()
  • WeakSet没有size属性,也没有返回元素数量的方法,因为其大小可能会随着垃圾回收的进行而动态变化
const ws = new WeakSet()
const obj = {}
const foo = {}

ws.add(window)
ws.add(obj)

ws.has(window) // true
ws.has(foo) // false

ws.delete(window) // true
ws.has(window) // false

ws.size // undefined
ws.forEach // undefined
ws.forEach(function(item){ console.log('WeakSet has ' + item)})
// TypeError: undefined is not a function

Map

JavaScript 的对象只能用字符串当作键,使用上有一定的限制。为解决该问题,ES6 提供了 Map 数据结构

Map 类似于对象,也是键值对的集合,但是键的范围不限于字符串,各种类型的值(包括对象)都可以当作键。与 Set 一样,Map 的遍历顺序也是插入顺序。其基本使用如下图所示:

image-20240605141446403.png

const map = new Map([
  ['F', 'no'],
  ['T', 'yes'],
])

for (let key of map.keys()) {
  console.log(key)
}
// "F"
// "T"

for (let value of map.values()) {
  console.log(value)
}
// "no"
// "yes"

for (let [key, value] of map.entries()) {
  console.log(key, value)
}
// "F" "no"
// "T" "yes"

// 等同于使用map.entries()
for (let [key, value] of map) {
  console.log(key, value)
}
// "F" "no"
// "T" "yes"

WeakMap

WeakMap结构与Map结构类似,也是用于生成键值对的集合。有以下几点区别:

  • WeakMap只接受对象(null除外)和Symbol作为键名,不接受其他类型的值
  • WeakMap的键名所指向的对象,不计入垃圾回收机制,键值正常引用
  • WeakMap不可迭代,不能用迭代器方法遍历
  • WeakMap没有clear()
  • WeakMap没有size属性
  • WeakMap只有四个方法可用:get()set()has()delete()
const wm = new WeakMap()

// size、forEach、clear 方法都不存在
wm.size // undefined
wm.forEach // undefined
wm.clear // undefined

WeakMap用途如下:

let myWeakmap = new WeakMap()
// 将DOM节点存入Map
myWeakmap.set(document.getElementById('logo'),{ timesClicked: 0 })
// DOM节点删除,timesClicked自动消失
document.getElementById('logo').addEventListener('click', function () {
  let logoData = myWeakmap.get(document.getElementById('logo'))
  logoData.timesClicked++
}, false)

拷贝

浅拷贝

浅拷贝只复制对象的第一层属性。如果属性是一个基本数据类型,那么它会复制这个值;如果属性是一个引用类型,那么它只会复制引用的地址,而不是引用的对象本身

  • 拷贝对象:Object.assgin()、展开运算符{...obj}
  • 拷贝数组:Array.prototype.concat()或者[...arr],当然其他返回新数组的方法也为浅拷贝
// 对象拷贝
const obj = {
  name: 'jack',
  age: 18
}
const newObj1 = Object.assign({}, obj)
const newObj2 = { ...obj }

//数组拷贝
const arr = [1, 2, 3]
const newArr1 = [].concat(arr)
const newArr2 = [...arr]

深拷贝

深拷贝会创建一个完全独立的新对象,其中所有的属性都是原始对象属性的副本。对于引用类型的属性,深拷贝会递归地复制这些属性,直到所有的属性都被复制完毕。深拷贝实现原理是递归

  • 递归实现
//方法一
function deepCopy (newObj, oldeObj) {
  for (let k in oldeObj) {
    if (oldObj[k] instanceof Array) {
      newObj[k] = []
      deepCopy(newObj[k], oldeObj[k])
    } else if (oldObj[k] instanceof Object) {
      newObj[k] = {}
      deepCopy(newObj[k], oldeObj[k])
    } else {
      newObj[k] = oldObj[k]
    }
  }
}

//方法二
function cloneDeep (oldObj) {
  let newObj = Array.isArray(oldObj) ? [] : {}
  for (let k in oldObj) {
    if (typeof oldObj[k] !== 'object') {
      newObj[k] = oldObj[k]
    } else {
      newObj[k] = cloneDeep(oldObj[k])
    }
  }
  return newObj
}
  • lodash库中cloneDeep内部实现了深拷贝,本质就是更彻底的递归,官网地址

  • 序列化与反序列化:先JSON.stringify()序列化,再JSON.parse()反序列化

const obj = {
  uname: 'pekiy',
  age: 18,
  hobby: ['篮球', '足球'],
  family: {
    baby: 'littlePekiy'
  }
}
// 序列化时会丢弃对象中的函数和 undefined 属性 
const o = JSON.parse(JSON.stringify(obj))

定时器

间歇函数

setInterval 是 JavaScript 中用于周期性调用函数的方法,它会按照指定的时间间隔(以毫秒为单位)重复执行指定的函数

let timeId = setInterval(func[, delay, arg1, arg2, ...]) // 返回值为定时器开启的ID

定时器函数可以开启和关闭定时器

// 开启
let timerId = setInterval(function() {
    console.log('hi~~~')
}, 1000)

// 关闭
clearInterval(timerId)

延迟函数

全局的 setTimeout()方法设置一个定时器,一旦定时器到期,就会执行一个函数或指定的代码片段,仅执行一次

let timeId = setTimeout(functionRef, delay, param1, param2, /* … ,*/ paramN)
// setInterval()和setTimeout()共享同一个 ID 池

关闭延时函数

let timerId = setTimeout(functionRef, delay)
clearTimeout(timerId)

日期对象

日期在 JS 中使用十分频繁,这里拿出来单独说明

日期对象Date是用来表示日期和时间的对象,用于获取当前系统日期和时间。有四种方法可以创建Date对象:

const d = new Date()
const d = new Date(milliseconds) // 参数为毫秒
const d = new Date(dateString) // 日期字符串
const d = new Date(year, month, day, hours, minutes, seconds, milliseconds)

日期对象使用时,有几种比较常见的时间格式:

方法时间格式说明
Date()Fri May 17 2024 09:59:55 GMT+0800 (中国标准时间)常见的时间格式(非标准)
new Date()2024-05-19T16:08:33.517ZISO 8601 日期时间格式
getTime()
Date.now()
+new Date()
1716134913517时间戳,为1970年1月1日以来的毫秒数

日期对象方法:

方法作用说明
getFullYear()获得年份获取四位年份
getMonth()获得月份取值为0~11
getDate()获取月份中的每一天不同月份取值也不相同
getDay()获取星期取值为0~6
getHours()获取小时取值为0~23
getMinutes()获取分钟取值为0 ~59
getSeconds()获取秒取值为0~59
toLocaleString()返回该日期对象的字符串(包含日期和时间)2099/9/2018:30:43
toLocaleDateString()返回日期对象日期部分的字符串2099/9/20
toLocaleTimeString()返回日期对象时间部分的字符串18:30:00

JavaScrip进阶

作用域

作用域规定了变量能够被访问的范围,离开了这个范围变量便不能被访问,作用域分为:

  • 局部作用域(模块作用域、函数作用域、块级作用域)
  • 全局作用域

局部作用域

局部作用域(官方无此定义)分为函数作用域块级作用域

函数作用域:

  • 函数内部声明变量只能在函数内部访问,外部无法直接访问
  • 函数的参数也是函数内部的局部变量
  • 函数执行完毕后,函数内部的变量会被回收
function counter(x, y) {
  // 函数内部声明的变量
  const s = x + y
  console.log(s) // 18
}
counter(10, 8)
// 访问变量 s
console.log(s)// 报错

块级作用域:

  • 使用 {} 包裹的代码称为代码块。代码块内部声明的变量,外部可能无法访问
  • let、const声明的变量会产生块作用域,var 不会产生块级作用域
  • 不同代码块之间的变量无法互相访问
{
  // age 只能在该代码块中被访问
  let age = 18
  console.log(age) // 正常
}

for (let t = 1; t <= 6; t++) {
  // t 只能在该代码块中被访问
  console.log(t) // 正常
  console.log(age) // 报错
}

console.log(age) // 报错
console.log(t) // 报错

全局作用域

全局作用域(Global Scope)指的是在整个脚本范围内都可访问的变量的作用域。全局变量在任何函数或块内都可以被访问和修改,因此它们是全局可访问的

  • 全局作用域中的变量实际上是全局对象的属性。在浏览器环境中,全局对象是window,而在Node环境中,全局对象是global
  • 函数内部未使用任何关键字声明的变量为全局变量(严格模式下报错)
  • 页面中的script标签共享一个全局作用域
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>全局作用域示例</title>
</head>
<body>
  <script>
    "use strict"
    // 这是一个全局变量,它也是window对象的一个属性
    var globalVar = 'I am a global variable'  
    // 尝试在严格模式下声明一个全局变量
    anotherGlobalVar = 'This will cause an error in strict mode' // 报错
  </script>
  <script src="./externalScript.js"></script> <!-- 引入一个外部脚本文件 -->
</body>
</html>

externalScript.js代码如下

// 这个变量同样会作为全局对象window的属性
functionVar = 'This is a function variable in the global scope'

// 调用一个全局函数
function globalFunction() {
  console.log(globalVar) // 可以访问在第一个<script>标签中声明的全局变量
}

globalFunction()

作用域链

嵌套关系的作用域串联起来形成了作用域链,作用域链本质是底层的变量查找机制

  • 在函数被执行时,会优先查找当前函数作用域中查找变量
  • 如果当前作用域查找不到,则会依次逐级查找父级作用域直到全局作用域
  • 子作用域能够访问父作用域,父级作用域无法访问子级作用域
//全局作用域
let a = 1
//局部作用域
function f () {
  let a = 2
  //局部作用域
  function g () {
    a = 3
    console.log(a) //3
  }
  g()
  console.log(a) //3
}
f()
console.log(a) //1

暂时性死区

TDZ(Temporal Dead Zone)它是 ECMAScript 6(ES6)引入的一个概念,与 letconst 声明方式有关

工作机制:

  • 初始化前访问:在 letconst 声明的变量被初始化之前,对它们的访问将会导致 ReferenceError
  • 块级作用域letconst 声明的变量只在它们的声明块内有效,这意味着它们在声明它们的块的开始到结束之间是不可访问的
  • 暂时性死区:从代码块的开始到 let/const 声明执行点之间的区域被称为 TDZ。在这个区域内,尝试访问这些变量将会导致错误
function test() {
  // 在 TDZ 内,尝试访问 let 声明的变量将会导致错误
  console.log(letVar) // ReferenceError: letVar is not defined、
  // letVar 声明并初始化
  let letVar = 5
  // 在此之后,letVar 可以正常访问
  console.log(letVar) // 5
}

垃圾回收机制

垃圾回收机制(Garbage Collection)简称 GC,是一种自动管理内存的方式。JS 中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收,如果内存没有被及时回收,就会造成内存泄露(本该被释放的内存没有被及时释放)

内存的生命周期

JS 环境中分配的内存, 一般有如下生命周期:

  • 内存分配:当我们声明变量、函数、对象的时,系统会自动分配内存
  • 内存使用:读写内存,也就是使用变量、函数等
  • 内存回收:使用完毕,由垃圾回收自动回收不再使用的内存

全局变量一般不会回收,在页面关闭时才回收。局部变量会在不使用时被自动回收

垃圾回收机制算法

堆栈空间分配区别:

  • 栈:由操作系统自动分配释放函数的参数值、局部变量等
  • 堆:一般由程序员分配释放,若程序员不释放,由垃圾回收机制回收

常见的浏览器垃圾回收算法主要有两种:引用计数法标记清除法

引用计数

  • IE采用的引用计数算法,定义“内存不再使用”,看一个对象是否有指向它的引用,没有引用就回收对象
  • 过程:
    • 跟踪记录被引用的次数
    • 如果被引用了一次,那么就记录次数 1,多次引用会累加
    • 如果减少一个引用就减 1
    • 如果引用次数为 0,则释放内存

如果两个对象相互引用,尽管他们已不再使用,垃圾回收器不会进行回收,从而导致内存泄露

标记清除法

  • 现代浏览器通用的大多是基于标记清除算法的某些改进算法,总体上一致
  • 过程:
    • 标记清除法将不再使用的对象定义为无法达到的对象
    • 从根部(全局对象)出发定时扫描内存中的对象。 能从根部到达的对象,则是仍在使用的对象
    • 那些无法由根部出发触及到的对象被标记为不再使用,稍后会被回收

image-20230522150049358.png

垃圾回收机制详细文档:javascript.info/garbage-col…

闭包

在 JavaScript 中,函数总是可以访问创建它的上下文,这就叫做closure。一个普通的函数,如果它可以访问外层作用域的自由变量,那么这个函数就是一个闭包

  • 从广义的角度:JavaScript 中的函数都是闭包
  • 从狭义的角度:JavaScript 中一个函数,如果访问了外层作用域的变量,那么它是一个闭包

严格上说,只有满足以下条件时,才能算是闭包:

  • 访问外部变量:函数必须引用在它外部定义的变量(自由变量)
  • 外部变量的生命周期:即使外部函数已经执行完毕,闭包仍然能够访问这些外部变量
  • 延迟释放:由于闭包持有对外部变量的引用,这些变量不会被垃圾回收机制回收,直到闭包本身也被销毁
function createCounter() {
  let count = 0 // 这是一个自由变量
  return function () {
    count += 1 // 闭包可以访问并修改自由变量
    return count
  }
}

let counter = createCounter()
console.log(counter()) // 1
console.log(counter()) // 2

闭包可以用于:实现数据的私有化、函数柯里化、数据缓存

// curry 函数的实现
function curry (fn) {
  return function judge (...args) {
    if (args.length === fn.length) {
      return fn(...args)
    }
    return function (...argus) {
      return judge(...args, ...argus)
    }
  }
}

提升

变量提升

变量提升(变量悬挂)是 JavaScript 中比较奇怪的现象,它允许在变量声明之前即被访问,其本质为:把var变量提升到当前作用域于最前面,只提升变量声明, 不提升变量赋值

特点:

  • 变量在var声明之前被访问,变量的值为undefined,即变量提升只提升声明,不提升赋值
  • let/const 声明的变量不存在变量提升,未被声明就使用会报语法错误(TDZ)
  • 变量提升出现在相同作用域当中
//原代码
console.log(a) // undefined
var a  = 1

//代码实际执行过程
var a
console.log(a)
a = 1


//原代码
a = 2
var a
console.log(a) //2

//代码实际执行过程
var a
a = 2
console.log(a)

函数提升

函数提升与变量提升比较类似,指的是函数在声明之前即可被调用

特点:

  • function关键字声明的函数会被提升到当前作用域的最前面,且函数提升是整体提升
  • 函数表达式不存在函数提升的现象,但var声明的变量会发生变量提升
  • 使用 Function 构造器创建的函数并不会被提升
fn() // fn: hello
function fn() {
  console.log('fn: hello')
}

fun() // 报错
let fun = function() {
  console.log('fun: hello')
}

console.log(func) // undefined
func() // 报错
var func = function () {
  console.log('func: hello')
}

函数声明和变量声明使用同一个变量名称时,目前有两种说法,但不影响最终结果:

  • 与优先级有关:函数的优先级高于变量的优先级
  • 与优先级无关:根据声明顺序,会发生覆盖,但由于变量提升只提升声明,纵使变量声明在函数声明后,也不影响最终结果,因为只覆盖了原本的函数名,由于同名,所以没有影响
console.log(fx) // fx定义的函数
var fx = 'fx'
function fx () {
  console.log('fx is a function')
}
console.log(fx) // fx

// 与以上结果一样
console.log(fx) // fx定义的函数
function fx () {
  console.log('fx is a function')
}
var fx = 'fx'
console.log(fx) // fx

原理总结

JavaScript 引擎在执行代码之前,会进行一个预处理阶段,此阶段会处理所有变量和函数的声明。这意味着所有的 var 变量声明和函数声明(包括函数表达式和预声明的函数)都会在这个阶段被处理。

  • 对于变量声明,JavaScript 引擎会将它们移动到当前作用域的顶部。但只有变量的声明会被提升,初始化(赋值)会保留在原位置
  • 对于函数声明,仅通过function关键字声明的函数会被提升到它们所在作用域的顶部。函数表达式只会提升变量,不会提升函数本身

JavaScript 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined

原型

构造函数

除了使用对象字面量new Object创建对象外,也可以使用构造函数来创建对象

构造函数是用来创建对象的函数。构造函数命名一般以大写字母开头,通过new关键字来调用构造函数

  • 使用new关键字调用函数的行为被称为实例化新创建的实例继承了构造函数的所有属性
  • 实例化构造函数时没有参数时可以省略()
  • 构造函数内部无需写return,返回值即为新创建的对象
  • 箭头函数不能作为构造函数(箭头函数没有this
function Pig(name) {
  this.name = name
}
const pig = new Pig('佩奇')
console.log(pig) // Pig { name: '佩奇' }

function Dog() {
  this.name = '大花'
}
const dog = new Dog
console.log(dog) // Dog { name: '大花' }

new实例化执行过程:

  • 创建一个新的空对象

  • 将新对象的__proto__指向构造函数的原型对象

  • 构造函数this指向新对象

  • 执行构造函数代码

  • 返回新对象

我们可以模拟这个过程,手写new

function mynew(Func, ...args) {
  // 创建一个新对象
  const obj = {}
  // 新对象原型指向构造函数原型对象
  obj.__proto__ = Func.prototype
  // 将构建函数的this指向新对象
  let result = Func.apply(obj, args)
  // 根据返回值判断
  return result instanceof Object ? result : obj
}

初学者很容易把构造函数和普通函数混淆,实际上new关键字也能对普通函数使用,也能创建一个对象,但为了避免混淆,规定new关键字只能对构造函数使用

与构造函数关联的属性和方法分为两类:实例成员、静态成员

  • 实例成员:通过构造函数创建的对象称为实例对象,实例对象中的属性和方法称为实例成员,实例对象之间互不影响
  • 静态成员:静态成员属于构造函数本身,在所有实例之间共享,并且可以通过构造函数的名称直接访问,而不需要创建构造函数的实例。静态方法中的this指向构造函数
function Person() {
  this.name = '小明' // 实例成员
  this.sayHi = function() { // 实例成员
    console.log('大家好~')
  }
}
Person.eyes = 2 // 静态成员
Person.walk = function() { // 静态成员
  console.log('走路')
}

const p = new Person() // 实例对象
console.log(Person.eyes) // 访问静态成员
console.log(p.name) // 访问实例成员

原型对象

在 JavaScript 中,原型对象是实现继承的核心机制。每个函数在创建时,都会自动拥有一个名为prototype的属性,这个属性指向一个对象,我们称之为原型对象

  • 原型对象可以挂载属性和方法,对象实例化不会多次创建原型上的属性和方法,节约内存
  • 构造函数通过原型分配的属性和方法是所有对象所共享的
function Person(name, age) {
  this.name = name
  this.age = age
}
Person.prototype.sayHi = function () {
  console.log(this.name + 'hi~')
}
Person.prototype.classNum = '101'
const zs = new Person('张三', 18)
const ls = new Person('李四', 19)

zs.sayHi() // 张三hi~
ls.sayHi() // 李四hi~
console.log(zs.classNum) // 101
console.log(ls.classNum) // 101

对象原型

实例对象都会有一个属性 __proto__ 指向构造函数的原型对象,之所以实例对象可以使用构造函数的原型对象上的属性和方法,就是因为对象有 __proto__属性的存在

注意:

  • __proto__ 是JS非标准属性,推荐使用Object.getPrototypeOf()来访问对象原型
  • [[prototype]](ES5)和__proto__(ES6)意义相同,用来表明当前实例对象指向哪个原型对象
function Person(name) {
  this.name = name
}

Person.prototype.greet = function () {
  console.log('Hello, my name is ' + this.name)
}

const alice = new Person('Alice')

// __proto__ 与 Object.getPrototypeOf()一致
console.log(Object.getPrototypeOf(alice) === alice.__proto__) // true

// 获取实例的原型
console.log(Object.getPrototypeOf(alice) === Person.prototype) // true

// 原型对象本身也是一个对象,可以有自己的原型
console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype) // true

constructor

每个原型对象里面都有个constructor属性,该属性指向该原型对象的构造函数

  • 构造函数.prototype.constructor === 构造函数
  • 原型对象构造函数原型对象(构造函数.prototype).constructor
  • 实例对象构造函数实例对象.__proto__.constructor(此处__proto__可省)

使用场景:

  • 如果有多个对象的方法,我们可以给原型对象采取对象形式赋值,但是这样就会覆盖构造函数原型对象原来的内容,修改后的原型对象 constructor 不再指向当前构造函数。此时,我们可以在修改后的原型对象中,添加一个 constructor 来指向原来的构造函数
function Star (name) {
  this.name = name
}
Star.prototype = {
  constructor: Star, //重新指回,此处不能使用this(指向window)
  sing () { console.log('唱歌') },
  dance () { console.log('跳舞') }
}
const pig = new Star('GGBang') //创建对象一定要在修改原型之后
pig.sing() //唱歌

构造函数、原型、实例对象三者关系图如下:

image-20240508141445206.png

原型链

基于原型对象的继承使得不同构造函数的原型对象关联在一起,并且这种关联的关系是一种链状的结构,我们将原型对象的链状结构关系称为原型链

  • JavaScript中,被继承的函数称为超类型(父类、基类),继承的函数称为子类型(子类、派生类)
  • Object 是 JavaScript 中所有对象的 supertype(超类,父型),也就是原型链的最顶层

image-20240508161630699.png

上图归类为以下几点:

  • 所有的构造函数,都是通过Function构造函数实例化完成,因此构造函数的__proto__(函数也是对象)指向Function.prototype,即Function构造函数的原型对象
  • 所有的普通对象,其__proto__指向其对应的构造函数的原型对象
  • Object.prototype 是所有对象的原型链的起点,是原型链的根基,再往上找对象原型则返回null
function Person(name) {
  this.name = name
}

// 每个构造函数的对象原型(__proto__)指向 Function.prototype
console.log(Person.__proto__ === Function.prototype) // true
console.log(Object.__proto__ === Function.prototype) // true
console.log(Function.__proto__ === Function.prototype) // true

// 构造函数原型对象(prototype)的对象原型(__proto__)指向 Object.prototype
console.log(Function.prototype.__proto__ === Object.prototype) // true
console.log(Person.prototype.__proto__ === Object.prototype) // true

// 原型链终端的对象原型(__proto__)指向null
console.log(Object.prototype.__proto__) // null

原型链查找规则

  • 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性
  • 如果没有就查找它的原型(__proto__指向的原型对象)
  • 如果还没有就查找原型对象的原型(Object的原型对象)
  • 依此类推一直找到 Object 为止(null)
  • __proto__对象原型的意义就在于为对象成员查找机制提供一个查找路线

instanceof

  • 可以使用instanceof运算符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上

  • typeof区别:

    • typeof用于判断基本数据类型
    • instanceof用于判断引用数据类型
// 实例对象  instanceof  构造函数
function Person (name) {
  this.name = name
}

function Person1 (name) {
  this.name = name
}
const zs = new Person('张三')
console.log(zs instanceof Person)  // true
console.log(zs instanceof Person1)  // false

// 数组 
const arr = [1, 2, 3]
console.log(arr instanceof Array)  //  true
console.log(arr instanceof Object)  //  true 
console.log(arr.__proto__ === Array.prototype) // true
console.log(Array.prototype.__proto__ === Object.prototype) // true
console.log(Array.__proto__ === Function.prototype) //true

原型继承

继承是面向对象编程的另一个特征,通过继承进一步提升代码封装的程度,JavaScript 中大多是借助原型对象实现继承的特性

原型链继承

原型链继承是 JavaScript 中最基本的继承方式,它通过将子类的原型指向父类的实例来实现继承,继承到的属性

// 父类
function Animal (name) {
  this.name = name
}

Animal.prototype.sayName = function () {
  console.log(this.name)
}

// 子类
function Dog () { }

Dog.prototype = new Animal('Default') // 原型链继承
Dog.prototype.constructor = Dog

const dog1 = new Dog()
dog1.sayName() //Default

原型链继承会有一些问题:

  • 共享属性: 所有实例共享原型对象上的属性和方法。如果一个实例修改了原型上的属性,其他所有实例也会受到影响。这可能导致意外的行为,特别是在修改引用类型的属性时
  • 无法向父类传递参数: 在使用原型链继承时,子类无法向父类构造函数传递参数。所有子类实例都会共享相同的父类实例,无法在创建子类实例时为父类构造函数传递参数
  • 原型对象上的引用类型属性共享: 如果原型对象上有引用类型的属性,那么所有实例将共享相同的引用,可能导致修改一个实例的属性会影响其他实例

出现的问题如下:

// 父类
function Animal (name) {
  this.name = name
  this.play = [1, 2, 3]
}

Animal.prototype.sayName = function () {
  console.log(this.name)
}

// 子类
function Dog () { }

Dog.prototype = new Animal('Default') // 原型链继承
Dog.prototype.constructor = Dog

const dog1 = new Dog()
const dog2 = new Dog()

console.log(Dog.prototype.name) // 输出 "Default"
console.log(dog1.name) // 输出 "Default"
console.log(dog2.name) // 输出 "Default"

dog1.name = 'New Dog 1' // 修改实例的属性,不影响其他实例
console.log(Dog.prototype.name) // 输出 "Default"
console.log(dog1.name) // 输出 "New Dog 1"
console.log(dog2.name) // 输出 "Default"

Dog.prototype.name = 'New Default' // 修改原型对象上的属性,影响所有实例
console.log(Dog.prototype.name) // 输出 "New Default"
// dog1实例上name属性覆盖了原型对象上的name属性
console.log(dog1.name) // 输出 "New Dog 1"
console.log(dog2.name) // 输出 "New Default"

// 使用push等方法直接修改原数组时,其他实例对象也会受到影响
console.log(dog1.play) // [1, 2, 3]
console.log(dog2.play) // [1, 2, 3]
dog1.play.push(4) // 此处修改的是原型链上的play属性,而不是在dog1对象上新增
// 如若改成 dog1.play = [1, 2, 3, 4] 则dog2不受影响
console.log(dog1.play) // [1, 2, 3, 4]
console.log(dog2.play) // [1, 2, 3, 4]

在 JavaScript 中,当实例对象访问一个属性时,它会按照以下顺序查找:

  • 实例对象自身属性: 如果实例对象直接拥有该属性,就返回该属性的值
  • 原型链上的属性: 如果实例对象没有直接拥有该属性,JavaScript 引擎会沿着原型链向上查找,找到第一个拥有该属性的对象,并返回该属性的值

构造函数继承

构造函数继承通过在子类构造函数中使用 call()apply() 方法来调用父类构造函数,并传递子类实例作为上下文,从而实现继承

构造函数继承优化了原型链继承中:共享属性造成的问题、无法向父类传参的问题、修改原型对象造成的问题

//父类
function Animal (name) {
  this.name = name
  this.sayHi = function () {
    console.log('Hi, I am ' + this.name)
  }
}

Animal.prototype.walk = function () {
  console.log(this.name + ' is walking.')
}

//子类
function Dog (name, breed) {
  Animal.call(this, name) // 调用父类构造函数,继承父类属性
  this.breed = breed
}

const dog = new Dog('Buddy', 'Golden Retriever')
console.log(dog.name) // 输出:Buddy
console.log(dog.breed) // 输出:Golden Retriever
dog.sayHi() // 输出:Hi, I am Buddy
dog.walk() // 报错,walk() 方法未被继承

使用构造函数继承只能继承父类的实例属性和方法,不能继承原型属性或者方法

组合继承

组合继承是将原型链继承和构造函数继承结合起来使用的方式,它通过调用父类构造函数并设置子类原型指向父类的实例来实现继承

// 父类
function Animal (name) {
  this.name = name
  this.sayHi = function () {
    console.log('Hi, I am ' + this.name)
  }
}

Animal.prototype.sayName = function () {
  console.log(this.name)
}

// 子类
function Dog (name, breed) {
  // 使用构造函数继承,继承实例属性
  Animal.call(this, name)
  this.breed = breed
}

// 使用原型链继承,继承原型上的方法
Dog.prototype = new Animal()
Dog.prototype.constructor = Dog

const dog = new Dog('Buddy', 'Golden Retriever')

// 原型的方法
dog.sayName() // 输出 "Buddy"
// 父类的方法
dog.sayHi() // 输出 "Hi, I am Buddy"
console.log(dog.breed) // 输出 "Golden Retriever"

原型链继承:本质是通过原型对象间接继承父类的实例属性和方法(父类属性和方法全部挂载在原型对象上)

构造函数继承:直接继承父类的实例属性和方法(父类属性和方法在子类实例对象上),但不能继承原型上的属性和方法(毕竟没有任何联系)

组合继承:包含上述两种继承的优点,父类的实例属性和方法在子类实例对象上,同时也继承了原型上的属性和方法

组合继承依然有一个小缺点,就是在调用父类构造函数时会调用两次,一次是为了继承实例属性,另一次是为了继承原型上的方法。这个问题可以通过使用 ES6 的 Object.create() 或者其他方式来避免

寄生组合继承

寄生组合继承是为了解决组合继承的一些性能问题而提出的一种继承方式

组合继承的问题:在创建子类实例时,父类的构造函数会被调用两次,一次是为了创建子类实例的属性,另一次是为了设置子类原型链上的方法

寄生组合继承在组合继承的基础上进行了改进:

  • 利用寄生(parasitic)方式修复子类原型: 创建一个空的中间构造函数,该构造函数的原型对象是父类的原型对象的副本。这样,就不需要调用父类构造函数来创建不必要的属性
  • 设置子类的原型为中间构造函数的实例: 这一步与组合继承相同,将子类的原型设置为一个父类的实例,但这次不再是直接使用 new Parent(),而是使用一个临时的构造函数,该构造函数的原型是 Parent.prototype 的副本

Object.create()静态方法以一个现有对象作为原型,创建一个新对象

function inheritPrototype (child, parent) {
  // 此处无需再调用一次父类构造函数
  child.prototype = Object.create(parent.prototype)
  child.prototype.constructor = child
}

// 父类
function Animal (name) {
  this.name = name
}

Animal.prototype.sayName = function () {
  console.log("My name is " + this.name)
}

// 子类
function Dog (name, breed) {
  // 调用父类构造函数
  Animal.call(this, name)
  this.breed = breed
}

// 利用寄生组合继承
inheritPrototype(Dog, Animal)

Dog.prototype.bark = function () {
  console.log("Woof!")
}

var myDog = new Dog("Buddy", "Golden Retriever")
// 调用父类的方法
myDog.sayName() //My name is Buddy
// 调用子类的方法
myDog.bark()    //Woof!

深入理解this

this指向

this 不是静态的,this 在运行时才绑定,它的绑定和函数声明的位置没有关系,只取决于函数调用的方式

  • 非严格模式下:this 不能是 undefined 或 null。当 this 指向为 undefined 或 null 时,this 会指向全局对象
  • 严格模式下:this 会区分指向,正常指向 undefined 和 null

this 指向有一个优先级链,后者不能更改前者的 this 指向

  • 箭头函数 > new关键字 > bind、apply、call > obj. > 直接调用

image-20240606151702814.png

改变this指向

call( )

call()

  • 立即调用一个函数,并指定函数内部的 this
  • 如果call()传入的this上下文是undefinednull,那么window对象将成为默认的this上下文
fun.call(thisArg, arg1, arg2, ...)
//thisArg:在 fun 函数运行时指定的 this 值
//arg1,arg2:传递的其他参数

常见用法:

  • 使用call实现属性继承(继承中常用)
  • Object.prototype.toString.call(数据) 检测数据类型
//检查数据类型(重写)
function checkType(obj) {
  return Object.prototype.toString.call(obj).slice(8, -1)
}

apply( )

apply()

  • 立即调用一个函数,并指定函数内部的 this
fun.apply(thisArg, [argsArray])
//thisArg:在fun函数运行时指定的 this 值
//argsArray:传递的值,必须包含在数组里面

常见用法:

  • 主要用于参数为数组时
//es6之前可以用于求数组的最大值
const arr = [1, 6, 12, 2, 33]
console.log(Math.max.apply(null, arr))

bind( )

bind()

  • bind() 方法不会调用函数,但是能改变函数内部this
  • bind()返回一个新的函数,该函数是改变this后的原函数的拷贝
fun.bind(thisArg, arg1, arg2, ...)
//thisArg:在 fun 函数运行时指定的 this 值
//arg1,arg2:传递的其他参数

常见用法:

  • 只改变 this 指向,不调用函数时,可以使用 bind。比如改变定时器内部的 this 指向
const obj = {
  message: 'Hello',
  greet: function () {
    console.log(this.message)
  }
}

// 使用 bind 修改 setTimeout 内部回调函数的 this 指向
setTimeout(obj.greet, 1000) //undefined
setTimeout(obj.greet.bind(obj), 1000) //Hello

总结

相同点方法传递参数是否调用函数使用场景
改变this指向call参数列表arg1, arg2...调用函数Object.prototype.toString.call()检测数据类型
改变this指向apply数组、伪数组调用函数跟数组相关,比如求数组最大值和最小值等
改变this指向bind参数列表arg1,arg2...不调用函数改变定时器内部的this指向

this指向面试题

基础部分

本节只需要搞懂前几题,后续的题无非就是变种,本质上还是一个东西

  • let、const 变量不会挂载到 window(不能被window访问到,但可以全局直接访问)
let a = 1;
const b = 2;
var c = 3;
function print () {
  console.log(this.a); //undefined
  console.log(this.b); //undefined
  console.log(this.c); //3
}
print();
  • 当函数不作为对象的方法调用时this 会指向全局对象
a = 1;
function foo () {
  console.log(this.a);
}
const obj = {
  a: 10,
  bar () {
    foo(); // 1,this指向window
  }
}
obj.bar(); 
  • 事件回调函数中,this指向绑定的DOM元素;不能用箭头函数,否则会指向window
document.getElementById('btn').addEventListener('click', function() {
  console.log(this); // <button id="btn"></button>
})
  • 立即执行函数中,this 指向 window
a = 1;
(function () {
  console.log(this); //window
  console.log(this.a) //1
}())
function bar () {
  b = 2; // 不写标识符 默认为全局变量
  (function () {
    console.log(this); //window
    console.log(this.b) //2
  }())
}
bar();
  • 对象链式调用,this 绑定最近的对象
var obj1 = {
  a: 1,
  obj2: {
    a: 2,
    foo () {
      console.log(this.a)
    }
  }
}
obj1.obj2.foo() // 2
  • 浏览器环境中,没有明确指定调用对象时,this 默认指向全局对象,通常是 window。此处的foo()不是window.foo()
a = 1
var obj = {
  a: 2,
  foo() {
    console.log(this.a)
  },
}
let foo = obj.foo
obj.foo() // 2
foo() // 1
  • obj2.foo指向了obj.foo的堆内存,此后执行与obj无关
var obj = {
  a: 1,
  foo () {
    console.log(this.a)
  }
};
var a = 2;
var foo = obj.foo;
var obj2 = { a: 3, foo: obj.foo }

obj.foo(); //1
foo(); //2
obj2.foo(); //3
  • call改变this指向,注意返回函数中的this,万变不离其宗
var obj = {
  a: 'obj',
  foo: function () {
    console.log('foo:', this.a)
    return function () {
      console.log('inner:', this.a)
    }
  }
}
var a = 'window'
var obj2 = { a: 'obj2' }

obj.foo()()
obj.foo.call(obj2)()
obj.foo().call(obj2)

//输出结果:
// foo: obj
// inner: window
// foo: obj2
// inner: window
// foo: obj
// inner: obj2
  • 使用call、apply、bind链式调用,以第一次为准
obj = {
  _name: "obj",
  func() {
    const arrowFunc = () => {
      console.log(this._name)
    }
    return arrowFunc
  }
}

obj.func()() //obj
func = obj.func
func()() //undefined, 直接调用, this指向window
obj.func.bind({ _name: "newObj" })()() //newObj
obj.func.bind()()() //undefined, 此处this指向window
obj.func.bind({ _name: "bindObj" }).apply({ _name: "applyObj" })() //bindObj
  • 箭头函数没有 自己的this,它的this指向外层作用域的this,且指向函数定义时的this而非执行时
name = 'tom'
const obj = {
  name: 'zc',
  intro: function () {
    return () => {
      console.log('My name is ' + this.name)
    }
  },
  intro2: function () {
    return function () {
      console.log('My name is ' + this.name)
    }
  }
}
obj.intro2()() //My name is tom
obj.intro()() //My name is zc
  • 箭头函数没有this,不能通过call/apply/bind来修改this指向,但可以通过修改外层作用域this来达成间接修改
var name = 'window'
var obj1 = {
  name: 'obj1',
  intro: function () {
    console.log(this.name)
    return () => {
      console.log(this.name)
    }
  },
  intro2: () => {
    console.log(this.name)
    return function () {
      console.log(this.name)
    }
  }
}
var obj2 = {
  name: 'obj2'
}
obj1.intro.call(obj2)() //obj2 obj2
obj1.intro().call(obj2) //obj1 obj1
obj1.intro2.call(obj2)() //window window
obj1.intro2().call(obj2) //window obj2
  • 本题为上文全部类型的集合
var name = 'window'
var user1 = {
  name: 'user1',
  foo1: function () {
    console.log(this.name)
  },
  foo2: () => console.log(this.name),
  foo3: function () {
    return function () {
      console.log(this.name)
    }
  },
  foo4: function () {
    return () => {
      console.log(this.name)
    }
  }
}
var user2 = { name: 'user2' }

user1.foo1() //user1
user1.foo1.call(user2) //user2

user1.foo2() //window
user1.foo2.call(user2) //window

user1.foo3()() //window
user1.foo3.call(user2)() //window
user1.foo3().call(user2) //user2

user1.foo4()() //user1
user1.foo4.call(user2)() //user2
user1.foo4().call(user2) //user1

综合提升

  • (foo.bar = foo.bar)()意为赋值并调用,类似于取别名;(foo.bar, foo.bar)逗号表达式,前后执行完后返回右边表达式的结果
var x = 10;
var foo = {
  x: 20,
  bar: function () {
    var x = 30;
    console.log(this.x)
  }
};
foo.bar(); //20
(foo.bar)(); //20
(foo.bar = foo.bar)(); //10,这里相当于是一个新的函数,在全局作用域内执行
(foo.bar, foo.bar)(); //10,这里相当于是一个新的函数,在全局作用域内执行

本题了解即可,没有太大意义

  • arguments为伪数组,实际上是一个对象,不过对象属性有0、1、2编号,并且有length属性
var length = 10;
function fn () {
  console.log(this.length);
}

var obj = {
  length: 5,
  method: function (fn) {
    fn();
    arguments[0](); //这里相当于arguments.fn()
  }
};

obj.method(fn, 1); //10 2
  • JavaScript 作用域链为静态,在函数定义时就已经确定;对象中的立即执行函数,会在对象创建时自动执行
var number = 5;
var obj = {
  number: 3,
  fn: (function () {
    var number;
    this.number *= 2;
    number = number * 2;
    number = 3;
    return function () {
      var num = this.number;
      this.number *= 2;
      console.log(num);
      number *= 3;
      console.log(number);
    }
  })()
}
var myFun = obj.fn;
myFun.call(null);
obj.fn();
console.log(window.number);

//输出结果为:
//10
//9
//3
//27
//20

节流防抖

防抖

单位时间内,频繁触发事件,只执行最后一次。防抖的目的是确保函数在指定的延迟时间内没有被再次调用,才会执行。在搜索框搜索输入、按钮点击等需要短暂等待用户操作完成的场景中非常适用

function debounce (func, delay) {
  let timeId
  return function () {
    clearTimeout(timeId)
    timeId = setTimeout(() => {
      func.apply(this, arguments)
    }, delay)
  }
}

节流

单位时间内,频繁触发事件,只执行一次。这在处理滚动、调整大小、按键等连续触发的事件时非常有用。通过节流,我们可以避免函数被过度调用

function throttle(func, delay) {
  let timerId
  return function () {
    if (!timerId) {
      timerId = setTimeout(() => {
        func.apply(this, arguments)
        timerId = null
      }, delay)
    }
  }
}

function throttle(func, delay) {
  let lastExec = 0
  return function () {
    const now = Date.now()
    if (now - lastExec >= delay) {
      lastExec = now
      func.apply(this, arguments)
    }
  }
}

模块化

基本知识

为了让 JavaScript 更模块化、更整洁以及更易于维护,ES6 引入了在多个 JavaScript 文件之间共享代码的机制。 它可以导出文件的一部分供其它文件使用,然后在需要它的地方按需导入

HTML中导入模块,需要定义属性 type="module",模块总是会在所有HTML解析后才执行

<script type="module" src="filename.js"></script>
<!-- 使用了 module 类型的脚本可以使用 import 和 export 特性 -->
  • 模块都有独立的顶级作用域,与普通script标签不同,不同模块不能互相访问
<script type="module">
  let test = "Hello World"
</script>

<script type="module">
  alert(test) // Error
</script>
  • 模块在导入时只执行一次解析,之后的导入不会再执行模块代码,而使用第一次解析结果,并共享数据
<script type="module" src="test.js"></script>
<script type="module" src="test.js"></script>

导入导出

ES6 使用基于文件的模块,即一个文件一个模块

  • 使用export 将开发的接口导出
  • 使用import 导入模块接口
  • 使用*可以导入全部模块接口
  • 导出是以引用方式导出,无论是标量还是对象,即模块内部变量发生变化将影响已经导入的变量

下面是对math.js模块进行导出

// 整体导出
const add = (x, y) => x + y
const subtract = (x, y) => x - y
export { add, subtract }

// 单个导出
export const add = (x, y) => x + y
export const subtract = (x, y) => x - y

导入分为两种:具名导入、批量导入

  • 具名导入(建议使用)
import { add, subtract } from './math.js'
// 用这种方式导入时,相对路径(./)和文件扩展名(.js)都是必需的
  • 批量导入(*
// import 语句会创建一个 addModule 的对象(命名自定义)
import * as addModule from "./math.js"

// addModule 对象包含 math.js 文件里的所有导出,可以直接访问其中的函数
myMathModule.add(2,3)
myMathModule.subtract(5,3)

别名

可以为导入和导出的模块起别名,使用as关键字

  • 有些导出的模块命名过长,起别名更简洁
  • 本模块与导入模块重名时,可以通过起别名防止冲突
// 导入
import { add as addFunc, subtract as subtractFunc } from './math.js'

// 导出
export { add as addFunc, subtract as subtractFunc }

默认导出

在文件中只有一个类或需要导出的时候,通常会使用默认导出语法,它也常常用于给文件或者模块创建返回值

使用default 定义默认导出的接口,导入时不需要使用 {}

  • 默认导出可以不命名,也可以为默认导出自定义别名
  • 只能有一个默认导出
  • 如果将导出命名为default也算默认导出
// 导出
export default function add(x, y) {
  return x + y
}

// 导入
import add from "./math.js"
// add只是变量名,对应 math.js 文件的任何默认导出值。在导入默认导出时,可以任意命名

如有多个函数或变量

const baseURL = 'https://www.baidu.com/'
const getArraySum = arr => arr.reduce((sum, item) => sum += item, 0)
export default {
  baseURL,
  getArraySum
}

混合导出

模块可以同时存在默认导出与命名导出

  • 使用export default 导出默认接口
  • 使用 export {} 导出普通接口
// 导出(math.js)
const baseURL = 'https://www.baidu.com/'
const getArraySum = arr => arr.reduce((sum, item) => sum += item, 0)
function add(x, y) {
  return x + y
}
export { baseURL, getArraySum, add as default } // 将导出命名为default也算默认导出

// 导入
import { baseURL, getArraySum } from './math.js' // 对应export
import addFunc from './math.js' // 对应export default
  • 可以使用一条语句导入默认接口与常规接口
import { baseURL, getArraySum }, addFunc from './math.js'
  • 也可以使用别名导入默认导出
import { baseURL, getArraySum, default as addFunc } from './math.js'
  • 如果是批量导入,可以使用 default 获得默认导出
import * as api from './math.js'
api.default.add()

导出合并

可以将导入的模块重新导出使用,将所有模块合并到一个入口文件中

  • math.js
export const add = (x, y) => x + y
export const subtract = (x, y) => x - y
  • base.js
const baseURL = 'https://www.baidu.com/'
const getArraySum = arr => arr.reduce((sum, item) => sum += item, 0)
export default {
  baseURL,
  getArraySum
}

统一导出

export * as math from "./math.js";
// 默认模块需要单独导出
export { default as base } from "./base.js";

动态加载

使用 import 必须在顶层静态导入模块,而使用import() 函数可以动态导入模块,它返回一个 promise 对象

不在顶层静态导入时是错误的,但使用动态加载却可以

if (true) {
  import { add, subtract } from "./math.js"; // Error
  const res = import(./math.js)
}

异步&正则&跨域

站内关于这些知识点的教程有很多,为防止内容过于冗杂,这里给出跳转链接,供大家参考

Web APIs

DOM简介

DOM(Document Object Model文档对象模型)是用来呈现以及与任意HTML或XML文档交互的API

  • DOM 是浏览器提供的一套专门用来操作网页内容的功能,DOM 的核心是把网页内容当做对象来处理,通过对象的属性和方法来操作网页内容,开发网页特效和实现用户交互

DOM 树结构如下:

  • DOM 树直观的体现了标签与标签之间的关系,每一个节点都代表一个对象
  • document对象是浏览器提供的全局对象,它是 DOM 的根节点,它提供的属性和方法都是用来访问和操作网页内容

image-20230511224731327.png

DOM对象

节点对象可以理解成具体的DOM对象,本文中DOM对象和节点对象意思一致

JS 中操作 DOM 的内容称为节点对象(Node类型),即然是对象就包括操作 NODE 的属性和方法

DOM 中包括12种类型的节点对象

  • 元素节点(Element Node)Node.ELEMENT_NODE(值为1)代表HTML中的元素,如<div><p>
  • 属性节点(Attribute Node):Node.ATTRIBUTE_NODE(值为2)代表元素的属性,现代 DOM 操作中不常作为独立节点使用
  • 文本节点(Text Node)Node.TEXT_NODE(值为3)代表元素的文本内容
  • CDATA节点(CDATA Section Node):Node.CDATA_SECTION_NODE(值为4)代表 XML 中的字符数据区域
  • 实体引用节点(Entity Reference Node):Node.ENTITY_REFERENCE_NODE(值为5)代表 XML 中的实体引用
  • 实体节点(Entity Node):Node.ENTITY_NODE(值为6)代表XML中的实体
  • 处理指令节点(Processing Instruction Node):Node.PROCESSING_INSTRUCTION_NODE(值为7)代表XML中的处理指令
  • 注释节点(Comment Node)Node.COMMENT_NODE(值为8)代表 HTML 或 XML 中的注释
  • 文档节点(Document Node)Node.DOCUMENT_NODE(值为9)代表整个文档,等同于document对象
  • 文档类型节点(DocumentType Node):Node.DOCUMENT_TYPE_NODE(值为10)代表文档类型声明,如<!DOCTYPE html>
  • 文档片段节点(DocumentFragment Node):Node.DOCUMENT_FRAGMENT_NODE(值为11)代表轻量级的 DOM 节点,可以包含多个子节点,但本身不是文档的一部分
  • 记法节点(Notation Node):Node.NOTATION_NODE(值为12)代表 XML 中的记法声明

节点原型链中,最主要的对象如下:

原型说明
Object根对象,提供 hasOwnProperty 等基本对象操作支持
EventTarget提供 addEventListener、removeEventListener 等事件支持方法
Node提供 firstChild、parentNode 等节点操作方法
Element提供 getElementsByTagName、querySelector 等方法
HTMLElement所有元素的基础类,提供 childNodes、nodeType、nodeName、className、nodeName 等方法

获取原型链对象代码如下:

function getPrototype(obj) {
  const prototypes = []
  while (obj) {
    prototypes.push(obj)
    obj = Object.getPrototypeOf(obj)
  }
  return prototypes
}

常用的 JavaScript 中的 DOM API 如下表:

方法说明
documentdocument 是 DOM 操作的起始节点
document.documentElement文档节点即 html 标签节点
document.bodybody 标签节点
document.headhead 标签节点
document.links超链接集合
document.anchors所有锚点集合
document.formsform 表单集合
document.images图片集合

此外,元素的标准属性具有相对应的 DOM 对象属性

  • DOM 对象不同生成的属性也不同
  • 操作属性区分大小写,多个单词使用小驼峰命名法
  • 属性值为多种类型,并不全是字符串,也可能是对。如:事件处理程序属性值为函数
  • style 属性为CSSStyleDeclaration对象

特殊的几个属性的对应关系如下:

HTML属性JS别名
classclassName
forhtmlFor

节点获取

根据各个属性来获取元素的基本选择器:

方法说明注意事项返回值
getElementById(id)根据id获取对应元素只能通过document对象调用,id大小写敏感HTMLElement
getElementsByClassName(class)根据class获取对应元素集合可以在任何元素上调用,调用该方法的元素作为本次查找的根元素HTMLCollection
getElementsByName(name)根据name获取对应元素集合只能通过document对象调用Nodelist
getElementsByTagName(tagName)根据tagName获取对应元素集合不区分大小写HTMLCollection

CSS 选择器:

方法说明注意事项返回值
querySelector('css选择器')根据css选择器获取匹配的第一个元素可以在任何元素上调用,调用该方法的元素作为本次查找的根元素HTMLElement
querySelectorAll('css选择器')根据class获取对应元素集合可以在任何元素上调用,调用该方法的元素作为本次查找的根元素Nodelist

节点属性

nodeType:以数值返回节点类型

nodeName:指定节点的名称,获取值为大写形式

tagName:用于获取标签节点的名称

nodeType说明nodeNamenodeValue
1元素节点元素名称,如DIVnull
2属性节点属性名称属性值
3文本节点#text文本内容
8注释节点#comment注释内容
9document 对象

节点集合

NodeListHTMLCollection都是包含多个节点标签的集合,大部分功能相同

  • NodelistHTMLCollection提供了item()方法来根据索引获取元素,也可以直接使用[索引]的形式
<div></div>
<div></div>
<script>
  //结果为NodeList
  const nodeLists = document.querySelectorAll('div')
  //结果为HTMLCollection
  const htmlCollection = document.getElementsByTagName('div')
  console.dir(nodeLists.item(0))
  console.dir(nodeLists[0])
</script>
  • HTMLCollection是动态的集合,随DOM动态变化;NodeList是静态集合,为固定快照
<h1>Hello World</h1>
<h1>Hello DOM</h1>
<button id="add">添加元素</button>

<script>
  const elementQuery = document.querySelectorAll('h1')
  const elementGet = document.getElementsByTagName('h1')
  document.querySelector('#add').addEventListener('click', () => {
    document.querySelector('body').insertAdjacentHTML('beforeend', '<h1>Hello JS</h1>')
    console.log('NodeList:', elementQuery, 'HTMLCollection:', elementGet)
  })
</script>
  • 节点集合对象原型中不存在map方法,但可以借用Array的原型map方法实现遍历
<div>Hello World</div>
<div>Hello DOM</div>

<script>
  const nodes = document.querySelectorAll('div')
  Array.prototype.map.call(nodes, (node, index) => {
    console.log(node, index)
  })
</script>
  • 伪数组也可以通过...展开运算符或者Array.from转换成数组从而实现遍历
<div>Hello World</div>
<div>Hello DOM</div>

<script>
  const nodes = document.querySelectorAll('div')
  Array.from(nodes).forEach(node => console.log(node))
  ;[...nodes].forEach(node => console.log(node))
</script>

NodeListHTMLCollection区别如下:

HTMLCollectionNodeList
集合元素节点的集合节点的集合,包含元素节点、属性节点、文本节点、注释节点等
静态和动态动态静态,但对节点内容修改能监听到如innerHTML
获取方式getElementsByTagName
getElementsByClassName
getElementById
querySelectorAll
是否为伪数组类数组,无法使用数组方法类数组,但本身实现了forEachkeysvaluesentries()
是否可迭代可迭代,for...of遍历可迭代,for...of遍历

节点关系

DOM 中的节点关系可大致分为:父子关系祖孙关系兄弟关系

下面是通过节点关系获取相应元素的方法

  • 文本与注释也是节点,例如换行'\n'
  • HTML标签的父节点是document
节点属性说明
childNodes获取所有子节点
parentNode获取父节点
firstChild第一个子节点
lastChild最后一个子节点
nextSibling下一个兄弟节点
previousSibling上一个兄弟节点

标签关系

使用childNodes等获取的节点包括文本节点与注释节点,DOM 也提供了只操作标签的方法

  • 文本与注释不属于标签
  • HTML标签的父元素为空
节点属性说明
parentElement获取父元素
children获取所有子元素
childElementCount子标签元素的数量
firstElementChild第一个子标签
lastElementChild最后一个子标签
previousElementSibling上一个兄弟标签
nextElementSibling下一个兄弟标签
contains返回布尔值,判断传入的节点是否为该节点的后代节点

节点内容

innerHTML:用于获取或设置一个元素内部的 HTML 内容,会解析新的 HTML 字符串,并更新 DOM 树,这通常会导致浏览器的重绘(repaint)和重排(reflow)

outerHTML:用于获取或设置一个元素的HTML内容,包括元素本身和它的所有子元素

<div id="myDiv">
  <p>Hello, World!</p>
</div>

<script>
  // 替换 #myDiv 内部的 <p>Hello, World!</p>
  document.getElementById('myDiv').innerHTML = '<p>Goodbye, World!</p>';
  // 直接替换了 整个 #myDiv 元素
  document.getElementById('myDiv').outerHTML = '<div><p>Goodbye, World!</p></div>';
</script>

textContent:获取或设置一个节点及其所有后代节点的纯文本内容,这包括隐藏的元素,不解析标签

innerText:表示元素及子孙节点渲染后的可视文本内容,不解析标签

<p>Some <strong>important</strong> text</p>
<script>
  console.log(document.querySelector('p').textContent) // Some important  text
  console.log(document.querySelector('p').innerText) // Some important text
</script>

outerText:与innerText类似,但会影响操作的标签

insertAdjacentText(position, element):将文本插入到元素指定位置,不会对文本中的标签进行解析,包括以下位置:

选项说明
beforebegin元素本身前面
afterend元素本身后面
afterbegin元素内部前面
beforeend元素内部后面
<!-- beforebegin -->
<p>
  <!-- afterbegin -->
  foo
  <!-- beforeend -->
</p>
<!-- afterend -->

节点操作

  • 创建节点:document.createElement('tagName')

  • 追加节点

方法说明
append节点最后一个子节点之后添加新节点或字符串(appendChild升级版)
prepend节点第一个子元素的之前添加新节点或字符串
before节点前面添加新节点或字符串
after节点后面添加新节点或字符串
replaceWith将节点替换为新节点或字符串
//插入节点
var parent = document.createElement("div");
var p = document.createElement("p");
parent.append(p);

console.log(parent.childNodes); // NodeList [ <p> ]

//插入文本
var parent = document.createElement("div");
parent.append("Some text");

console.log(parent.textContent); // "Some text"

//插入节点同时插入文本
var parent = document.createElement("div");
var p = document.createElement("p");
parent.append("Some text", p);

console.log(parent.childNodes); // NodeList [ #text "Some text", <p> ]
  • 删除节点:element.remove(),把对象从它所属的 DOM 树中删除
  • 虚拟节点:DocumentFragment,不会造成性能问题,DOM 操作频繁时使用

元素属性

常用属性

常用属性指的是元素本身的属性,使用对象.属性 = 值的方法,直接修改对象属性值

对于标准的属性可以使用 DOM 属性的方式进行操作,但对于标签的非标准的定制属性则不可以

JS 提供了方法来控制标准或非标准的属性(属性名称不区分大小写):

方法说明
getAttribute获取属性
setAttribute设置属性
removeAttribute删除属性
hasAttribute属性检测

元素也提供了attributes属性来只读的获取元素的属性

自定义属性

HTML5新增了data-自定义属性

  • 标签上以data-开头
  • 在DOM对象上以dataset对象方式获取
  • DOM对象.dataset获取自定义属性对象,属性为各个自定义属性
<div class='box' data-id='10'>盒子</div>
<script>
  const box = document.querySelector('.box')
  console.log(box.dataset.id)
</script>

样式属性

通过 JavaScript 设置或修改标签元素的样式属性

  • 通过style属性操作 CSS
//对象.style.样式属性 = 值
const box = document.querySelector('.box')
box.style.width = '200px'
box.style.marginTop = '15px'//此处的连接符使用小驼峰命名法

此处,如果width属性未在元素上定义但通过CSS定义,使用element.style.width时,获取为null,需要使用window.getComputedStyle(元素, 伪元素)方法

  • 操作类名className操作CSS
    • 如果修改的样式比较多,可以通过修改类名来操作 CSS,classNameclass别名
    • className 是使用新值换旧值,如果需要添加一个类,需要保留之前的类名
元素.className = 'active'
  • 通过classList追加和删除类来控制 CSS
// 追加一个类
元素.classList.add('类名')
// 删除一个类
元素.classList.remove('类名')
// 切换一个类,如果该类名存在则移除,不存在则添加
元素.classList.toggle('类名')
// 是否包含某个类,如果有则返回true,没有则返回false
元素.classList.contains('类名')

DOM事件

在文档、浏览器以及标签元素等在特定状态下触发的行为即为事件,如用户的点击行为、表单内容的改变行为,我们可以为不同的事件定义处理程序。JS 使用异步事件驱动的形式管理事件

事件监听

事件监听指的是让程序检测是否有事件产生,一旦有事件触发,就立即调用一个函数做出响应,也称为绑定事件或者注册事件

  • 事件监听是将事件处理函数注册到元素对象上,包括事件源事件类型事件处理函数
  • 可以给同一个 DOM 对象添加同一种类型的事件监听,它们会按照添加的顺序执行
  • 可以给动态生成的元素绑定事件
方法说明
addEventListener(type, listener)
addEventListener(type, listener, options)
addEventListener(type, listener, useCapture)
添加事件处理程序
removeEventListener(type, listener)
removeEventListener(type, listener, options)
removeEventListener(type, listener, useCapture)
移除事件处理程序,匿名函数无法移除

如给button按钮添加点击事件

<button class="btn1">按钮1</button>
<button class="btn2">移除按钮1事件</button>

<script>
  const btn1 = document.querySelector('.btn1')
  const btn2 = document.querySelector('.btn1')
  function alert1() {
    alert('点击事件1执行了~')
  }
  function alert2() {
    alert('点击事件2执行了~')
  }
  btn1.addEventListener('click', alert1)
  btn1.addEventListener('click', alert2)
  btn2.addEventListener('click', function () {
    btn1.removeEventListener('click', alert1)
    btn1.removeEventListener('click', alert2)
  })
</script>

事件监听还有一种写法,但由于其局限性,目前已经很少被使用,此处了解即可

  • 事件源.on事件 = function() {}

  • <button onclick="函数名()">按钮</button>

  • on方式会被覆盖(DOM L0),addEventListener方式可绑定多次,拥有事件更多特性(DOM L2)

事件类型

事件类型种类有很多,这里列举常见的事件类型

  • 鼠标事件:鼠标事件会触发在z-index层级最高的元素上
事件触发时机
click鼠标单击事件
dblclick鼠标双击事件
mouseover鼠标移动到元素上
mouseout鼠标从元素上移出
mouseenter鼠标移入时触发,不产生冒泡行为
mouseleave鼠标移出时触发,不产生冒泡行为
oncopy复制内容时触发
  • 键盘事件keydown > input > keyup
事件触发时机
keydown键盘按下触发
keyup键盘抬起触发
  • 表单事件
事件触发时机
focus获取焦点事件
blur失去焦点事件
change文本框在内容发生改变并失去焦点时触发,select/checkbox/radio 选项改变时触发事件
inputInput、textarea、select 元素的 value 被修改时触发 。而 change 是鼠标离开后或选择一个不同的 option 时触发
submit提交表单

对象.click() 方法是程序可以模拟用户点击事件,自动执行。常见的有click()、blur()、focus()

  • 页面事件
事件触发时机
load监听页面所有资源加载完毕(window对象)
DOMContentLoaded监听页面 DOM 加载完毕(document对象)无需等待样式表、图像等完全加载
scroll页面滚动时触发(window对象)
resize窗口尺寸改变的时候触发事件(window对象)
unload文档卸载时
beforeunload文档刷新或关闭时

事件对象

执行事件处理程序时,会产生当前事件相关信息的对象,即为事件对事。系统会自动做为参数传递给事件处理程序

  • 事件绑定的回调函数的第一个参数就是事件对象,一般命名为event、e。可以不显式声明事件对象,直接使用event
元素.addEventListener('click', function(e) {
  //其中e即为事件对象
})

元素.addEventListener('click', function() {
  //其中event即为事件对象
})

事件对象常用属性如下:

属性名类型说明
altKeyboolean事件发生时,是否按下alt按键
ctrlkeyboolean事件发生时,是否按下ctrl按键
shiftKeyboolean事件发生时,是否按下shift按键
offsetXnumber事件发生时,鼠标相对于事件源的x坐标
offsetYnumber事件发生时,鼠标相对于事件源的y坐标
targetobject事件源对象
pageXnumber事件发生时,鼠标相对于网页的x坐标
pageYnumber事件发生时,鼠标相对于网页的y坐标
clientXnumber事件发生时,鼠标相对于视口的x坐标
clientYnumber事件发生时,鼠标相对于视口的y坐标
keystring如果是键盘相关事件,则事件对象中包含该属性,表示键盘事件发生时,按下的是什么键。'Enter'回车键

事件流

事件流指的是事件完整执行过程中的流动路径

  • 例如:假设页面里有个div,当触发事件时,会经历三个阶段,分别是捕获阶段(Capturing)目标阶段(Target)冒泡阶段(Bubbling)
  • 简单来说:捕获阶段是从父到子、冒泡阶段是从子到父,并且我们只能干预捕获和冒泡阶段中的一个
  • 目标阶段指的是触发自己的事件

image-20230517171243300.png

捕获阶段--->目标阶段--->冒泡阶段

事件捕获

当一个元素的事件被触发时,会从 DOM 的根元素开始依次调用同名事件(从外到里)

DOM.addEventListener(事件类型, 事件处理函数, 是否使用捕获机制)
  • 第三个参数传入true代表是捕获阶段触发
  • 若传入false代表冒泡阶段触发,默认为false
<body>
  <div class="father">
        父盒子
    <div class="son">子盒子</div>
  </div>
  <script>
    // 事件流
    const father = document.querySelector('.father')
    const son = document.querySelector('.son')
    // 事件捕获
    father.addEventListener('click', function () {
      alert('我是爸爸')
    }, true)  // 事件捕获
    son.addEventListener('click', function () {
      alert('我是儿子')
    }, true) // 事件捕获
  </script>
</body>

事件冒泡

当一个元素的事件被触发时,同样的事件将会在该元素的所有祖先元素中依次被触发。这一过程被称为事件冒泡(从里到外)

  • 简单理解:当一个元素触发事件后,会依次向上调用所有父级元素的同名事件
  • 事件冒泡是默认存在的(默认第三参数为false

因为默认就有冒泡阶段的存在,所以容易导致事件影响到父级元素。若想把事件就限制在当前元素内,就需要阻止事件冒泡,阻止事件冒泡需要拿到事件对象

事件对象.stopPropagation()

示例:

<div class="father">
    父盒子
    <div class="son">子盒子</div>
</div>
<script>
  // 事件流
  const father = document.querySelector('.father')
  const son = document.querySelector('.son')
  // 点击父盒子
  father.addEventListener('click', function () {
    alert('我是爸爸')
  })
  // 点击子盒子
  son.addEventListener('click', function (e) {
    alert('我是儿子')
    e.stopPropagation() //阻止冒泡
  }) 
</script>

阻止默认行为

阻止元素发生默认行为:事件对象.preventDefault()

<body>
  <form action="">
    姓名: <input type="text" name="username">
    <button>提交</button>
  </form>
  <a href="http://www.baidu.com">点击跳转</a>
  <script>
    // 阻止默认行为
    const form = document.querySelector('form')
    const input = document.querySelector('[name=username]')
    form.addEventListener('submit', function (e) {
      // 如果input表单的值为空则不允许提交
      if (input.value === '') {
        // return 无法阻止提交事件
        e.preventDefault()  // 阻止提交事件
      }
    })

    document.querySelector('a').addEventListener('click', function (e) {
      e.preventDefault()
    })
  </script>
</body>

事件委托

事件委托指的是:原本需要注册在子元素的事件委托给父元素,让父元素担当事件监听的职务。是JavaScript中注册事件的常用技巧,也称为事件委派事件代理

  • 优点:减少注册次数,可以提高程序性能
  • 原理:事件委托其实是利用事件冒泡的特点,给父元素注册事件,当我们触发子元素的时候,会冒泡到父元素身上,从而触发父元素的事件(事件冒泡)

获取当前点击的元素:

事件对象.target.tagName //e.target事件目标

注意:

  • e.target.tagName获取的标签名要大写
  • e.target得到目标元素

视口

首先理解视口(窗口)与文档的含义

  • 视口是用户可以直接看到的网页区域,它通常小于整个网页的尺寸,所以文档尺寸一般大于视口尺寸
  • 视口的尺寸不包括浏览器的UI元素,如工具条、菜单、标签页、状态栏等
  • 当浏览器窗口大小改变时,视口的尺寸也会相应改变。
  • 当用户滚动页面时,视口会移动,显示文档的不同部分。
  • 视口的尺寸可以通过window.innerWidthwindow.innerHeight来获取

视口坐标需要知道滚动条位置才可以进行计算,有以下几种方式获取滚动位置

方法说明注意
window.innerWidth视口宽度包括滚动条(不常用)
window.innerHeight视口高度包括滚动条(不常用)
document.documentElement.clientWidth视口宽度
document.documentElement.clientHeight视口高度

L0hUTUw15byA5Y-R5paH5qGjL2ltYWdlcy9Dc3NCb3hNb2RlbC5wbmc.png

下面是获取尺寸的方法或属性

方法说明备注
element.getBoundingClientRect返回元素在视口坐标及元素大小,包括外边距,width/height 与 offsetWidth/offsetHeight 匹配窗口坐标
element.getClientRects行级元素每行尺寸位置组成的数组
element.offsetParent拥有定位属性的父级,或 body/td/th/table对于隐藏元素 /body/html 值为 null
element.offsetWidth元素宽度尺寸,包括内边距、边框、滚动条
element.offsetHeight元素高度尺寸,包括内边距、边框、滚动条
element.offsetLeft相对于最近祖先定位元素的 X 轴坐标
element.offsetTop相对于最近祖先定位元素的 Y 轴坐标
element.clientWidth元素宽度,不包含边框,只包含内容和内边距内联元素尺寸为 0
element.clientHeight元素高度,不包含边框,只包含内容和内边距内联元素尺寸为 0
element.clientLeft内容距离外部的距离,滚动条在左侧时包括滚动条尺寸
element.clientTop内容距离顶部的距离,滚动条在顶部时包括滚动条尺寸
element.scrollWidth元素宽度,内容+内边距+内容溢出的尺寸
element.scrollHeight元素高度,内容+内边距+内容溢出的尺寸
element.scrollLeft水平滚动条左侧已经滚动的宽度
element.scrollTop垂直滚动条顶部已经滚动的高度

总结如下:

联想截图_20240524153757.png

BOM

简介

BOM (Browser Object Model) 浏览器对象模型

  • window 对象是一个全局对象,是 JavaScript 中的顶级对象
  • 例如document、alert()、console.log()这些都是 window 的属性,基本BOM的属性和方法都属于 window
  • 所有通过var定义在全局作用域中的变量以及全局作用域中定义的函数都会变成 window 对象的属性和方法
  • window 对象下的属性和方法调用的时候可以省略 window

image-20230519181829238.png

location

location它拆分并保存了 URL 地址的各个组成部分, 是一个对象

常用属性和方法:

属性/方法说明
href属性,获取完整的URL地址,赋值时用于地址的跳转
search属性,获取地址中携带的参数,符号?后面部分
hash属性,获取地址中的啥希值,符号#后面部分
reload()方法,用来刷新当前页面,传入参数true时表示强制刷新

示例:

<body>
  <form>
    <input type="text" name="search"> <button>搜索</button>
  </form>
  <a href="#/music">音乐</a>
  <a href="#/download">下载</a>

  <button class="reload">刷新页面</button>
  <script> 
    // href 属性  得到完整地址,赋值则是跳转到新地址
    console.log(location.href)
    // location.href = 'http://www.baidu.com'

    // search 属性  得到 ? 后面的地址 
    console.log(location.search)  // ?search=笔记本

    // hash 属性  得到 # 后面的地址
    console.log(location.hash)

    // reload 方法  刷新页面
    const btn = document.querySelector('.reload')
    btn.addEventListener('click', function () {
      // location.reload() // 页面刷新
      location.reload(true) // 强制页面刷新 ctrl+f5
    })
  </script>
</body>

navigator

只读属性 window.navigator 会返回一个 Navigator 对象的引用,可以用于请求运行当前代码的应用程序的相关信息。如通过 userAgent 检测浏览器的版本及平台

// 检测 userAgent(浏览器信息)
(function () {
const userAgent = navigator.userAgent
// 验证是否为Android或iPhone
const android = userAgent.match(/(Android);?[\s\/]+([\d.]+)?/)
const iphone = userAgent.match(/(iPhone\sOS)\s([\d_]+)/)
// 如果是Android或iPhone,则跳转至移动站点
if (android || iphone) {
location.href = 'http://m.itcast.cn' }
})();

history

history对象主要管理历史记录, 该对象与浏览器地址栏的操作相对应,如前进、后退等

常见方法:

方法作用
back()后退
forward()前进
go(参数)-1后退,1前进,0刷新页面

示例:

<body>
  <button class="back">←后退</button>
  <button class="forward">前进→</button>
  <script>
    // 前进
    const forward = document.querySelector('.forward')
    forward.addEventListener('click', function () {
      // history.forward() 
      history.go(1)
    })
    // 后退
    const back = document.querySelector('.back')
    back.addEventListener('click', function () {
      // history.back()
      history.go(-1)
    })
  </script>
</body>

本地存储

将数据存储在本地浏览器中,主要用于页面刷新数据不丢失,实现数据持久化存储。sessionStoragelocalStorage约5M左右

localStorage

数据可以长期保留在本地浏览器中,刷新页面和关闭页面,数据也不会丢失。localStorage 以键值对的形式存储,并且存储的是字符串

  • 存储:localStorage.setItem(key, value)
  • 获取:localStorage.getItem(key)
  • 删除:localStorage.removeItem(key)

由于本地只能存储字符串,无法存储引用数据类型。所以在存储引用数据类型时,需要将其转换成 JSON 字符串,在使用时,再转换成引用数据类型。其转换过程如下:

image-20230519190050874.png

(1)将复杂数据类型转换成 JSON (JavaScript Object Notation) 字符串:JSON.stringify(复杂数据类型)

  • JSON字符串:
    • 本质上是一个字符串
    • 属性名使用双引号引起来,不能单引号
    • 属性值如果是字符串型也必须双引号

(2)把取出来的字符串转换为对象:JSON.parse(JSON字符串)

  • 注意:数据多了会自动用数组存储起来

使用示例:

// 本地存储复杂数据类型
const goods = {
  name: '华为',
  price: 9999
}
// 1. 把对象转换为JSON字符串  JSON.stringify
localStorage.setItem('goods', JSON.stringify(goods))
// 2.取出goods JSON.parse
JSON.parse(localStorage.getItem('goods'))

sessionStorage

用法跟 localStorage 基本相同,区别是:当页面浏览器被关闭时,存储在sessionStorage的数据会被清除

  • 存储:sessionStorage.setItem(key, value)
  • 获取:sessionStorage.getItem(key)
  • 删除:sessionStorage.removeItem(key)

cookie

Cookie 是一种由网站存储在用户设备(通常是浏览器)上的小数据片段。它们用于互联网通信过程,以识别浏览器,关联用户的活动并维持状态信息。以下是其注意点:

  • Cookie 可以存储用户的偏好设置、登录状态、会话信息或其他数据
  • 每次用户请求同一服务器时,浏览器会在 HTTP 请求头中自动发送之前存储的 Cookie 信息
  • 会话 Cookie:这些是临时的,当用户关闭浏览器时它们就会被删除
  • 持久 Cookie:可以设置过期日期,即使关闭浏览器,它们也会保留,直到超过设定的过期时间
  • 敏感信息不应该直接存储在 Cookie 中,因为它们可以被用户查看和修改。存储在 Cookie 中的应该是一些非敏感信息或加密的会话标识符
  • 单个 cookie 保存的数据不能超过 4K,很多浏览器都限制一个域名最多保存 50 个 cookie

除了通过document.cookie访问cookie外,一般也可以引入封装好的cookie库进行使用

总结

cookie、localStorage 和 sessionStorage 的异同点:

存储方式生命周期存储容量存储位置
cookie默认保存在内存中,随浏览器关闭失效。如果设置过期时间,在到过期时间后失效4KB保存在客户端,每次请求时都会带上
localStorage理论上永久有效的,除非主动清除。约5MB(不同浏览器情况不同)保存在客户端,不与服务端交互。节省网络流量
sessionStorage仅在当前网页会话下有效,关闭页面或浏览器后会被清除约5MB(不同浏览器情况不同)保存在客户端,不与服务端交互。节省网络流量