1.1 浏览器中的5个进程(浏览器、插件、渲染、网路、前四个一个页面打开必备、GPU)
1.2 浏览器发送 HTTP 请求的流程(B/S模式)(注意:http协议是基于tcp/ip协议的,http协议是应用层协议,tcp/ip协议是通信层协议,也就是tcp提供的是通信的管道。)
通过上图第一次请求可以看出,当服务器返回HTTP响应头给浏览器时,浏览器通过响应头的Cache-Control字段来设置是否缓存该资源。
Cache-Control:Max-age=2000 //缓存过期时间是2000
这也就意味着,在该缓存资源还没有过期的情况下,如果再次发送请求该资源,会之间返回缓存中的资源给浏览器。
但如果缓存过期了,浏览器则会继续发送网络请求,并且在HTTP请求头中带上:
If-None-Match:"4f80f-13c-3a1xb12a"
简要来说,很多网站第二次访问能够秒开,是因为这些网站把很多资源都缓存在了本地,浏览器缓存直接使用本地副本来回应请求,而不会产生真实的网络请求,从而节省了时间。同时,DNS 数据也被浏览器缓存了,这又省去了 DNS 查询环节。
1.3 输入url地址到浏览器显示页面发生了什么
从上图可以看到,整个过程需要各个进程之间的配合,我们结合上图我们从进程的角度,描述一下
1、浏览器进程接收到用户输入的URL请求,浏览器进程便将URL转发给网络进程。
2、网络进程中发起真正的URL请求。
3、网络进程接收到响应头数据,便解析响应头数据,并将数据转发给浏览器进程。
4、浏览器进程接收到网络进程的响应头数据之后,发送"提交文档"消息到渲染进程。
5、渲染进程接收到"提交文档"的消息之后,便开始准备接收HTML数据,接收数据的方式是直接和网络进程建立数据管道。
6、等文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程。
7、浏览器进程接收到渲染进程"确认提交"的消息之后,便开始移除之前旧的文档,然后更新浏览器进程中的页面状态。
所谓提交文档,就是浏览器主进程,将网络进程接收到的HTML数据提交给渲染进程。
2.1. typeof():我们知道typeof在判断变量类型的时候比较适合用来处理基本数据类型,如果是引用类型的值,typeof恐怕就心有余而力不足了。
console.log(typeof ['h', 'e', 'l', 'l', 'o']); //object
console.log(typeof { name: 'susu', age: 3 }); //object
console.log(typeof null); //object
console.log(typeof new Date()); //object复制代码
2.2.instanceof: 一个对象在原型链上是否存在一个构造函数的prototype属性,通过instanceof很容易就能判断一个变量是数组还是对象,貌似比typeof要高级了一些。但如果遇到数组,情况可能跟我们想象的不一样了。
let arr = [1, 2, 3];
console.log(arr instanceof Array); //true
console.log(arr instanceof Object); //true复制代码
instanceof的判断规则:是通过__proto__和prototype能否找到同一个引用对象。通过打印下面的等式,我们就能知道为什么上面的栗子都会打印出true
console.log(arr.__proto__.__proto__ === Object.prototype); //true
console.log(arr.__proto__ === Array.prototype); //true复制代码
2.3.constructor:此属性返回对创建此对象的数组函数的引用
let arr = [1, 2, 3];
let obj = { name: 'susu', age: 3 };
console.log(arr.constructor === Array); //true
console.log(arr.constructor === Object); //false
console.log(obj.constructor === Array); //false
console.log(obj.constructor === Object); //true
2.4.Object.prototype.toString.call():在js中该方法可以精准的判断对象类型,也是推荐使用的方法。
可以判断基本数据类型:
console.log(Object.prototype.toString.call(3)); //[object Number]
console.log(Object.prototype.toString.call(true)); //[object Boolean]
console.log(Object.prototype.toString.call(null)); //[object Null]
console.log(Object.prototype.toString.call('hello')); //[object String]
console.log(Object.prototype.toString.call(undefined)); //[object Undefined]复制代码
也可以判断引用数据类型:
let arr = [1, 2, 3];
let obj = {name: 'susu', age: 3};
let date = new Date();
function fn(){console.log('hello world!')};
console.log(Object.prototype.toString.call(arr)); //[object Array]
console.log(Object.prototype.toString.call(obj)); //[object Object]
console.log(Object.prototype.toString.call(date)); //[object Date]
console.log(Object.prototype.toString.call(fn)); //[object Function]复制代码
3.1、ES6的新特性有那些?
1、定义函数: es6
var human =
{
breathe(name){// 不需要function也能定义breathe函数
console.log(name+'is breathing...')
}};
human.breathe('jarson') //输出'jarson is breathing...'
//js
var human ={
breathe : funtionn (name){
console.log(name+ 'is breathing...')
}
};
human.breathe('jarson')
2、创建类
//es6
class Human {
constructor(name){
this.name= name;
}
breathe (){
console.log(this.name + 'is breathing...')
}
}
var man = new Human ('jarson');
man.breathe();//jarson
// js
function Human (name){
this.name = name ;
this.breathe =function (){
console.log(this.name + 'is breathing...')
}
}
var man = new Human('jarson');
man.breathe();
继承父类 :注意 super()是父类的构造函数
//es6
class Man extends Human{
constructor (name,sex){
super(name);
this.sex=sex;
}
info(){
console.log(this.name+'is'+this.sex);
}
}
var xx= new Man ('jarson','boy');
xx.breathe();// jarson is breathing
xx.info();//arsonis boy
3.模板
es6,js原生支持modules。将不同功能的代码分别写在不同文件中,各模块只需导出export 公共接口部分。然后在需要使用的地方通过模块的导入 import 就可以
内联导出
export class Human{
constructor(name){this.name =name}
breathe(){console.log(this.name+'is breathe...')}
}
export function run(){ console.log('i am runing')}
function eat(){console.log('i am eating')}
导出一组对象
class Human{
construcotor(name){this.name=name}
breathe(){console.log(this.name+'is breathe')}
}
function run(){
console.log('i am runing')
}
function eat(){
console.log('i am eating')
}
export{Human, run }
无对象导入
import './modules.js'
导入默认对象
import app from './modules.js'
如果导出一组对象
import {Human, run } from './app.js'
4.let 、const
for (let i=0;i<2;i++){
console.log(i)//0,1
}
const 定义变量;无法被更改的变量
var、let、const 区别:
用var关键字定义的变量:
用let关键字定义变量:
结果和var一样,函数体内和函数体外输出的内容都是对应的,互不影响,且也可以重新赋值
证明不论在全局作用域下还是在局部作用域,let关键字定义的变量是不允许重新定义的
用const关键字定义常量
此时,用const关键字定义的常量和上面的情况一一样,都是正常的
此时,程序还没有被运行,代码提示就会报红,所以说,常量是不能被重复定义并赋值的
此时我只是重新赋值了a,并没有重新定义,虽然代码提示没有报红,但在控制台中,就已经出现了错误,同样,即使是在函数中定义赋值常量并再次赋值,也是和上述一样,所以说,常量是不能被重新定义和赋值的
但是const定义的对象属性可以改变
当const对象是引用类型时,之保持栈里边的储存地址不变,堆里的值随便修改
若保持对象当中的属性值不变,改变属性;是不被允许的。栈中的地址一旦发生改变则会报错
若修改对象当中的属性值,只是修改堆中的内容是可以被允许的
const aa=true
aa=false
console.log(aa)
// 报错 VM1089:2 Uncaught TypeError: Assignment to constant variable.
const aaa=3
aaa=5
console.log(aaa)
// 报错 VM1152:2 Uncaught TypeError: Assignment to constant variable.
const aaaa='ss'
aaaa='dd'
console.log(aaaa)
// 报错 VM1194:2 Uncaught TypeError: Assignment to constant variable.
const aaaaa={aa:'dd'}
aaaaa={aa:'bb'}
console.log(aaaaa)
//报错 VM1257:2 Uncaught TypeError: Assignment to constant variable.
const aaaaaa={aa:'dd'}
aaaaaa.aa='bb'
console.log(aaaaaa)
//正确 {aa: "bb"}
栈中用来存储地址,堆则用来存储真正的数据
5、箭头函数
let arr=[6,7,8,29,10];
arr.forEach((item,i)=>{console.log(item,i)})
let newArr=arr.filter((item)=>{item<10})
防抖:触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间
应用场景:提交按钮、用户注册时候的手机号验证、邮箱验证
节流:高频事件触发,但在n秒内只会执行一次,所以节流会稀释函数的执行频率。
应用场景: window对象的resize、scroll事件 拖拽时候的mousemove 射击游戏中的mousedown、keydown事件 文字输入、自动完成的keyup事件
防抖怎么做:
function debounce(fn, delay) {
let timer = null;
return function () {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, arguments);
}, deply)
}
}
节流怎么做:
function throttle(fn, delay) {
let valid = true;
return function () {
if (valid) { //如果阀门已经打开,就继续往下
setTimeout(() => {
fn.apply(this, arguments); //定时器结束后执行
valid = true; //执行完成后打开阀门
}, delay)
valid = false; //关闭阀门
}
}
}
// 刚开始valid为true,然后将valid重置为false,进入了定时器,在定时器的时间期限之后,才会将valid重置为true,valid为true之后,之后的点击才会生效
// 在定时器的时间期限内,valid还没有重置为true,会一直进入return,就实现了在N秒内多次点击只会执行一次的效果
//用法:
function fn(value) {
console.log(value);
}
var throttleFunc = throttle(fn, 2000); //节流函数
//事件处理函数,按钮点击事件
btn.addEventListener("click", function () {
throttleFunc(Math.random()); // 给节流函数传参
})
Vue的双向绑定原理是使用了数据劫持和发布-订阅模式相结合的方式实现的。
数据劫持:Vue使用了Object.defineProperty()方法来实现数据劫持,即通过重写对象的get和set方法,在数据被读取或者修改时,触发相应的方法,从而实现对数据的监控和控制。
发布-订阅模式:Vue通过一个中央观察者(即watcher)来实现数据的发布和订阅。当数据发生变化时,触发对应的watcher,然后通知订阅者进行相应的更新操作。
关键点:
数据劫持:通过重写对象的get和set方法,实现对数据的监控和控制。
发布-订阅模式:通过watcher实现数据的发布和订阅,即当数据发生变化时,通知订阅者进行相应的更新操作。
模板编译:将模板编译成渲染函数,然后通过渲染函数将数据渲染到视图中。
组件化:Vue将视图封装成组件,每个组件都有自己的状态,通过父子组件之间的数据传递和事件触发,实现整个应用的状态管理和组件通信。
父设:display:grid; 给子:align-self:center;justify-self:center
父:display:flex; justify-content:center;align-items:center;
absolute + transform (过渡)
absolute + calc (计算)
absolute + 负 margin (首先知道子元素的宽高,给子元素设置top:50%;left:50%,但绝对定位是基于子元素的左上角,我们所希望的效果是子元素的中心居中示。借助外边距的负值,负的外边距可以让元素向相反方向定位。通过指定子元素的外边距为子元素宽度一半的负值,就可以让子元素居中了)
absolute + margin auto(首先知道子元素的宽高,需要将各个方向的距离都设0,再将margin设为auto)
1):first-letter:向文本的第一个字母添加特殊样式。
2):first-line: 向文本的首行添加特殊样式。
3):before:在元素之前添加内容。(这个前端必会)
4):after:在元素之后添加内容。(这个前端必会)
5)::placeholder:匹配占位符的文本,只有元素设置了placeholder属性时,该伪元素才能生效。(只支持双冒号的形式)。
6)::selection:CSS伪元素应用于文档中被用户高亮的部分(比如使用鼠标或其他选择设备选中的部分)。(只支持双冒号的形式)。
7)::backdrop(处于试验阶段):用于改变全屏模式下的背景颜色,全屏模式的默认颜色为黑色。(只支持双冒号的形式)。
(1)通过媒体查询的方式即CSS3的meida queries
@media screen and (max-width: 600px) { /*当屏幕尺寸小于600px时,应用下面的CSS样式*/
/*你的css代码*/
}
(2)以天猫首页为代表的 flex 弹性布局
高度定死,宽度自适应,元素都采用px做单位。
随着屏幕宽度变化,页面也会跟着变化,效果就和PC页面的流体布局差不多,在哪个宽度需要调整的时候使用响应式布局调调就行(比如网易新闻),这样就实现了『适配』。
(3)以淘宝首页为代表的 rem+viewport缩放
实现原理
根据rem将页面放大dpr倍, 然后viewport设置为1/dpr.
如iphone6 plus的dpr为3, 则页面整体放大3倍, 1px(css单位)在plus下默认为3px(物理像素)
然后viewport设置为1/3, 这样页面整体缩回原始大小. 从而实现高清。
这样整个网页在设备内显示时的页面宽度就会等于设备逻辑像素大小,也就是device-width。
这个device-width的计算公式为:设备的物理分辨率/(devicePixelRatio * scale),
在scale为1的情况下,device-width = 设备的物理分辨率/devicePixelRatio 。
(4)rem 方式
viewport也是固定的:
。
cookie可以设置过期时间 路径 domin
1.cookie在浏览器和服务器间来回传递。而sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存。
2.存储大小限制也不同,
cookie数据不能超过4k,同时因为每次http请求都会携带cookie,所以cookie只适合保存很小的数据,
如会话标识。sessionStorage和localStorage 虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大。
3.数据有效期不同,sessionStorage:仅在当前浏览器窗口关闭前有效,自然也就不可能持久保持;
localStorage:始终有效,窗口或浏览器关闭也一直保存,因此用作持久数据;cookie在设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭。
Es6新增构造函数set、数组迭代
深拷贝|浅拷贝
Vue生命周期
组件通信
Vuex
watch、computend
Vue Router
v-for v-if为啥不能同时使用 ,怎么解决
在 Vue 中,v-for 和 v-if 指令确实不能同时使用在同一个元素上,这是因为它们的优先级不同,会导致渲染结果出现意外的错误。
具体来说,v-for 指令优先级比 v-if 指令高,当它们同时出现在同一个元素上时,Vue 会先根据 v-for 指令生成多个元素,然后再根据 v-if 指令过滤掉不符合条件的元素。这可能会导致一些性能问题,特别是在列表很长时。
解决这个问题的方法有两种:
使用计算属性代替 v-if 指令
可以将需要过滤的列表项放到一个计算属性中,并在模板中使用 v-for 指令渲染整个列表。然后在计算属性中使用条件语句来过滤不符合条件的列表项。
例如:
{{ item.name }}
使用包装元素代替 v-if 指令
可以将需要过滤的元素放到一个包装元素中,并在包装元素上使用 v-if 指令来控制是否显示。然后在包装元素内部使用 v-for 指令渲染整个列表。
例如:
{{ item.name }}
这两种方法都可以避免 v-for 和 v-if 同时使用的问题,具体选择哪种方法取决于具体的场景和需求。
闭包:闭包我们要清楚函数作用域、内存回收机制、作用域继承。
实现:函数中的自由变量,取决于函数定义的地方,跟执行的地方没关系
// 函数作为返回值
function create() {
const a = 100
return function () {
console.log(a)
}
}
const fn = create()
const a = 200
fn() // 100
// 函数作为参数被传递
function print(fn) {
const a = 200
fn()
}
const a = 100
function fn() {
console.log(a)
}
print(fn) // 100
Vue.js 是一款流行的前端 JavaScript 框架,它的主要特点是数据驱动和组件化。Vue.js 有两个主要版本:Vue 2.0 和 Vue 3.0,它们之间有以下区别:
性能优化:Vue 3.0 的性能比 Vue 2.0 更好,主要是因为 Vue 3.0 使用了新的响应式系统,可以更好地利用 JavaScript 引擎的优化能力,同时还引入了编译时优化和 Tree shaking 等新特性。
Composition API:Vue 3.0 引入了 Composition API,这是一种新的 API 风格,它可以更好地组织和重用逻辑代码。相比之下,Vue 2.0 的 API 风格是基于选项的。
TypeScript 支持:Vue 3.0 对 TypeScript 的支持更好,包括更好的类型推导和更严格的类型检查。
指令和组件:Vue 3.0 引入了一些新的指令和组件,例如 v-model 的多个绑定值、 组件、 组件等等。
其他特性:Vue 3.0 还引入了一些其他特性,例如全局 API 的修改、响应式数据的改进、虚拟 DOM 的改进等等。
总体来说,Vue 3.0 在性能、API 设计、TypeScript 支持等方面都有很大的提升,但是它也带来了一些新的学习成本和迁移成本。如果你已经熟悉了 Vue 2.0,那么学习 Vue 3.0 应该不会太困难。
v-if 和 v-show 都是 Vue.js 中用于控制元素显示和隐藏的指令,它们的区别如下:
v-if 是“真正的”条件渲染,它根据表达式的值的真假来销毁或重建元素。如果条件为假,则元素不会被渲染到 DOM 中,如果条件为真,则元素会被渲染到 DOM 中。因此,v-if 在切换时有更高的切换开销,但在初始渲染时有更低的初始渲染开销。
v-show 是简单的条件切换,它只是通过修改元素的 display 样式来切换元素的可见性。如果条件为假,则元素会被隐藏,但仍然会存在于 DOM 中,如果条件为真,则元素会显示。因此,v-show 在切换时有更低的切换开销,但在初始渲染时有更高的初始渲染开销。
因此,如果需要频繁地切换元素的可见性,应该使用 v-show;如果在运行时条件很少改变,则使用 v-if 更合适。
添加 key 属性的原因是为了帮助 Vue.js 更好地跟踪每个节点的身份,从而实现高效的更新策略。在没有 key 属性的情况下,当列表中的顺序发生变化时,Vue.js 会尝试就地复用已有元素来优化渲染,但这可能会导致意外的行为。例如,可能会出现一个元素的状态被复制到了另一个元素上的情况。
通过为每个元素或组件添加唯一的 key 属性,Vue.js 可以更好地跟踪它们的身份。当列表中的顺序发生变化时,Vue.js 可以快速定位到每个元素或组件,并决定是否复用它们或销毁并重新创建它们。这样可以确保列表的渲染结果始终正确,并且可以提高性能。
因此,使用 v-for 渲染列表时,建议为每个被渲染的元素或组件添加一个唯一的 key 属性。key 的值应该是每个元素或组件在列表中的唯一标识符。例如,可以使用每个元素或组件的 ID 作为 key 的值。
封装一个 Vue.js 组件通常需要以下步骤:
定义组件:使用 Vue.component() 方法或者单文件组件的方式定义一个组件。在组件中,可以定义组件的属性、数据、方法和生命周期钩子等。
编写模板:在组件中,可以编写模板来描述组件的外观和行为。模板可以使用 Vue.js 的模板语法,包括插值、指令、事件绑定等。
定义属性:在组件中,可以定义属性来接收外部传入的数据。属性可以通过 props 选项来定义,它可以指定属性的类型、默认值、验证规则等。
发射事件:在组件中,可以使用 $emit() 方法来发射事件,从而向外部传递消息。在组件中,可以使用 $on() 方法来监听事件,从而接收消息。
使用插槽:在组件中,可以使用插槽来允许外部传入内容。插槽可以使用 slot 标签来定义,它可以指定插槽的名称和默认内容。
定义样式:在组件中,可以定义样式来控制组件的外观。样式可以使用 CSS 或者 CSS 预处理器来编写。
测试组件:在开发组件时,应该编写测试用例来验证组件的行为。测试用例可以使用 Jest 等测试框架来编写。
综上所述,封装一个 Vue.js 组件需要熟悉 Vue.js 的组件 API 和模板语法,以及良好的编码习惯和测试实践。通过良好的封装实践,可以使组件具有更好的可重用性、可维护性和可测试性。
示如何封装一个基本的按钮组件:
这个按钮组件具有以下特点:
支持传入 type 和 size 属性,用于控制按钮的颜色和尺寸;
支持插槽,可以在按钮中插入任意内容;
支持发射 click 事件,可以在外部监听按钮的点击事件;
支持样式隔离,使用了 scoped 样式。
这个按钮组件可以在其他 Vue.js 应用中被重复使用,从而提高开发效率和代码复用性。
技术选型:在项目开始之前,我们需要选择合适的技术栈和框架来实现项目。这个过程需要考虑多个因素,例如项目需求、团队技能、开发成本等。如果选择不当,可能会导致后续开发过程中遇到各种问题。
需求变更:在项目开发过程中,客户或用户可能会提出新的需求或修改旧的需求。这个过程需要及时响应和调整,否则可能会导致项目进度延误或质量问题。
代码质量:在项目开发过程中,我们需要保证代码质量和可维护性,以便后续维护和升级。这个过程需要遵循一些编码规范和最佳实践,例如代码风格、注释、单元测试等。
性能优化:在项目开发过程中,我们需要保证系统的性能和稳定性。这个过程需要进行一些性能优化和测试,例如缓存、压缩、CDN 加速等。
团队协作:在项目开发过程中,团队成员之间需要进行有效的沟通和协作,以便高效地完成任务。这个过程需要遵循一些团队协作规范和流程,例如代码评审、会议记录、任务分配等。
当我们在浏览器的地址栏中输入一个 URL(Uniform Resource Locator)时,浏览器会执行以下操作:
解析 URL:浏览器会解析 URL,将其分解成协议、主机名、端口号、路径等组成部分。
DNS 解析:如果 URL 中包含主机名,浏览器会将主机名转换为 IP 地址。这个过程称为 DNS 解析,浏览器会向 DNS 服务器发送请求,获取主机名对应的 IP 地址。
建立 TCP 连接:浏览器会通过 TCP 协议与服务器建立连接。这个过程称为三次握手,客户端和服务器之间会交换一些数据包,以确保连接的可靠性。
发送 HTTP 请求:一旦建立了 TCP 连接,浏览器就会向服务器发送 HTTP 请求。HTTP 请求由请求行、请求头和请求体组成,其中请求行包含请求方法、URL 和协议版本等信息。
接收 HTTP 响应:服务器收到 HTTP 请求后,会返回 HTTP 响应。HTTP 响应由状态行、响应头和响应体组成,其中状态行包含响应状态码和协议版本等信息。
渲染页面:一旦浏览器接收到 HTTP 响应,它会根据响应中的 HTML、CSS 和 JavaScript 等内容来渲染页面。这个过程包括解析 HTML、构建 DOM 树、计算样式、布局和绘制等操作。
断开 TCP 连接:当浏览器完成页面渲染后,它会关闭与服务器的 TCP 连接。这个过程称为四次挥手,客户端和服务器之间会交换一些数据包,以确保连接的正确关闭。
以上是 URL 到浏览器的一个大致过程,其中涉及到许多细节和技术细节,例如缓存、压缩、SSL 加密等。希望这些信息能对您有所帮助。
实现一个小程序的 request 封装及拦截,可以通过以下步骤:
创建一个 request 封装函数:首先,我们需要创建一个封装函数,用于发送请求并返回响应结果。在这个函数中,我们可以设置默认的请求头、请求方法、请求参数等信息,以便在发送请求时使用。
创建一个拦截器:拦截器是一个函数,用于在发送请求或接收响应时进行拦截和处理。我们可以在拦截器中对请求或响应进行一些处理,例如添加请求头、修改请求参数、处理错误等。
设置拦截器:在创建封装函数时,我们可以设置拦截器,以便在发送请求或接收响应时调用拦截器。拦截器可以是一个或多个函数,它们按照设置的顺序依次执行。
下面是一个简单的示例代码:
// 创建封装函数
function request(url, options = {}) {
// 设置默认请求头
const headers = {
'content-type': 'application/json'
}
// 合并请求参数
const opts = Object.assign({}, options, { headers })
// 发送请求
return new Promise((resolve, reject) => {
wx.request({
url,
...opts,
success: res => {
// 调用响应拦截器
if (opts.responseInterceptor) {
res = opts.responseInterceptor(res)
}
resolve(res)
},
fail: err => {
// 调用错误拦截器
if (opts.errorInterceptor) {
err = opts.errorInterceptor(err)
}
reject(err)
}
})
})
}
// 创建拦截器
function requestInterceptor(req) {
// 在请求头中添加 token
const token = wx.getStorageSync('token')
if (token) {
req.headers.Authorization = `Bearer ${token}`
}
return req
}
function responseInterceptor(res) {
// 处理响应结果
if (res.statusCode !== 200) {
throw new Error('请求失败')
}
return res.data
}
// 设置拦截器
request.interceptors = {
request: requestInterceptor,
response: responseInterceptor
}
// 使用封装函数发送请求
request('https://example.com/api/getData', {
method: 'GET',
data: { id: 1 },
responseInterceptor: res => {
// 处理响应结果
return res.data
},
errorInterceptor: err => {
// 处理错误信息
console.error(err)
return err
}
}).then(res => {
console.log(res)
}).catch(err => {
console.error(err)
})
在上面的示例代码中,我们创建了一个名为 request 的封装函数,用于发送请求。在发送请求时,我们可以设置请求头、请求方法、请求参数等信息。同时,我们还创建了两个拦截器函数 requestInterceptor 和 responseInterceptor,用于在发送请求或接收响应时进行拦截和处理。在使用封装函数时,我们可以设置拦截器,以便在发送请求或接收响应时调用拦截器。
如果在 Vue 项目中不使用 Vue 框架,我们可以使用原生的 JavaScript 和浏览器提供的 API 来实现一个类似于 Vue 的封装。
以下是一个简单的示例代码,用于实现一个类似于 Vue 的双向绑定功能:
// 定义一个双向绑定类
class TwoWayBinding {
constructor(options) {
this.el = options.el
this.data = options.data
this.init()
}
// 初始化函数
init() {
// 解析 DOM 树,实现双向绑定
this.compile(this.el)
}
// 解析 DOM 树
compile(el) {
// 遍历 DOM 树,查找指令和插值表达式
const nodes = el.childNodes
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (node.nodeType === 1) {
// 处理指令
this.compileDirective(node)
// 继续遍历子节点
this.compile(node)
} else if (node.nodeType === 3) {
// 处理插值表达式
this.compileText(node)
}
}
}
// 处理指令
compileDirective(node) {
const attrs = node.attributes
for (let i = 0; i < attrs.length; i++) {
const attr = attrs[i]
if (attr.name.startsWith('v-')) {
const directive = attr.name.slice(2)
const value = attr.value
switch (directive) {
case 'model':
// 实现双向绑定
node.value = this.data[value]
node.addEventListener('input', e => {
this.data[value] = e.target.value
})
break
// 其他指令的处理
}
}
}
}
// 处理插值表达式
compileText(node) {
const reg = /\{\{(.+?)\}\}/g
const text = node.textContent
if (reg.test(text)) {
node.textContent = text.replace(reg, (_, key) => {
return this.data[key.trim()]
})
}
}
}
// 使用示例
const vm = new TwoWayBinding({
el: document.querySelector('#app'),
data: {
message: 'Hello, world!'
}
})
在上面的示例代码中,我们定义了一个名为 TwoWayBinding 的类,用于实现双向绑定功能。在类的构造函数中,我们传入了一个 options 对象,包含了要绑定的 DOM 元素和数据对象。在初始化函数中,我们调用了 compile 方法,用于解析 DOM 树并实现双向绑定。
在解析 DOM 树时,我们遍历了所有的节点,查找指令和插值表达式。如果找到了指令,我们就根据指令的类型进行相应的处理;如果找到了插值表达式,我们就替换文本内容,实现数据的动态更新。
在使用示例中,我们创建了一个 TwoWayBinding 实例,并传入了要绑定的 DOM 元素和数据对象。然后,我们就可以在 HTML 中使用指令和插值表达式,实现双向绑定功能。
在 JavaScript 中,每个对象都有一个原型对象,它是一个普通的对象,用于存储共享的属性和方法。当我们访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript 引擎会自动查找该对象的原型对象,以此类推,直到找到一个具有该属性或方法的对象为止。这种查找属性或方法的过程就是原型链。
JavaScript 中的原型链是通过每个对象的 __proto__ 属性实现的。当我们访问一个对象的属性或方法时,JavaScript 引擎会首先在该对象本身的属性中查找,如果没有找到,则会沿着该对象的 __proto__ 属性指向的原型对象继续查找,直到找到一个具有该属性或方法的对象为止。
例如,我们可以通过以下方式创建一个对象和它的原型对象:
// 创建一个原型对象
const personProto = {
sayHello() {
console.log('Hello, world!')
}
}
// 创建一个对象,并将其原型设置为 personProto
const person = Object.create(personProto)
// 在 person 对象中添加一个属性
person.name = 'Alice'
// 访问 person 对象的属性和方法
console.log(person.name) // 输出 "Alice"
person.sayHello() // 输出 "Hello, world!"
在上面的示例代码中,我们首先创建了一个原型对象 personProto,它包含了一个 sayHello 方法。然后,我们通过 Object.create 方法创建了一个对象 person,并将其原型设置为 personProto。最后,我们在 person 对象中添加了一个属性 name,并访问了它的属性和方法。
在访问 person 对象的属性和方法时,JavaScript 引擎会首先查找 person 对象本身的属性和方法,如果没有找到,则会沿着 person 对象的 __proto__ 属性指向的原型对象 personProto 继续查找。由于 personProto 对象中包含了一个 sayHello 方法,因此 JavaScript 引擎会找到该方法并执行。
这就是 JavaScript 中原型和原型链的基本概念。原型和原型链是 JavaScript 中非常重要的概念,对理解 JavaScript 的面向对象编程模型和一些高级特性非常有帮助。
在 JavaScript 中,闭包是指一个函数可以访问其定义时所处的词法作用域中的变量,即使该函数在定义时所处的作用域已经销毁了。闭包是 JavaScript 中非常重要的概念,它可以用于实现许多高级的编程技巧,例如模块化、私有变量等。
闭包的原理可以通过以下示例代码来说明:
function createCounter() {
let count = 0
return function() {
count++
console.log(count)
}
}
const counter1 = createCounter()
const counter2 = createCounter()
counter1() // 输出 1
counter1() // 输出 2
counter2() // 输出 1
counter2() // 输出 2
在上面的示例代码中,我们定义了一个 createCounter 函数,它返回一个函数。在 createCounter 函数中,我们定义了一个局部变量 count,并返回一个匿名函数。这个匿名函数可以访问 count 变量,并对其进行操作。
然后,我们分别调用 createCounter 函数两次,得到两个不同的函数 counter1 和 counter2。当我们调用 counter1 和 counter2 函数时,它们都可以访问 createCounter 函数中定义的 count 变量,并对其进行操作。由于 count 变量是在 createCounter 函数中定义的,所以它们都可以访问同一个 count 变量,而不会相互干扰。
这就是闭包的原理:当一个函数返回另一个函数时,返回的函数可以访问其定义时所处的词法作用域中的变量。在上面的示例代码中,createCounter 函数返回了一个匿名函数,这个匿名函数可以访问 createCounter 函数中定义的 count 变量,因此它是一个闭包。
需要注意的是,由于闭包可以访问其定义时所处的词法作用域中的变量,因此如果闭包中包含对外部变量的引用,这些变量就会一直存在于内存中,直到闭包被销毁。因此,在使用闭包时需要注意内存泄漏的问题。
在计算机编程中,作用域是指程序中变量、函数等名称的可见范围。作用域规定了在哪些地方可以访问某个变量或函数,以及如何解析名称冲突。
在 JavaScript 中,作用域分为全局作用域和局部作用域两种。全局作用域是指在整个程序中都可以访问的变量和函数,而局部作用域是指在某个函数内部定义的变量和函数,只能在该函数内部访问。
JavaScript 中的作用域是基于词法作用域的,也就是说,作用域是由代码中变量和函数声明的位置所决定的。当引擎在执行 JavaScript 代码时,会根据词法作用域规则来确定每个变量和函数的作用域范围。
在 JavaScript 中,如果一个变量在当前作用域中没有被定义,引擎会沿着作用域链向上查找,直到找到该变量为止。作用域链是一个由当前作用域和所有外层作用域的变量对象组成的链表结构,它决定了变量的查找顺序。
以下是一个简单的示例代码,演示了 JavaScript 中的作用域:
let x = 1
function foo() {
let y = 2
function bar() {
let z = 3
console.log(x + y + z)
}
bar()
}
foo() // 输出 6
在上面的示例代码中,我们定义了一个全局变量 x,一个函数 foo,以及一个函数 bar。在函数 bar 中,我们定义了一个局部变量 z,并访问了全局变量 x 和外层函数 foo 中定义的局部变量 y。由于 JavaScript 中的作用域链是由当前作用域和所有外层作用域的变量对象组成的链表结构,因此在函数 bar 中可以访问全局变量 x 和外层函数 foo 中定义的局部变量 y,并且它们的值分别为 1 和 2。
需要注意的是,在 JavaScript 中,变量的作用域是静态的,也就是说,在代码执行之前就已经确定了。因此,在函数内部定义的变量不会影响函数外部的变量,而且在同一作用域中不能定义同名的变量。
在 JavaScript 中,我们可以使用多种方式来操作数组,以下是其中的一些常见方式:
创建数组:可以使用 [] 或 Array() 构造函数来创建一个新数组。例如:
const arr1 = [1, 2, 3] // 使用 [] 创建数组
const arr2 = Array(4, 5, 6) // 使用 Array() 构造函数创建数组
访问数组元素:可以使用下标运算符 [] 来访问数组中的元素。注意,数组的下标从 0 开始,而且如果访问的下标超出了数组的范围,会返回 undefined。例如:
const arr = [1, 2, 3]
console.log(arr[0]) // 输出 1
console.log(arr[3]) // 输出 undefined
修改数组元素:可以使用下标运算符 [] 来修改数组中的元素。例如:
const arr = [1, 2, 3]
arr[0] = 4
console.log(arr) // 输出 [4, 2, 3]
添加元素:可以使用 push()、unshift() 或 splice() 方法来向数组中添加元素。push() 方法可以将一个或多个元素添加到数组的末尾,unshift() 方法可以将一个或多个元素添加到数组的开头,而 splice() 方法可以在任意位置添加元素。例如:
const arr = [1, 2, 3]
arr.push(4)
console.log(arr) // 输出 [1, 2, 3, 4]
arr.unshift(0)
console.log(arr) // 输出 [0, 1, 2, 3, 4]
arr.splice(2, 0, 'a', 'b')
console.log(arr) // 输出 [0, 1, 'a', 'b', 2, 3, 4]
删除元素:可以使用 pop()、shift() 或 splice() 方法来删除数组中的元素。pop() 方法可以删除数组的最后一个元素,shift() 方法可以删除数组的第一个元素,而 splice() 方法可以在任意位置删除元素。例如:
const arr = [1, 2, 3]
arr.pop()
console.log(arr) // 输出 [1, 2]
arr.shift()
console.log(arr) // 输出 [2]
arr.splice(0, 1)
console.log(arr) // 输出 []
数组迭代:可以使用 for 循环、forEach()、map()、filter() 等方法来迭代数组中的元素。例如:
const arr = [1, 2, 3]
// 使用 for 循环迭代数组
for (let i = 0; i < arr.length; i++) {
console.log(arr[i])
}
// 使用 forEach() 方法迭代数组
arr.forEach(function(item) {
console.log(item)
})
// 使用 map() 方法迭代数组并返回新数组
const newArr = arr.map(function(item) {
return item * 2
})
console.log(newArr) // 输出 [2, 4, 6]
// 使用 filter() 方法迭代数组并返回符合条件的元素组成的新数组
const filteredArr = arr.filter(function(item) {
return item > 1
})
console.log(filteredArr) // 输出 [2, 3]
除了以上这些方式,JavaScript 中还有许多其他的数组操作方法,例如 slice()、concat()、reduce() 等,可以根据具体需求选择使用。
keep-alive 是 Vue.js 的一个内置组件,用于缓存组件实例,以便在它们之间切换时保留它们的状态或避免重新渲染。keep-alive 可以用来优化组件的性能,特别是在需要频繁切换组件时。
keep-alive 组件有两个重要的属性:
include:用于指定哪些组件应该被缓存。
exclude:用于指定哪些组件不应该被缓存。
当一个组件被包裹在 keep-alive 组件中时,它的生命周期钩子函数会发生一些变化。具体来说,它会额外触发两个生命周期钩子函数:
activated:在组件被激活时调用。
deactivated:在组件被停用时调用。
除了这两个生命周期钩子函数之外,被缓存的组件还会触发其他常规的生命周期钩子函数,例如 created、mounted、updated 等。
下面是 keep-alive 组件的完整生命周期:
beforeCreate
created
beforeMount
mounted
activated
beforeUpdate
updated
deactivated
beforeDestroy
destroyed
需要注意的是,当一个组件被缓存时,它的 beforeDestroy 和 destroyed 生命周期钩子函数不会被触发,因为组件实例并没有被销毁,而是被缓存起来了。如果需要在组件被缓存时执行一些清理操作,可以使用 deactivated 生命周期钩子函数。
在大多数编程语言中,0.1 + 0.2 的结果并不是精确的 0.3,而是一个非常接近 0.3 的浮点数。这是因为计算机使用二进制来表示浮点数,而在二进制中,一些小数无法被准确表示,例如 0.1 和 0.2。
具体来说,在二进制中,0.1 的表示方式是一个无限循环的小数,而计算机只能使用有限的位数来表示它。因此,计算机在表示 0.1 时会出现一些误差,这个误差会被累加到后续的计算中。同样的,0.2 也存在类似的问题。
要解决这个问题,可以使用一些方法来处理浮点数的精度。例如,可以使用 toFixed() 方法将浮点数转换为指定位数的字符串,并进行精确的比较。另外,一些编程语言也提供了专门用于处理精度问题的库和函数,例如 Python 中的 decimal 模块和 JavaScript 中的 BigNumber.js 库。
下面是使用 toFixed() 方法解决这个问题的示例代码:
const a = 0.1;
const b = 0.2;
const c = 0.3;
console.log((a + b).toFixed(1) === c.toFixed(1)); // true
在这个示例中,我们将浮点数转换为一个保留一位小数的字符串,并进行比较。这样可以避免浮点数误差带来的问题。
在 JavaScript 中,有多种方法可以判断一个变量是否是数组,下面列举其中的几种:
Array.isArray()
Array.isArray() 方法是判断一个变量是否是数组的最简单、最可靠的方法。该方法会返回一个布尔值,表示该变量是否是数组
const arr = [1, 2, 3];
console.log(Array.isArray(arr)); // true
const obj = { a: 1, b: 2 };
console.log(Array.isArray(obj)); // false
instanceof 运算符
instanceof 运算符可以用来判断一个对象是否是某个类的实例。由于数组也是一个对象,因此可以使用 instanceof 运算符来判断一个变量是否是数组。
const arr = [1, 2, 3];
console.log(arr instanceof Array); // true
const obj = { a: 1, b: 2 };
console.log(obj instanceof Array); // false
需要注意的是,instanceof 运算符只能判断对象是否是某个类的实例,而不能判断基本类型的值是否是数组。
Object.prototype.toString.call()
Object.prototype.toString.call() 方法可以返回一个对象的类型字符串,例如 [object Array] 表示数组。因此,可以使用该方法来判断一个变量是否是数组。
const arr = [1, 2, 3];
console.log(Object.prototype.toString.call(arr) === '[object Array]'); // true
const obj = { a: 1, b: 2 };
console.log(Object.prototype.toString.call(obj) === '[object Array]'); // false
需要注意的是,由于该方法返回的字符串是以 [object 类型] 的形式表示的,因此需要将判断条件写成 '[object Array]' 的形式。
综上所述,使用 Array.isArray() 方法是判断一个变量是否是数组的最佳方法,因为它简单、可靠、易于理解。
在 JavaScript 中,有多种方法可以判断一个变量是否是对象,下面列举其中的几种:
typeof 运算符
typeof 运算符可以返回一个变量的类型字符串,例如 'object' 表示对象类型。因此,可以使用该运算符来判断一个变量是否是对象。
const obj = { a: 1, b: 2 };
console.log(typeof obj === 'object'); // true
const arr = [1, 2, 3];
console.log(typeof arr === 'object'); // true
const str = 'hello';
console.log(typeof str === 'object'); // false
需要注意的是,由于 typeof null 返回的是 'object',因此需要特殊处理 null 值。
instanceof 运算符
instanceof 运算符可以用来判断一个对象是否是某个类的实例。由于对象也是一个类,因此可以使用 instanceof 运算符来判断一个变量是否是对象
const obj = { a: 1, b: 2 };
console.log(obj instanceof Object); // true
const arr = [1, 2, 3];
console.log(arr instanceof Object); // true
const str = 'hello';
console.log(str instanceof Object); // false
需要注意的是,由于基本类型的值并不是对象,因此不能使用 instanceof 运算符来判断基本类型的值是否是对象。
Object.prototype.toString.call()
Object.prototype.toString.call() 方法可以返回一个对象的类型字符串,例如 [object Object] 表示对象。因此,可以使用该方法来判断一个变量是否是对象。
const obj = { a: 1, b: 2 };
console.log(Object.prototype.toString.call(obj) === '[object Object]'); // true
const arr = [1, 2, 3];
console.log(Object.prototype.toString.call(arr) === '[object Object]'); // false
const str = 'hello';
console.log(Object.prototype.toString.call(str) === '[object Object]'); // false
需要注意的是,由于该方法返回的字符串是以 [object 类型] 的形式表示的,因此需要将判断条件写成 '[object Object]' 的形式。
综上所述,使用 typeof 运算符是判断一个变量是否是对象的最简单、最常用的方法,但需要特殊处理 null 值。使用 instanceof 运算符和 Object.prototype.toString.call() 方法也可以判断一个变量是否是对象,但相对来说更复杂一些。
const obj = { a: 1, b: 2, c: 3 };
const keys = Object.keys(obj);
console.log(keys); // ['a', 'b', 'c']
Object.values()
Object.values() 方法可以返回一个对象的所有属性值组成的数组。
const obj = { a: 1, b: 2, c: 3 };
const values = Object.values(obj);
console.log(values); // [1, 2, 3]
Object.entries()
Object.entries() 方法可以返回一个对象的所有属性名和属性值组成的二维数组。
const obj = { a: 1, b: 2, c: 3 };
const entries = Object.entries(obj);
console.log(entries); // [['a', 1], ['b', 2], ['c', 3]]
Object.assign()
Object.assign() 方法可以将多个对象合并成一个对象。
const obj1 = { a: 1 };
const obj2 = { b: 2 };
const obj3 = { c: 3 };
const obj4 = Object.assign({}, obj1, obj2, obj3);
console.log(obj4); // { a: 1, b: 2, c: 3 }
Object.freeze()
Object.freeze() 方法可以冻结一个对象,使其不能被修改。
Copy
const obj = { a: 1, b: 2 };
Object.freeze(obj);
obj.a = 3;
console.log(obj); // { a: 1, b: 2 }
数组的常用方法
Array.push()
Array.push() 方法可以向数组的末尾添加一个或多个元素,并返回新数组的长度。
Copy
const arr = [1, 2, 3];
const len = arr.push(4, 5);
console.log(arr); // [1, 2, 3, 4, 5]
console.log(len); // 5
Array.pop()
Array.pop() 方法可以从数组的末尾删除一个元素,并返回被删除的元素。
Copy
const arr = [1, 2, 3];
const last = arr.pop();
console.log(arr); // [1, 2]
console.log(last); // 3
Array.shift()
Array.shift() 方法可以从数组的开头删除一个元素,并返回被删除的元素。
Copy
const arr = [1, 2, 3];
const first = arr.shift();
console.log(arr); // [2, 3]
console.log(first); // 1
Array.unshift()
Array.unshift() 方法可以向数组的开头添加一个或多个元素,并返回新数组的长度。
Copy
const arr = [1, 2, 3];
const len = arr.unshift(0, -1);
console.log(arr); // [-1, 0, 1, 2, 3]
console.log(len); // 5
Array.slice()
Array.slice() 方法可以返回数组的一部分,不会修改原数组。
Copy
const arr = [1, 2, 3, 4, 5];
const subArr = arr.slice(1, 4);
console.log(subArr); // [2, 3, 4]
console.log(arr); // [1, 2, 3, 4, 5]
Array.splice()
Array.splice() 方法可以删除数组的一部分,并可以在删除的位置插入新的元素。
Copy
const arr = [1, 2, 3, 4, 5];
const deleted = arr.splice(1, 3, 'a', 'b', 'c');
console.log(arr); // [1, 'a', 'b', 'c', 5]
console.log(deleted); // [2, 3, 4]
Array.concat()
Array.concat() 方法可以将多个数组合并成一个新数组。
Copy
const arr1 = [1, 2];
const arr2 = [3, 4];
const arr3 =
在 JavaScript 中,创建空数组和空对象有多种方式。
创建空数组的方式
直接使用字面量语法
可以使用字面量语法直接创建一个空数组。
Copy
const arr = [];
使用构造函数
可以使用 Array 构造函数创建一个空数组。
Copy
const arr = new Array();
创建空对象的方式
直接使用字面量语法
可以使用字面量语法直接创建一个空对象。
Copy
const obj = {};
使用构造函数
可以使用 Object 构造函数创建一个空对象。
Copy
const obj = new Object();
使用 Object.create() 方法
可以使用 Object.create() 方法创建一个空对象,该方法的第一个参数是该对象的原型。
Copy
const obj = Object.create(null);
在这种方式下,创建的对象没有原型,因此它不会继承任何属性或方法。
在 JavaScript 中,有些遍历方式会改变数组,有些则不会。
不改变数组的遍历方式
for 循环
使用 for 循环可以遍历数组,但不会改变数组。
Copy
const arr = [1, 2, 3];
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
forEach() 方法
forEach() 方法可以遍历数组,但不会改变数组。
Copy
const arr = [1, 2, 3];
arr.forEach((item) => {
console.log(item);
});
map() 方法
map() 方法可以遍历数组,并返回一个新的数组,但不会改变原数组。
Copy
const arr = [1, 2, 3];
const newArr = arr.map((item) => {
return item * 2;
});
console.log(newArr); // [2, 4, 6]
console.log(arr); // [1, 2, 3]
filter() 方法
filter() 方法可以遍历数组,并返回一个新的数组,但不会改变原数组。
Copy
const arr = [1, 2, 3];
const newArr = arr.filter((item) => {
return item > 1;
});
console.log(newArr); // [2, 3]
console.log(arr); // [1, 2, 3]
reduce() 方法
reduce() 方法可以遍历数组,并返回一个新的值,但不会改变原数组。
Copy
const arr = [1, 2, 3];
const sum = arr.reduce((acc, cur) => {
return acc + cur;
}, 0);
console.log(sum); // 6
console.log(arr); // [1, 2, 3]
改变数组的遍历方式
map() 方法
map() 方法可以遍历数组,并返回一个新的数组,但会改变原数组。
Copy
const arr = [1, 2, 3];
arr.map((item) => {
return item * 2;
});
console.log(arr); // [2, 4, 6]
filter() 方法
filter() 方法可以遍历数组,并返回一个新的数组,但会改变原数组。
Copy
const arr = [1, 2, 3];
arr.filter((item) => {
return item > 1;
});
console.log(arr); // [2, 3]
reverse() 方法
reverse() 方法可以将数组反转,改变原数组。
Copy
const arr = [1, 2, 3];
arr.reverse();
console.log(arr); // [3, 2, 1]
sort() 方法
sort() 方法可以将数组排序,改变原数组。
Copy
const arr = [3, 1, 2];
arr.sort();
console.log(arr); // [1, 2, 3]
Set 和 Map 都是 ES6 中新增的数据结构,用于存储数据集合。
Set
Set 是一种类似于数组的集合,它的成员都是唯一的,没有重复的值。Set 中的值可以是任何类型,包括基本类型和对象引用。通过 new Set() 构造函数创建一个空的 Set,可以通过 add() 方法向 Set 中添加元素,通过 has() 方法判断一个元素是否在 Set 中,通过 delete() 方法删除一个元素,通过 size 属性获取 Set 的大小。
以下是一些常见的 Set 操作:
Copy
const set = new Set();
set.add(1);
set.add(2);
set.add(3);
console.log(set.has(2)); // true
set.delete(2);
console.log(set.size); // 2
set.forEach((value) => {
console.log(value);
});
// 1
// 3
Map
Map 是一种键值对集合,其中每个键都是唯一的。与 Set 不同,Map 中的键和值可以是任何类型,包括基本类型和对象引用。通过 new Map() 构造函数创建一个空的 Map,可以通过 set() 方法向 Map 中添加键值对,通过 get() 方法获取指定键对应的值,通过 has() 方法判断一个键是否在 Map 中,通过 delete() 方法删除一个键值对,通过 size 属性获取 Map 的大小。
以下是一些常见的 Map 操作:
Copy
const map = new Map();
map.set('name', 'Alice');
map.set('age', 18);
map.set('gender', 'female');
console.log(map.get('age')); // 18
map.delete('gender');
console.log(map.size); // 2
map.forEach((value, key) => {
console.log(`${key}: ${value}`);
});
// name: Alice
// age: 18
Set 和 Map 都是非常有用的数据结构,它们提供了高效的数据存储和检索方式,并且可以通过迭代器进行遍历。
Promise 是 JavaScript 中用于处理异步操作的一种机制,它可以将异步操作转换为类似于同步操作的代码结构,使得异步操作更加直观、可读和易于维护。Promise 是 ES6 中新增的语法特性,已经被现代浏览器和 Node.js 等环境广泛支持。
Promise 的基本用法
Promise 对象表示一个尚未完成的异步操作,它有三种状态:
pending:表示异步操作正在进行中,尚未结束;
fulfilled:表示异步操作已经成功完成;
rejected:表示异步操作已经失败。
通过 new Promise() 构造函数可以创建一个 Promise 对象,它接受一个函数作为参数,这个函数称为 executor。executor 函数会立即执行,并且接受两个参数:resolve 和 reject。当异步操作成功完成时,调用 resolve 函数并传递一个结果值;当异步操作失败时,调用 reject 函数并传递一个错误对象。
以下是一个简单的 Promise 示例:
Copy
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Hello, world!');
}, 1000);
});
promise.then((result) => {
console.log(result); // Hello, world!
});
在上面的示例中,我们创建了一个 Promise 对象,并在 executor 函数中使用 setTimeout() 模拟了一个异步操作,1 秒钟后调用了 resolve 函数并传递了一个字符串。然后我们使用 then() 方法注册了一个回调函数,在异步操作成功完成后打印出结果。
Promise 的链式调用
Promise 还支持链式调用,可以将多个异步操作按照顺序串联起来,使得代码更加简洁和易于维护。在 then() 方法中返回一个新的 Promise 对象,可以将当前的异步操作和下一个异步操作连接起来。如果当前的异步操作成功完成,调用 resolve 函数并传递一个结果值,将会触发下一个异步操作;如果当前的异步操作失败,调用 reject 函数并传递一个错误对象,将会直接跳转到 catch() 方法。
以下是一个 Promise 链式调用的示例:
Copy
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
promise
.then((result) => {
console.log(result); // 1
return result + 1;
})
.then((result) => {
console.log(result); // 2
return Promise.resolve(result + 1);
})
.then((result) => {
console.log(result); // 3
throw new Error('Something went wrong!');
})
.catch((error) => {
console.error(error); // Error: Something went wrong!
});
在上面的示例中,我们创建了一个 Promise 对象,并在 executor 函数中使用 setTimeout() 模拟了一个异步操作,1 秒钟后调用了 resolve 函数并传递了一个数字。然后我们使用 then() 方法注册了三个回调函数,它们分别将数字加 1,并返回一个新的 Promise 对象。第二个回调函数使用 Promise.resolve() 方法创建了一个新的 Promise 对象,它的结果值为数字加 2。第三个回调函数抛出了一个错误对象。最后我们使用 catch() 方法捕获了所有的错误,并打印出错误信息。
Promise 的静态方法
除了实例方法外,Promise 还提供了一些静态方法,用于处理多个 Promise 对象的状态和结果。
Promise.all()
Promise.all() 方法接受一个由多个 Promise 对象组成的数组作为参数,返回一个新
1、链式回调
2、同时发起几个异步请求,谁先有结果就用谁的
3、发起多个请求,等到所有请求还有在做一步处理
这三种方式promise是怎么处理的?
是的,Promise 可以解决以下三种常见的异步编程问题:
链式回调:通过 then() 方法实现链式调用,使得异步操作更加直观、可读和易于维护。
同时发起几个异步请求,谁先有结果就用谁的:使用 Promise.race() 方法可以将多个 Promise 对象组合成一个新的 Promise 对象,该对象的状态取决于其中最先解决的 Promise 对象的状态。
发起多个请求,等到所有请求还有在做一步处理:使用 Promise.all() 方法可以将多个 Promise 对象组合成一个新的 Promise 对象,该对象的状态取决于其中所有 Promise 对象的状态,如果其中任意一个 Promise 对象失败,整个新的 Promise 对象也会失败;如果所有 Promise 对象都成功,则新的 Promise 对象的结果值为所有 Promise 对象的结果值组成的数组。
下面分别介绍这三种方式在 Promise 中的实现:
链式回调:通过 then() 方法实现链式调用。每次调用 then() 方法都会返回一个新的 Promise 对象,可以在回调函数中返回一个值或者一个新的 Promise 对象,以便在下一次调用 then() 方法时继续处理。如果回调函数中抛出了一个错误对象,或者返回一个被拒绝的 Promise 对象,当前的 Promise 对象就会被标记为失败状态,并跳转到 catch() 方法。
同时发起几个异步请求,谁先有结果就用谁的:使用 Promise.race() 方法可以将多个 Promise 对象组合成一个新的 Promise 对象。该方法接受一个由多个 Promise 对象组成的数组作为参数,返回一个新的 Promise 对象。当其中任意一个 Promise 对象解决时,新的 Promise 对象就会解决,并返回该 Promise 对象的结果值或状态。如果其中任意一个 Promise 对象失败,新的 Promise 对象就会失败,并返回失败的原因。
发起多个请求,等到所有请求还有在做一步处理:使用 Promise.all() 方法可以将多个 Promise 对象组合成一个新的 Promise 对象。该方法接受一个由多个 Promise 对象组成的数组作为参数,返回一个新的 Promise 对象。当其中所有 Promise 对象都解决时,新的 Promise 对象就会解决,并返回一个由所有 Promise 对象的结果值组成的数组。如果其中任意一个 Promise 对象失败,新的 Promise 对象就会失败,并返回失败的原因。如果数组中没有任何 Promise 对象,则新的 Promise 对象将立即解决,并返回一个空数组。
JavaScript 中,可以使用 call()、apply() 或 bind() 方法来改变一个函数的上下文,从而间接地改变一个变量的上下文。
假设有一个变量 a 和一个函数 foo(),我们要改变 foo() 函数中 this 关键字的上下文为 a,可以按照以下步骤进行操作:
使用 call() 方法:使用 call() 方法可以直接调用函数,并将第一个参数作为函数的上下文,即 this 关键字的值。因此,可以使用以下语法来改变 foo() 函数中 this 关键字的上下文为 a:
Copy
foo.call(a);
使用 apply() 方法:apply() 方法与 call() 方法类似,也可以直接调用函数,并将第一个参数作为函数的上下文。不同之处在于,apply() 方法接受一个数组作为第二个参数,该数组包含了所有需要传递给函数的参数。因此,可以使用以下语法来改变 foo() 函数中 this 关键字的上下文为 a:
Copy
foo.apply(a);
使用 bind() 方法:bind() 方法不会直接调用函数,而是返回一个新的函数,该函数的上下文已经被绑定到指定的值。因此,可以使用以下语法来创建一个新的函数,并将其上下文绑定到 a:
Copy
var newFoo = foo.bind(a);
在上述三种方法中,call() 和 apply() 方法可以直接改变函数的上下文,而 bind() 方法则需要创建一个新的函数。这些方法可以帮助我们在 JavaScript 中更加灵活地操作变量的上下文。
Read this Article
call() 和 apply() 都是 JavaScript 中用于改变函数上下文的方法,它们的作用是相同的,都可以在调用函数时将一个对象指定为函数体内的 this 值,从而改变函数体内部的上下文。
call() 和 apply() 的区别在于它们传递参数的方式不同:
call() 方法传递参数时,需要将每个参数依次列出,作为 call() 方法的参数传入。例如:
Copy
function foo(a, b, c) {
console.log(this, a, b, c);
}
foo.call("hello", 1, 2, 3); // 输出:String { "hello" } 1 2 3
在上面的例子中,call() 方法的第一个参数是要绑定到 this 上下文中的对象,在这里是字符串 "hello"。后面的参数 1、2、3 分别对应函数 foo() 的三个参数。
apply() 方法传递参数时,需要将参数打包成数组,作为 apply() 方法的第二个参数传入。例如:
Copy
function foo(a, b, c) {
console.log(this, a, b, c);
}
foo.apply("hello", [1, 2, 3]); // 输出:String { "hello" } 1 2 3
在上面的例子中,apply() 方法的第一个参数是要绑定到 this 上下文中的对象,在这里是字符串 "hello"。第二个参数是一个数组 [1, 2, 3],其中每个元素分别对应函数 foo() 的三个参数。
因此,call() 和 apply() 的区别在于它们传递参数的方式不同,但它们的作用是相同的,都可以改变函数体内部的上下文。在实际使用中,我们可以根据具体的情况选择使用 call() 或 apply() 方法。
在 Vue.js 中,父子组件的生命周期钩子函数的执行顺序是固定的,可以按照以下顺序进行理解:
父组件 beforeCreate()
父组件 created()
父组件 beforeMount()
子组件 beforeCreate()
子组件 created()
子组件 beforeMount()
子组件 mounted()
父组件 mounted()
其中,父组件的 beforeCreate() 和 created() 钩子函数会在子组件的钩子函数之前执行,而父组件的 beforeMount() 钩子函数会在子组件的 beforeCreate() 和 created() 钩子函数之后执行。子组件的 mounted() 钩子函数会在父组件的 mounted() 钩子函数之前执行。
这个执行顺序的原因是因为 Vue.js 在挂载组件时会先从父组件开始,递归地挂载子组件,然后再依次挂载所有子组件。因此,父组件的生命周期钩子函数会先于子组件的生命周期钩子函数执行。
需要注意的是,如果在父组件中使用了 v-if 或 v-for 等指令来动态渲染子组件,那么子组件的生命周期钩子函数的执行顺序可能会受到影响。在这种情况下,可以使用 key 属性来保证子组件的正确渲染顺序。
面试题:
实现一个方法,检查一个 npm 包的依赖项中有没有存在循环依赖。
不用考虑版本,只考虑包名即可
入参 pkgs 为项目中所有的依赖(包括子依赖)
返回 boolean
pkg 数据结构即为 package.json 内容的数组, 如有三个包 a、b、c 则入参为:
[
{
"name": "a",
"dependencies": {
"b": "^1.0.0"
}
},
{
"name": "b",
"dependencies": {
"c": "^1.0.0"
}
},
{
"name": "c",
"dependencies": {
"a": "^1.0.0"
}
}
]
function hasCircularDependency(pkgs) {
const graph = {};
for (const pkg of pkgs) {
const name = pkg.name;
const dependencies = pkg.dependencies || {};
graph[name] = Object.keys(dependencies);
}
const visited = new Set();
function dfs(node, ancestors) {
if (visited.has(node)) {
return ancestors.includes(node);
}
visited.add(node);
ancestors.push(node);
for (const neighbor of graph[node] || []) {
if (dfs(neighbor, ancestors)) {
return true;
}
}
ancestors.pop();
return false;
}
for (const node of Object.keys(graph)) {
if (dfs(node, [])) {
return true;
}
}
return false;
}
要求: 产出一个数组, 将输入数组转化为二层树状结构, 把parendId为0的输入项添加到输出数组第一层,每个节点的children存放的是parentId与该节点的id相同。
注意: 二层结构, children中的项不再有children
// 输入项
const itemList = [
{
id: 4,
paramName: '供应链属性',
parentId: 0
},
{
id: 2,
paramName: '供应链属性',
parentId: 4
},
{
id: 5,
paramName: '供应链属性',
parentId: 0
},
{
id: 6,
paramName: '供应链属性',
parentId: 5
}
];
buildTree(itemList);
/**
* 补充下面函数,函数返回示例如下
* @param {ItemList} arr
* @return {ItemTreeNode[]}
**/
function buildTree(arr) {
const tree = [];
const map = {};
for (const item of arr) {
const id = item.id;
const parentId = item.parentId;
if (!map[id]) {
map[id] = { children: [] };
}
map[id] = { ...item, children: map[id].children };
const node = map[id];
if (parentId === 0) {
tree.push(node);
} else {
if (!map[parentId]) {
map[parentId] = { children: [] };
}
map[parentId].children.push(node);
}
}
return tree;
}
// ==========> 函数返回示例
// [
// {
// id: 4,
// paramName: '供应链属性',
// parentId: 0,
// children: [
// {
// id: 2,
// paramName: '供应链属性',
// parentId: 4,
// },
// ],
// },
// {
// id: 5,
// paramName: '供应链属性',
// parentId: 0,
// children: [
// {
// id: 6,
// paramName: '供应链属性',
// parentId: 5,
// },
// ],
// },
// ];
不使用数组的 arr.flat() API,自己实现一个数组拍平函数,需要支持任意层级
const arr = ['hi',['hello',1],2,[3,[4,[5]]]]
function flat(list, depth=1) {
//...
}
flat(arr);
// 默认展开一层
// ["hi","hello",1,2,3,[4,[5]]]
flat(arr, 3);
// 第二个参数支持控制层级
// ['hi', 'hello', 1, 2, 3, 4, 5]
// 递归算法
function flat(list, depth = 1) {
let result = [];
for (let item of list) {
if (Array.isArray(item) && depth > 0) {
result = result.concat(flat(item, depth - 1));
} else {
result.push(item);
}
}
return result;
}
const arr = ['hi',['hello',1],2,[3,[4,[5]]]];
console.log(flat(arr)); // ["hi","hello",1,2,3,[4,[5]]]
console.log(flat(arr, 3)); // ['hi', 'hello', 1, 2, 3, 4, 5]
/**
* 说明:
* 写个转换函数,把一个JSON对象的key从下划线形式(Pascal)转换到小驼峰形式(Camel)
* 示例:
* converter({"a_bc_def": 1}); // 返回 {"aBcDef": 1}
*/
function converter(obj) {
/* 功能实现 */
}
/**
* 说明:
* 写个转换函数,把一个JSON对象的key从下划线形式(Pascal)转换到小驼峰形式(Camel)
* 示例:
* converter({"a_bc_def": 1}); // 返回 {"aBcDef": 1}
*/
function converter(obj) {
/* 功能实现 */
const result = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
const newKey = key.replace(/_([a-z])/g, function(match, p1) {
return p1.toUpperCase();
});
result[newKey] = obj[key];
}
}
return result;
}
以下是一些前端工程化、模块化和组件化开发的场景、案例和方法:
代码规范:使用 ESLint 等工具进行代码规范检查,保证代码风格的一致性。
自动化构建:使用 Webpack 等工具进行自动化构建,包括代码打包、压缩、混淆等操作,提高开发效率和代码质量。
自动化测试:使用 Jest 等工具进行自动化测试,包括单元测试、集成测试、端到端测试等,保证代码质量和功能稳定性。
持续集成:使用 Jenkins、Travis CI 等工具进行持续集成,自动化构建和测试,并及时反馈开发人员。
模块化:
使用 ES6 的 import/export 语法进行模块化开发。
使用 CommonJS 或 AMD 规范进行模块化开发。
使用 Webpack 等工具进行模块打包和管理。
组件化:
使用 React、Vue.js 等框架进行组件化开发。
使用 Web Components 进行组件化开发,实现跨框架、跨平台的组件复用。
使用 Storybook 等工具进行组件开发和文档编写。
以上仅是一些常见的场景、案例和方法,具体的实践还需要根据具体项目和团队的情况进行调整和优化。
Redux Toolkit 是一个官方推荐的 Redux 库,它提供了一些工具和约定,使 Redux 的使用更加简单和高效。使用 Redux Toolkit 可以帮助我们更快地编写 Redux 应用程序,并减少样板代码的编写量。
要使用 Redux Toolkit 进行全局状态管理,您可以按照以下步骤操作:
安装 Redux Toolkit
您可以使用 npm 或 yarn 安装 Redux Toolkit:
bashCopy
# 使用 npm 安装
npm install @reduxjs/toolkit
# 使用 yarn 安装
yarn add @reduxjs/toolkit
创建 store
在 Redux 中,store 是应用程序的单一状态树。使用 Redux Toolkit 可以通过 configureStore 函数来创建 store。例如:
javascriptCopy
import { configureStore } from '@reduxjs/toolkit';
const store = configureStore({
reducer: {
// 这里添加您的 reducer
},
});
创建 reducer
在 Redux 中,reducer 是一个纯函数,它接收当前状态和一个 action,然后返回一个新的状态。使用 Redux Toolkit 可以使用 createSlice 函数来创建 reducer。例如:
javascriptCopy
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: 0,
reducers: {
increment: (state) => state + 1,
decrement: (state) => state - 1,
},
});
export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;
在组件中使用 store
在组件中使用 Redux Toolkit 可以使用 useSelector 和 useDispatch 钩子。例如:
javascriptCopy
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './counterSlice';
function Counter() {
const count = useSelector((state) => state.counter);
const dispatch = useDispatch();
return (
{count}
);
}
以上是使用 Redux Toolkit 进行全局状态管理的基本步骤。希望这些信息能对您有所帮助!
什么是Redux Toolkit,它的主要特点是什么?(1)
Redux Toolkit是一个官方提供的用于简化Redux开发的工具集。它提供了一组工具和约定,可以帮助开发者更快速、更简单地编写Redux代码。Redux Toolkit的主要特点包括:
提供了configureStore函数,可以简化创建Redux store的过程。
引入了slices的概念,使用createSlice可以快速创建Redux reducer和action。
提供了createAsyncThunk函数,可以简化在Redux中处理异步逻辑的过程。
引入了createEntityAdapter,可以帮助开发者更方便地管理规范化的数据。
可以大大减少Redux应用程序中的样板代码
configureStore()如何简化设置Redux store的过程?
configureStore()是Redux Toolkit提供的一个函数,用于简化设置Redux store的过程。它会自动集成一些常见的Redux中间件,如redux-thunk和redux-logger,并可以通过参数进行自定义配置。使用configureStore()可以将以下代码:
javascriptCopyimport { createStore, applyMiddleware } from'redux';
import thunkMiddleware from'redux-thunk';
import loggerMiddleware from'redux-logger';
import rootReducer from'./reducers';
const store = createStore(
rootReducer,
applyMiddleware(thunkMiddleware, loggerMiddleware)
);
简化成以下代码:
javascriptCopyimport { configureStore } from'@reduxjs/toolkit';
import rootReducer from'./reducers';
const store = configureStore({
reducer: rootReducer
});
使用configureStore()可以减少Redux应用程序中的样板代码,同时还可以提高开发效率和代码可读性。
在Redux Toolkit中,什么是slice,如何使用createSlice()创建slice?
在Redux Toolkit中,slice是一个概念,用于组织Redux应用程序中的reducer和action。slice包含了一个reducer函数和一组相关的action creators,可以通过createSlice()函数来创建。createSlice()函数接受一个包含了reducer和action creator的对象作为参数,并会自动生成对应的action types和action creators。
以下是一个使用createSlice()函数创建slice的示例代码:
javascriptCopyimport { createSlice } from'@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: 0,
reducers: {
increment: state => state + 1,
decrement: state => state - 1
}
});
exportconst { increment, decrement } = counterSlice.actions;
exportdefault counterSlice.reducer;
在上面的代码中,createSlice()函数创建了一个名为counter的slice,它包含了一个名为increment和一个名为decrement的action creator。在createSlice()函数中,我们还可以指定slice的初始状态(initialState)和slice的名称(name)。
最后,需要将slice的reducer和action creator导出,以便在Redux应用程序中使用。
引用:
createAsyncThunk()如何简化在Redux中处理异步逻辑的过程?
createAsyncThunk()是Redux Toolkit提供的一个函数,用于简化在Redux中处理异步逻辑的过程。使用createAsyncThunk()可以将异步操作的逻辑和数据获取过程封装在一个action creator中,并且可以处理异步操作中的成功和失败情况。
以下是一个使用createAsyncThunk()函数创建异步action creator的示例代码:
javascriptCopyimport { createAsyncThunk } from'@reduxjs/toolkit';
import { fetchUser } from'./api';
exportconst fetchUserById = createAsyncThunk(
'users/fetchById',
async id => {
const response = awaitfetchUser(id);
return response.data;
}
);
在上面的代码中,fetchUserById是一个异步action creator,它使用createAsyncThunk()函数创建。在createAsyncThunk()函数中,第一个参数是action的type前缀,第二个参数是一个异步函数,用于执行具体的异步操作。当异步操作完成时,createAsyncThunk()函数会自动派发成功或失败的action。
使用createAsyncThunk()可以大大简化在Redux中处理异步逻辑的过程,并且可以提高代码的可读性和可维护性。
什么是规范化数据,如何使用createEntityAdapter()进行管理?
规范化数据是指将数据存储在一个单一的地方,并使用唯一的标识符来引用它们,以便在整个应用程序中共享和重用数据。在Redux中,可以使用createEntityAdapter()函数来管理规范化的数据,并自动生成常见的CRUD操作(retrieve, create, update, delete)的reducer函数。
以下是一个使用createEntityAdapter()函数创建规范化的数据管理器的示例代码:
javascriptCopyimport { createEntityAdapter } from'@reduxjs/toolkit';
const usersAdapter = createEntityAdapter({
selectId: user => user.id,
sortComparer: (a, b) => a.name.localeCompare(b.name)
});
const usersSlice = createSlice({
name: 'users',
initialState: usersAdapter.getInitialState(),
reducers: {
addUser: usersAdapter.addOne,
updateUser: usersAdapter.updateOne,
removeUser: usersAdapter.removeOne,
removeAllUsers: usersAdapter.removeAll
}
});
exportconst { addUser, updateUser, removeUser, removeAllUsers } = usersSlice.actions;
exportdefault usersSlice.reducer;
在上面的代码中,createEntityAdapter()函数创建了一个名为usersAdapter的规范化数据管理器,它包含了selectId和sortComparer等配置选项。在createSlice()函数中,我们使用usersAdapter.getInitialState()来初始化slice的初始状态(initialState),并且通过addUser、updateUser、removeUser和removeAllUsers等action creator来处理数据的CRUD操作。
使用createEntityAdapter()函数可以使我们更容易地管理规范化的数据,并且可以自动生成常见
Redux Toolkit如何帮助减少Redux应用程序中的样板代码?
Redux Toolkit通过提供一组工具函数和约定来帮助减少Redux应用程序中的样板代码。Redux Toolkit中的createSlice()函数可以自动生成常见的reducer和action creator,createAsyncThunk()函数可以简化处理异步逻辑的过程,createEntityAdapter()函数可以帮助管理规范化的数据。此外,Redux Toolkit还提供了一些其他实用工具,如configureStore()函数用于配置Redux store,以及createSelector()函数用于创建可重用的selector函数。
你能提供一个你如何在项目中使用Redux Toolkit并简化代码的例子吗?
Redux Toolkit通过提供一组工具函数和约定来帮助减少Redux应用程序中的样板代码。Redux Toolkit中的createSlice()函数可以自动生成常见的reducer和action creator,createAsyncThunk()函数可以简化处理异步逻辑的过程,createEntityAdapter()函数可以帮助管理规范化的数据。此外,Redux Toolkit还提供了一些其他实用工具,如configureStore()函数用于配置Redux store,以及createSelector()函数用于创建可重用的selector函数。这些工具函数和约定可以使Redux应用程序的开发更加高效和简洁
使用Redux Toolkit有哪些潜在的缺点或限制?
Redux Toolkit与其他状态管理库(如MobX或Recoil)相比如何?
例如,MobX可能更加灵活和易于使用,而Recoil则专注于管理React组件之间的状态。选择哪种状态管理库取决于具体的项目需求和开发人员的个人偏好。
如何处理Redux Toolkit应用程序中的调试问题?
持续更新。。。