1.异步加载JS的⽅式有哪些?
1. 使用<script>
标签的属性
- defer属性:
-
- 特点:
defer
属性会告诉浏览器立即下载脚本,但延迟执行,直到整个页面都解析完毕后再运行。这意味着脚本会按照它们在文档中出现的顺序执行。 - 兼容性:
defer
属性在所有主流浏览器中均受支持,包括IE。 - 示例:
<script defer src="script.js"></script>
- 特点:
- async属性:
-
- 特点:
async
属性是HTML5新增的,用于指定脚本异步执行。当脚本可用时,浏览器会立即下载并执行,但执行顺序可能与它们在文档中出现的顺序不同。 - 兼容性:
async
属性在Chrome、Firefox、Safari、Edge等现代浏览器中受支持,但在IE8及以下版本中不被支持。 - 示例:
<script async src="script.js"></script>
- 特点:
2. 动态创建<script>
标签
- 方法:通过JavaScript动态创建
<script>
元素,并将其添加到DOM中。这种方法提供了最大的灵活性,允许你根据需要动态地加载和执行JavaScript代码。 - 示例:
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'path/to/your/script.js';
document.head.appendChild(script);
4. 利用window.onload
事件
- 特点:
window.onload
事件会在整个页面(包括所有依赖的资源如图片、样式表和子框架)完全加载后触发。虽然这不是专门用于异步加载JS的,但你可以在这个事件处理函数中执行需要等待页面完全加载后才能运行的JS代码。 - 示例:
window.onload = function() {
// 你的代码
};
2.异步编程的实现⽅式
1. 回调函数(Callbacks)
回调函数是最早的异步编程方式。当某个任务完成后,调用一个指定的函数来继续执行后续操作。
function fetchData(url, callback) {
// 假设这是一个异步的数据获取操作
setTimeout(() => {
const data = '这里是数据';
callback(data);
}, 1000);
}
fetchData('https://example.com/data', (data) => {
console.log(data);
});
2. Promises
Promises 是解决异步回调地狱(Callback Hell)的一种方案。它代表了一个最终可能完成或失败的操作及其结果值。
function fetchData(url) {
return new Promise((resolve, reject) => {
// 假设这是一个异步的数据获取操作
setTimeout(() => {
const data = '这里是数据';
resolve(data); // 异步操作成功时调用
// reject(new Error('失败')); // 异步操作失败时调用
}, 1000);
});
}
fetchData('https://example.com/data')
.then(data => {
console.log(data);
})
.catch(error => {
console.error(error);
});
3. async/await
async/await
是建立在Promises之上的,它使得异步代码的管理趋向同步代码。
async
函数会隐式地返回一个Promise,而await
表达式会暂停async
函数的执行,等待Promise处理完成后再继续执行async
函数并返回结果。
async function fetchData(url) {
try {
const response = await fetch(url); // 假设fetch函数返回了一个Promise
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
fetchData('https://example.com/data');
4. 事件监听(Event Listeners)
事件监听也是一种非常自然的异步处理方式。
document.getElementById('myButton').addEventListener('click', function() {
// 处理点击事件
console.log('按钮被点击了');
});
5. Generators
Generator函数是 ES6 提供的一种异步编程解决方案, async是该方案的语法糖
3.同步和异步的区别
同步(Synchronous)
- 定义:同步编程意味着代码的执行顺序与书写顺序完全一致。
- 特点:
-
- 顺序执行:代码按照书写顺序从上到下依次执行。
- 阻塞性:如果某段代码执行时间较长(如等待用户输入、进行网络请求等),则整个程序会暂停执行,直到该段代码执行完毕。
- 适用场景:执行非耗时任务。
- 示例:
console.log('开始');
let result = someSynchronousFunction(); // 假设这是一个同步函数
console.log(result);
console.log('结束');
异步(Asynchronous)
- 定义:代码不会等待当前操作,而是继续执行后续代码,当操作完成时,通过回调机制处理结果。
- 特点:
-
- 非顺序执行:代码的执行顺序可能与书写顺序不一致。
- 非阻塞性:即使某个操作需要较长时间,程序也不会等待该操作完成,而是继续执行后续代码。
- 适用场景:适用于耗时任务,如网络请求、文件读写等。
- 示例(使用 Promise):
console.log('开始');
someAsynchronousFunction()
.then(result => {
console.log(result);
})
.then(() => {
console.log('结束');
});
console.log('不会被阻塞');
4.说说你对promise的了解
Promise是JavaScript中用于处理异步操作的一种机制,它提供了一种优雅的方式来处理异步任务。帮助我们避免回调地狱(Callback Hell)的问题,使异步代码更加清晰、易于管理和维护。
1. 基本概念
- 定义:Promise是一个容器,里面保存着某个未来才会结束的事件。从语法上说,Promise是一个对象,通过它可以获取异步操作的消息。
- 状态:Promise主要有三种状态,分别是pending(进行中)、fulfilled(完成)、rejected(失败)。Promise的状态只能从pending转变为fulfilled或rejected,且一旦转变就不能再改变。
2. 创建Promise
Promise对象可以通过两种方式创建:
- 构造函数方式:通过
new Promise(executor)
来创建一个Promise实例,其中executor
是一个函数,它接受两个参数:resolve
和reject
。当异步操作成功时,调用resolve(value)
;当异步操作失败时,调用reject(error)
。 - 静态方法:Promise提供了
Promise.resolve(value)
和Promise.reject(reason)
两个静态方法,分别用于快速创建一个已经处于fulfilled状态或rejected状态的Promise实例。
3. 使用Promise
- then方法:
then
方法用于指定当Promise状态改变时要执行的操作。接受两个可选的回调函数作为参数:第一个回调函数是成功(fulfilled)时被调用的,第二个回调函数是失败(rejected)时被调用的(注意:第二个回调函数是可选的)。 - catch方法:
catch
方法是then(null, rejection)
的语法糖,用于捕获Promise中的错误。它只处理rejected的情况。 - finally方法:
finally
方法用于指定不管Promise最终状态如何都会执行的操作。这为在Promise操作完成时执行清理工作提供了一种方便的方式。
4. 链式调用
Promise支持链式调用,即可以在一个then
方法中返回一个新的Promise,然后在其后继续调用then
或catch
方法。这使得异步操作的流程控制变得非常灵活和强大。
5. Promise的静态方法
- Promise.all(iterable):接受一个Promise对象的数组作为参数,并返回一个新的Promise实例。这个新的Promise实例会在所有传入的Promise对象都成功解决后解决,其结果为所有传入的Promise对象的结果数组。如果任何一个传入的Promise对象失败,则新的Promise实例会立即失败,其结果为第一个失败的Promise对象的结果。
- Promise.race(iterable):同样接受一个Promise对象的数组作为参数,但返回的Promise实例会在传入的Promise对象中任何一个解决(无论成功还是失败)后立即解决,其结果为第一个解决的Promise对象的结果。
5.js延迟加载的⽅式有哪些
JavaScript(JS)延迟加载是一种优化网页加载速度的技术
1. 使用async
属性
当<script>
标签设置了async
属性时,浏览器会异步下载脚本,不会阻塞页面的其他操作。下载完成后,就会立即执行。适用于独立运行的脚本。
<script src="myscript.js" async></script>
2. 使用defer
属性
设置了defer
属性的脚本会异步下载, 但是会等到文档完全解析和显示之后才执行,保证脚本的执行不会影响页面的加载, 并且会按照脚本在文档中出现的顺序执行。
<script src="myscript.js" defer></script>
3. 动态创建<script>
标签
通过JavaScript动态创建<script>
标签并插入到DOM中,可以控制脚本的加载时机。
var script = document.createElement('script');
script.src = 'myscript.js';
document.head.appendChild(script);
4. 按需加载资源
Intersection Observer API可以观察元素是否进入视口(即用户的可视区域),并在满足条件时执行相关代码或加载资源。这对于图片、视频等资源的延迟加载特别有用,可以显著提升页面性能。
const observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
// 元素进入视口,加载资源
// ...
observer.unobserve(entry.target); // 停止观察该元素
}
});
});
const targetElement = document.querySelector('.target');
observer.observe(targetElement);
5. 代码分割
对于工程化的程序,可以使用打包工具(如Webpack)来按需加载模块。这些工具通常支持代码分割,可以将代码分割成多个块,并在需要时动态加载它们。
6. 延迟执行函数
使用setTimeout
或setInterval
等定时器函数也可以实现简单的延迟加载。
setTimeout(function() {
// 执行延迟加载的代码或加载资源
}, 2000); // 延迟2000毫秒后执行
总结
J对于简单的脚本加载,可以使用async
或defer
属性;
对于复杂的应用程序,可能需要使用模块加载器或打包工具的代码分割功能;
而对于需要根据用户交互条件加载的资源,可以使用Intersection Observer API等更高级的技术。
6.如何理解this关键字
this
的值并不是在编写时静态确定的,而是在运行时基于函数的调用方式动态绑定的
1. 默认绑定
当函数不是作为某个对象的方法被调用时,this
指向全局对象(在浏览器中是 window
,在Node.js中是 global
)。但在严格模式('use strict')下,this
会保持为 undefined
。
function foo() {
console.log(this);
}
foo(); // windows
'use strict';
function bar() {
console.log(this);
}
bar(); // undefined
2. 隐式绑定
如果函数是对象的方法,并且被对象调用,那么 this
指向这个对象。
var obj = {
foo: function() {
console.log(this);
}
};
obj.foo(); // obj
3. 显式绑定
通过 call()
、apply()
或 bind()
方法,可以显式地设置 this
的值。
call()
和apply()
方法是调用时设置this
的值bind()
方法是创建一个新的函数
function baz() {
console.log(this);
}
var obj = { a: 2 };
// 调用时修改
// cal方法和apply方法接受参数的方法不同
baz.call(obj); // obj
baz.apply(obj); // obj
// 创建时修改
var boundBaz = baz.bind(obj);
boundBaz(); // obj
4. 箭头函数中的 this
箭头函数不绑定自己的 this
,它会捕获所在上下文的 this
值,作为自己的 this
值。
var obj = {
foo: function() {
setTimeout(() => {
console.log(this); // 捕获了 foo 的 this
}, 100);
}
};
obj.foo(); // obj
7.call和apply的区别?
相同: 都可以在调用函数时修改this的指向
不同: call方法传参用逗号隔开, apply方法传参用数组
运行机制
单线程
什么是单线程,和异步的关系
单线程
JS是单线程模型,程序的执行是线性的,即一次只能执行一个任务,任务完成后才能执行下一个任务。这样设计的好处是可以避免DOM渲染的冲突, 缺点会存在阻塞的问题
异步
异步编程是一种与同步编程相对的概念。在异步编程中,程序的执行不会等待, 耗时任务执行后, 继续执行后面的代码,当耗时的任务完成后,通过回调机制通知主线程。
关系
在JavaScript中,主执行线程是单线程的,但JavaScript引擎会利用后台线程来处理耗时的任务。这些后台线程完成操作后,会通过事件循环(Event Loop)和回调函数(或Promise或async/await)将结果返回给主线程,从而实现了非阻塞的异步编程。
8.说说 event loop
Event Loop即事件循环,是JavaScript在单线程中运行异步代码的机制。它允许JavaScript在执行耗时任务时,不会阻塞其他任务的执行。
二、工作原理
- 任务队列:
-
- JS中的任务可以分为同步任务和异步任务,
- 异步任务会放任务队列中, 遵循先进先出的原则。
- 异步任务分为宏任务(定时器/延时器/Ajax/DOM操作)和微任务(Promise.then, await/async)。
- 执行流程:
-
- JavaScript运行时,首先会执行同步代码。
- 当同步代码全部执行完毕,Event Loop会检查并执行微任务队列中的任务。
- 当所有的微任务执行完毕,在执行宏任务。
- 重复上述过程,直到所有任务都被执行完毕。
9.JS 原型链,原型链的顶端是什么?Object 的原型是什么?
在JavaScript中,每个对象都有一个内部链接指向另一个对象, 这个对象我们称之为原型(prototype)。
通过这个链接,一个对象可以访问其原型的属性和方法, 这种查找链条称为原型链。
原型链的顶端
Object.prototype
是大多数对象的原型链的终点,
Object的原型
Object.prototype
的原型是null
。
10.实现继承的关键字
ES6及以后, 通过class
关键字和extends
关键字来实现的
11.new的执行过程
JS 的 new 操作符做了哪些事情 *
- 创建一个新的空对象
- 将对象与构建函数通过原型链连接起来
- 让 this 指向这个新的对象
- 执行构造函数后返回这个对象。
12.JS 中继承实现的几种方式
- 原型链继承(Prototype Inheritance)
将一个对象的原型设置为另一个对象的实例,可以实现继承
function Animal(name) {
this.name = name;
}
Animal.prototype.sayName = function() {
console.log(this.name);
};
function Dog(name) {
Animal.call(this, name); // 借用构造函数继承属性
}
Dog.prototype = new Animal(); // 原型链继承方法
Dog.prototype.constructor = Dog; // 修复constructor指向
Dog.prototype.bark = function() {
console.log('Woof!');
};
var dog = new Dog('Buddy');
dog.sayName(); // Buddy
dog.bark(); // Woof!
注意:上述代码同时使用了借用构造函数和原型链继承,但此处我们主要关注原型链继承的部分。
- 构造函数继承(Constructor Inheritance)
在子类的构造函数中调用父类的构造函数,并将父类的this指向子类的实例,可以继承父类实例的属性。这种方式不会继承父类原型上的属性和方法。
function Animal(name) {
this.name = name;
}
function Dog(name) {
Animal.call(this, name); // 借用构造函数继承
}
var dog = new Dog('Buddy');
console.log(dog.name); // Buddy
- 组合继承(Combination Inheritance)
就是原型链继承和构造函数继承的组合。这种方式既可以在子类实例上继承父类的实例属性,也可以继承父类原型上的方法。(已在原型链继承示例中展示)
- 原型式继承(Prototypal Inheritance)
通过复制现有的对象来创建新对象,然后基于这个新对象来创建新的原型链。
可以使用Object.create()
方法来实现。
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
console.log(anotherPerson.friends); // ["Shelby", "Court", "Van"]
- 寄生式继承(Parasitic Inheritance)
寄生式继承是创建一个仅用于封装继承过程的函数,该函数在内部来增强对象,最后再返回对象。这种方式通常不会用于创建新类型的对象,而是给对象添加一些额外的功能。
- 寄生组合式继承(Parasitic Combination Inheritance)
借用构造函数来继承属性,通过原型链来继承方法。这种方式可以避免调用两次父类构造函数。
function inheritPrototype(subType, superType) {
var prototype = Object.create(superType.prototype); // 创建对象,以父类原型为原型
prototype.constructor = subType; // 增强对象,将constructor指向子类
subType.prototype = prototype; // 将子类原型指向新创建的对象
}
// 使用inheritPrototype来继承
13.⾯向对象编程思想
面向对象编程思想(Object-Oriented Programming, 简称OOP)是一种软件开发方法,它将真实世界中的事物抽象成一个个对象,通过对象之间的交互实现软件系统的功能。其核心特性包括封装、继承和多态。
面向对象编程思想的核心特性
- 封装:
-
- 封装是指将数据(属性)和方法(操作数据的行为)封装在一起,形成一个独立的对象。
- 外部程序只能通过对象提供的接口(即公共方法)来访问和操作对象内部的数据,而不能直接访问其内部结构。
- 封装提高了代码的安全性和可维护性,因为它保护了对象的数据不被外部程序直接修改,同时也隐藏了对象的内部实现细节。
- 继承:
-
- 继承是一种允许子类继承现有父类的属性和方法的机制。
- 子类可以扩展或修改继承来的属性和方法,从而创建出具有特定功能的类。
- 继承提高了代码的复用性,同时也使得类之间的关系更加清晰
- 多态:
-
- 多态是指同一种类型的对象在不同的情况下可以表现出不同的行为。
- 多态可以通过方法重载和方法重写来实现。方法重载是指在同一个类中定义多个同名的方法,但这些方法的参数类型或数量不同;方法重写则是指子类重新实现了父类中已有的方法,在子类中调用这个方法时,会优先调用子类的实现。
- 多态使得程序能够更加灵活地应对不同的情况,提高了程序的可扩展性。
14.什么是⾯向对象编程及⾯向过程编程,它们的异同和优缺点
面向对象编程(OOP)
定义:
面向对象它围绕对象来组织软件,这些对象相互协作来实现软件的功能。
特点:
- 封装:将数据和操作数据的方法封装在对象中,提高安全性和模块化程度。
- 继承:允许子类继承父类的属性和方法,实现代码复用。
- 多态:允许对象在不同情况下有不同的表现,是通过方法重载和方法重写实现的
优势:
- 适合多人协作的大型项目
面向过程编程(POP)
定义:
面向过程关注的是解决问题的步骤和顺序。它通过调用不同的功能的方法来实现软件的目标。
特点:
- 线性流程:将一个大任务分解成多个可执行的函数,按照程序执行的先后顺序排列。
- 数据与函数分离:函数通常接受数据作为输入参数,并且可能返回结果,数据和操作数据的过程相对独立。
- 重用:通过函数模块化实现代码重用,但粒度一般较粗。
优势:
- 适合快速开发的小型项目。