1.JS原生常用dom操作方法有哪些
JavaScript 提供了许多原生 DOM 操作方法,用于创建、修改、删除和查询 DOM 元素。以下是常用的一些原生 DOM 操作方法:
- getElementById():通过元素的 ID 获取对应的 DOM 元素。
const element = document.getElementById('myElement');
- getElementsByClassName():通过元素的类名获取所有匹配的 DOM 元素集合。
const elements = document.getElementsByClassName('myClass');
- getElementsByTagName():通过元素的标签名获取所有匹配的 DOM 元素集合。
const elements = document.getElementsByTagName('div');
- querySelector():通过 CSS 选择器获取第一个匹配的 DOM 元素。
const element = document.querySelector('.myClass');
- querySelectorAll():通过 CSS 选择器获取所有匹配的 DOM 元素集合。
const elements = document.querySelectorAll('div');
- createElement():创建一个新的 DOM 元素节点。
const newElement = document.createElement('div');
- appendChild():将一个元素节点作为子节点添加到另一个元素节点中。
parentElement.appendChild(childElement);
- removeChild():从其父节点中删除指定的子节点。
parentElement.removeChild(childElement);
- setAttribute():设置元素节点的属性。
element.setAttribute('attributeName', 'attributeValue');
- getAttribute():获取元素节点的属性值。
const value = element.getAttribute('attributeName');
- classList:用于操作元素节点的 class 属性,提供了添加、删除、切换和包含 class 的方法。
element.classList.add('className'); element.classList.remove('className'); element.classList.toggle('className'); element.classList.contains('className');
- innerHTML:用于设置或获取元素节点的 HTML 内容。
element.innerHTML = '<p>Hello</p>'; const htmlContent = element.innerHTML;
- innerText / textContent:用于设置或获取元素节点的文本内容。
element.innerText = 'Hello'; const textContent = element.innerText;
- style:用于设置元素节点的样式属性。
element.style.color = 'red';
这些是常用的原生 DOM 操作方法,可以帮助你在 JavaScript 中对 DOM 元素进行增删改查和样式操作。需要注意的是,DOM 操作可能会导致重绘和回流,因此应该尽量减少 DOM 操作的频次,以提高性能。
2.ES6的新增特性有哪些
ES6(ECMAScript 2015)是 ECMAScript 的第六个版本,引入了许多新特性和语法改进,丰富了 JavaScript 的功能和表达能力。以下是 ES6 中的一些新增特性:
- let 和 const 声明:引入了块级作用域的变量声明方式。
let
声明的变量具有块级作用域,而const
声明的变量是常量,不可被重新赋值。 - 箭头函数:使用箭头 (
=>
) 符号定义函数,简化了函数的写法,并且自动绑定了函数上下文。 - 模板字符串:使用反引号 (`) 定义字符串,可以在字符串中嵌入变量和表达式,更方便地拼接字符串。
- 解构赋值:允许从数组或对象中提取值,并赋给多个变量,简化了变量赋值的过程。
- 默认参数值:允许在函数参数中指定默认值,简化了函数调用时的参数传递。
- 展开运算符:使用三个点 (
...
) 对数组或对象进行展开,方便地将它们组合或复制。 - 类和继承:引入了类和继承的语法糖,使得在 JavaScript 中使用面向对象编程更加直观和方便。
- 模块化:引入了
import
和export
关键字,支持模块化的开发方式,帮助将代码拆分成独立的模块。 - Promise:引入了 Promise 对象,用于更方便地处理异步操作,解决了回调地狱的问题。
- Generator:引入了 Generator 函数,可以产生一系列的值,并支持在函数执行过程中暂停和恢复。
- Symbol:引入了 Symbol 数据类型,表示独一无二的值,可以用作对象属性的唯一标识符。
- Map 和 Set:引入了 Map 和 Set 数据结构,提供了更好的键值对存储和集合操作。
- Iterator 和 for…of:引入了迭代器和 for…of 循环,用于遍历数据结构的值。
- Proxy 和 Reflect:引入了 Proxy 和 Reflect 对象,用于拦截对象的操作并自定义其行为。
- Async/Await:引入了 async/await 关键字,用于更直观地处理异步操作,基于 Promise 实现异步编程。
这些是 ES6 中的一些主要新增特性,它们使得 JavaScript 更强大、更灵活,同时提升了开发效率和代码可读性。随着 ES6 的普及和广泛支持,它们已经成为现代 JavaScript 开发中常用的特性。
3.JS设计模式有哪些
JavaScript 设计模式是在编程中常用的一些通用解决方案,它们有助于提高代码的可重用性、可维护性和可扩展性。以下是一些常见的 JavaScript 设计模式:
- 单例模式 (Singleton Pattern):确保一个类只有一个实例,并提供一个全局访问点。
- 工厂模式 (Factory Pattern):通过工厂方法来创建对象,将对象的创建和使用解耦,提供更灵活的对象创建方式。
- 构造函数模式 (Constructor Pattern):使用构造函数创建对象,并通过原型链来实现继承。
- 模块模式 (Module Pattern):使用闭包封装私有变量和方法,将相关功能组织为一个独立的模块。
- 发布-订阅模式 (Publish-Subscribe Pattern):定义了一种依赖关系,当一个对象的状态发生变化时,所有依赖它的对象都会得到通知并自动更新。
- 观察者模式 (Observer Pattern):一种一对多的依赖关系,当一个对象的状态发生变化时,所有依赖它的对象都会得到通知。
- 策略模式 (Strategy Pattern):定义一系列算法,并将它们封装成独立的类,使得它们可以互相替换。
- 命令模式 (Command Pattern):将请求封装成对象,使得可以用不同的请求来参数化其他对象,并支持请求的排队和记录。
- 代理模式 (Proxy Pattern):控制对其他对象的访问,允许在目标对象的访问前后进行一些额外的操作。
- 装饰器模式 (Decorator Pattern):动态地给对象添加额外的职责,比继承更灵活地扩展对象功能。
- 适配器模式 (Adapter Pattern):将一个类的接口转换成客户端所期望的另一个接口,使得原本由于接口不兼容而不能一起工作的类可以在一起工作。
- 迭代器模式 (Iterator Pattern):提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。
这些设计模式在 JavaScript 中都有应用场景,可以根据具体需求和情况选择合适的设计模式来提高代码的质量和可维护性。值得注意的是,并非所有场景都需要使用设计模式,适度使用设计模式是为了增加代码的灵活性和可扩展性。
4.对JS面向对象的理解
在 JavaScript 中,面向对象编程(Object-Oriented Programming,OOP)是一种编程范式,它以对象为基本单元,通过封装、继承和多态等特性来组织和管理代码。JavaScript 是一门支持面向对象编程的多范式语言,它可以使用原型链实现对象之间的继承和共享。
理解 JavaScript 中的面向对象编程包括以下几个方面:
- 对象:对象是 JavaScript 中的基本数据类型之一,它是一组键值对的集合。对象可以包含属性和方法,属性是对象的状态,而方法是对象的行为。
- 构造函数:构造函数是一种特殊的函数,用于创建和初始化对象。使用
new
关键字调用构造函数可以实例化一个对象,并且构造函数内部的this
指向新创建的对象。 - 原型:每个 JavaScript 对象都有一个原型对象(
prototype
),它是一个普通的对象,包含共享的属性和方法。通过原型链,对象可以继承其原型对象的属性和方法。 - 继承:继承是面向对象编程中的重要概念,它允许一个对象继承另一个对象的属性和方法。在 JavaScript 中,可以使用原型链实现继承。
- 封装:封装是一种将数据和操作封装在对象内部,只暴露有限接口给外部访问的机制。通过闭包或者访问控制符(例如 ES6 中的类修饰符)可以实现对象的封装。
- 多态:多态是指同一个接口可以对应多种实现方式。在 JavaScript 中,由于动态类型的特性,不需要特定的语法来实现多态。
在 JavaScript 中,面向对象编程可以使用原型继承实现,也可以使用 ES6 中引入的 class
关键字和 extends
关键字来实现类和继承。例如:
使用构造函数和原型链的方式:
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.sayHello = function() { console.log(`Hello, my name is ${this.name}, I'm ${this.age} years old.`); }; const person1 = new Person('Alice', 30); person1.sayHello();
使用 ES6 的类和继承的方式:
class Person { constructor(name, age) { this.name = name; this.age = age; } sayHello() { console.log(`Hello, my name is ${this.name}, I'm ${this.age} years old.`); } } const person2 = new Person('Bob', 25); person2.sayHello();
在现代 JavaScript 开发中,面向对象编程是一种非常常见的编程范式,它使代码更易于组织、维护和扩展,也提高了代码的可重用性和可读性。
5.JS数组常用方法(至少6个)
JavaScript 中的数组提供了许多常用的方法,用于对数组进行操作、遍历和转换等。以下是至少六个常用的数组方法:
- push():将一个或多个元素添加到数组的末尾,并返回新数组的长度。
const fruits = ['apple', 'banana']; fruits.push('orange', 'grape'); console.log(fruits); // 输出: ['apple', 'banana', 'orange', 'grape']
- pop():移除数组的最后一个元素,并返回被移除的元素。
const fruits = ['apple', 'banana', 'orange']; const removedFruit = fruits.pop(); console.log(fruits); // 输出: ['apple', 'banana'] console.log(removedFruit); // 输出: 'orange'
- shift():移除数组的第一个元素,并返回被移除的元素。该操作会改变数组的长度。
const fruits = ['apple', 'banana', 'orange']; const removedFruit = fruits.shift(); console.log(fruits); // 输出: ['banana', 'orange'] console.log(removedFruit); // 输出: 'apple'
- unshift():将一个或多个元素添加到数组的开头,并返回新数组的长度。
const fruits = ['banana', 'orange']; fruits.unshift('apple', 'grape'); console.log(fruits); // 输出: ['apple', 'grape', 'banana', 'orange']
- indexOf():返回指定元素在数组中第一次出现的索引,如果没有找到,则返回 -1。
const fruits = ['apple', 'banana', 'orange']; const index = fruits.indexOf('banana'); console.log(index); // 输出: 1
- slice():从原数组中截取指定位置的元素,返回一个新的数组。它不会修改原数组。
const fruits = ['apple', 'banana', 'orange', 'grape']; const slicedFruits = fruits.slice(1, 3); console.log(slicedFruits); // 输出: ['banana', 'orange']
7.splice():在指定位置插入、删除或替换数组的元素,可修改原数组。
const fruits = ['apple', 'banana', 'orange']; fruits.splice(1, 1, 'grape'); // 从索引1位置删除1个元素,并在该位置插入'grape' console.log(fruits); // 输出: ['apple', 'grape', 'orange']
8.join():将数组中的所有元素以指定的分隔符连接成一个字符串。
const fruits = ['apple', 'banana', 'orange']; const joinedFruits = fruits.join(', '); console.log(joinedFruits); // 输出: 'apple, banana, orange'
9.concat():将多个数组合并成一个新数组,不会修改原数组。
const fruits = ['apple', 'banana']; const moreFruits = ['orange', 'grape']; const allFruits = fruits.concat(moreFruits); console.log(allFruits); // 输出: ['apple', 'banana', 'orange', 'grape']
10.forEach():对数组的每个元素执行指定的回调函数。
const fruits = ['apple', 'banana', 'orange']; fruits.forEach((fruit, index) => { console.log(`Fruit at index ${index}: ${fruit}`); }); // 输出: // Fruit at index 0: apple // Fruit at index 1: banana // Fruit at index 2: orange
11.filter():使用指定的函数过滤数组中的元素,并返回过滤后的新数组。
const numbers = [1, 2, 3, 4, 5]; const evenNumbers = numbers.filter(num => num % 2 === 0); console.log(evenNumbers); // 输出: [2, 4]
12.map():使用指定的函数对数组中的每个元素进行处理,并返回处理后的新数组。
const numbers = [1, 2, 3, 4, 5]; const squaredNumbers = numbers.map(num => num * num); console.log(squaredNumbers); // 输出: [1, 4, 9, 16, 25]
13.sort():对数组的元素进行排序,可以接受一个比较函数用于指定排序规则。
const fruits = ['banana', 'apple', 'orange']; fruits.sort(); console.log(fruits); // 输出: ['apple', 'banana', 'orange'] const numbers = [10, 2, 5, 8, 1]; numbers.sort((a, b) => a - b); console.log(numbers); // 输出: [1, 2, 5, 8, 10]
14.some():判断数组中是否至少有一个元素满足指定条件,返回布尔值。
const numbers = [1, 2, 3, 4, 5]; const hasEvenNumber = numbers.some(num => num % 2 === 0); console.log(hasEvenNumber); // 输出: true
15.every():判断数组中是否所有元素都满足指定条件,返回布尔值。
const numbers = [1, 2, 3, 4, 5]; const allEvenNumbers = numbers.every(num => num % 2 === 0); console.log(allEvenNumbers); // 输出: false
这些方法涵盖了 JavaScript 数组中一些常用的操作,它们在实际开发中非常有用,并可以帮助我们对数组进行处理、筛选、转换和排序等操作。
6.从输入URL到页面加载完中间发生了什么?
从输入URL到页面加载完整的过程可以分为以下几个步骤:
- URL 解析和DNS查询:当用户在浏览器中输入URL时,浏览器会解析URL,提取出协议、主机名、端口号、路径等信息。然后,浏览器会进行DNS查询,将主机名转换为对应的IP地址,以便浏览器能够访问服务器。
- 建立 TCP 连接:浏览器使用IP地址和端口号与服务器建立TCP连接。TCP是一种可靠的传输协议,它确保数据的有序传输和可靠接收。
- 发起 HTTP 请求:一旦建立了TCP连接,浏览器会向服务器发送HTTP请求,请求页面的资源(如HTML、CSS、JavaScript、图片等)。
- 服务器处理请求并返回响应:服务器收到浏览器的HTTP请求后,会根据请求的内容进行处理,然后返回相应的HTTP响应。响应包含了请求的资源和状态码等信息。
- 浏览器接收响应:浏览器接收服务器返回的HTTP响应,根据响应头中的内容进行处理,比如根据Content-Type确定响应是HTML还是其他类型的资源。
- 解析HTML和构建DOM树:一旦浏览器接收到HTML响应,它会开始解析HTML代码,并构建DOM树。DOM树是由HTML中的元素和它们之间的关系构成的树状结构,表示了页面的结构。
- 加载和执行CSS和JavaScript:浏览器在解析HTML过程中,如果遇到外部CSS和JavaScript文件的引用,会再次发起HTTP请求获取这些资源,并在获取到后加载和执行它们。
- 渲染页面:浏览器在构建DOM树和加载执行CSS和JavaScript的过程中,会根据这些信息计算页面的布局,并将页面渲染到用户的屏幕上。
- 页面加载完毕:当所有的资源都加载完成并且页面渲染完毕,浏览器会触发
DOMContentLoaded
事件,表示页面已经加载完毕。接着,浏览器可能会继续加载一些异步加载的资源,如图片或者通过JavaScript动态加载的内容。
需要注意的是,以上过程是简化的描述,实际加载过程可能会受到网络速度、服务器性能、缓存等因素的影响。现代的浏览器和服务器会采取一系列优化策略,使页面加载速度更快和用户体验更好。
7.JS事件代理(也称事件委托)是什么,及实现原理?
事件代理(或事件委托)是一种在 JavaScript 中常用的事件处理机制,它利用了事件冒泡的特性,将事件处理程序绑定在父元素上,而不是直接绑定在子元素上。通过捕获和冒泡阶段,父元素可以捕获到子元素触发的事件,并执行相应的事件处理程序。这样做的好处是可以减少事件处理程序的数量,提高性能,同时还能处理动态添加的子元素。
实现事件代理的原理是,当一个子元素触发了某个事件,事件会向上冒泡至父元素。父元素通过监听事件冒泡阶段(通常使用事件委托在捕获阶段),判断事件的目标元素是否是它的子元素,如果是,则执行相应的事件处理程序。
下面是一个简单的示例来说明事件代理的实现原理:
HTML 结构:
<ul id="parentList"> <li>Item 1</li> <li>Item 2</li> <li>Item 3</li> </ul>
JavaScript 代码:
const parentList = document.getElementById('parentList'); // 通过事件代理,在父元素上监听点击事件 parentList.addEventListener('click', function(event) { // 判断事件的目标元素是否是 li 元素 if (event.target.tagName === 'LI') { console.log('点击了:', event.target.textContent); } });
在上面的例子中,我们将点击事件的监听器绑定在父元素 parentList
上,而不是直接绑定在每个子元素 li
上。当点击任意一个 li
元素时,事件会向上冒泡至父元素,父元素通过判断事件的目标元素是否是 li
元素来确定点击的是哪个子元素,并执行相应的事件处理程序。
事件代理的优点包括:
- 减少事件处理程序的数量:通过将事件处理程序绑定在父元素上,避免了给每个子元素都绑定事件处理程序的麻烦。
- 动态添加元素支持:对于通过 JavaScript 动态添加的元素,事件代理可以自动为新添加的元素绑定事件处理程序。
- 简化代码结构:通过事件代理,可以将事件处理逻辑集中在父元素上,使代码结构更清晰和易于维护。
需要注意的是,事件代理只适用于可以冒泡的事件(如 click、change 等),对于不能冒泡的事件(如 focus、blur 等),事件代理是不起作用的。因此,在使用事件代理时,需要根据实际情况选择合适的事件类型。
8.说一下call,apply,bind区别
call
、apply
和 bind
都是 JavaScript 中用于改变函数执行上下文(即函数内部的 this
指向)的方法。它们的区别如下:
- call():
- 语法:
function.call(thisArg, arg1, arg2, ...)
- 功能:
call()
方法可以调用一个函数,并且将一个指定的对象绑定为函数执行时的this
值。thisArg
是要绑定的对象,后续的参数是传递给函数的参数列表。 - 使用:适用于参数数量已知的情况,参数需逐个列出。
示例:
function greet(name) { console.log(`Hello, ${name}! I'm ${this.title}.`); } const person = { title: 'Mr' }; greet.call(person, 'Alice'); // 输出:Hello, Alice! I'm Mr.
- apply():
- 语法:
function.apply(thisArg, [argsArray])
- 功能:
apply()
方法和call()
类似,可以调用一个函数,并且将一个指定的对象绑定为函数执行时的this
值。不同的是,apply()
的参数是一个数组或类数组对象,它将数组中的元素作为参数传递给函数。 - 使用:适用于参数数量不确定的情况,可以通过数组来传递参数。
示例:
function greet(name, age) { console.log(`Hello, ${name}! I'm ${this.title}. I'm ${age} years old.`); } const person = { title: 'Mr' }; const args = ['Alice', 30]; greet.apply(person, args); // 输出:Hello, Alice! I'm Mr. I'm 30 years old.
- bind():
- 语法:
function.bind(thisArg, arg1, arg2, ...)
- 功能:
bind()
方法不会调用函数,而是创建一个新的函数,并将指定的对象绑定为新函数执行时的this
值。后续的参数是传递给新函数的参数列表。 - 使用:适用于需要创建一个新的函数,并固定部分参数的情况。
示例:
function greet(name) { console.log(`Hello, ${name}! I'm ${this.title}.`); } const person = { title: 'Mr' }; const greetPerson = greet.bind(person, 'Alice'); greetPerson(); // 输出:Hello, Alice! I'm Mr.
总结:
call()
和apply()
在函数执行时立即调用,而bind()
返回一个新的函数,并可以在后续的任意时刻调用。call()
和apply()
的参数传递方式不同,call()
是逐个列出参数,apply()
是使用数组传递参数。bind()
可以固定部分参数,返回一个新函数,不会立即执行。
9.===和= =有什么不同?
===
和 ==
是 JavaScript 中用于比较值的操作符,它们有以下不同:
- 严格相等运算符(===):
- 语法:
a === b
- 功能:
===
运算符比较两个值是否相等,并且要求值的类型也必须相等。如果值和类型都相等,则返回true
,否则返回false
。
示例:
console.log(5 === 5); // 输出:true console.log('5' === 5); // 输出:false,因为一个是字符串类型,一个是数值类型
- 相等运算符(==):
- 语法:
a == b
- 功能:
==
运算符比较两个值是否相等,但它在比较前会进行类型转换。如果两个值的类型不同,会尝试将其中一个值转换成另一个值的类型,然后再进行比较。这种类型转换可能导致一些意想不到的结果。
示例:
console.log(5 == 5); // 输出:true console.log('5' == 5); // 输出:true,因为字符串 '5' 会被转换成数值 5 进行比较 console.log(null == undefined); // 输出:true,null 和 undefined 在相等比较时会被转换成相同的值 console.log(true == 1); // 输出:true,true 被转换成数值 1 进行比较 console.log(true == '1'); // 输出:true,字符串 '1' 被转换成数值 1 进行比较 console.log(true == 'true'); // 输出:false,因为字符串 'true' 不能被转换成数值
由于 ==
运算符会进行类型转换,它可能会导致一些不直观的结果,因此在大多数情况下,推荐使用 ===
运算符进行比较,因为它在比较时不会进行类型转换,更加严谨和安全。只有在明确需要进行类型转换的情况下,才使用 ==
运算符。
#10.JS微任务和宏任务
在 JavaScript 中,任务可以分为两种类型:微任务(Microtask)和宏任务(Macrotask)。它们都是异步编程中用于处理延迟执行的机制,但它们之间有一些重要的区别。
- 微任务(Microtask):
- 微任务是一个需要在当前任务执行结束后立即执行的任务。它会优先于下一个宏任务执行。
- 微任务通常包括 Promise 的回调函数、
MutationObserver
的回调函数等。 - 微任务的执行时机在每个宏任务执行结束后,当前执行栈清空之前。
- 如果一个宏任务中产生了微任务,那么这些微任务会在同一个宏任务中依次执行,直到所有微任务执行完毕。
示例:
console.log('Start'); // 宏任务:setTimeout setTimeout(() => { console.log('Timeout'); }, 0); // 微任务:Promise Promise.resolve().then(() => { console.log('Promise'); }); console.log('End'); // 输出顺序: // Start // End // Promise // Timeout
- 宏任务(Macrotask):
- 宏任务是一个需要在当前任务执行结束后尽可能快地执行的任务。它会在微任务之后执行。
- 宏任务通常包括
setTimeout
、setInterval
、requestAnimationFrame
、事件回调等。 - 宏任务的执行时机在每个宏任务执行结束后,下一个宏任务执行前。
示例:
console.log('Start'); // 宏任务:setTimeout setTimeout(() => { console.log('Timeout'); }, 0); console.log('End'); // 输出顺序: // Start // End // Timeout
总结:
- 微任务主要用于处理异步操作的回调,它在当前任务执行结束后尽快执行,并在所有微任务执行完毕后才执行下一个宏任务。
- 宏任务主要用于处理用户交互、定时器等,它会在当前任务执行结束后进行执行,并在所有微任务执行完毕后才执行下一个宏任务。
- 微任务优先级高于宏任务,即微任务会在下一个宏任务之前执行。
- 在一个宏任务中产生的微任务,会在同一个宏任务中依次执行。
在实际应用中,理解微任务和宏任务的执行顺序对于处理异步代码的正确性非常重要,特别是在处理需要确保执行顺序的场景下。