HTML、CSS、JavaScript、Vue、React、浏览器原理、计算机网络、前端性能优化、手写代码、前端工程化、代码输出结果。
作用域
作用域表示一个区间,在这个区间内声明的所有内容(比如方法或变量)都可以被该区间内的代码访问到。
作用域链
作用域链是一个独特空间。当一个变量被调用,那么变量在 被调用 时所在的局部作用域和全局作用域之间,就形成了一个作用域链。
JS数据类型
基本数据类型:String , Number , Boolean , null , undefined , Symbol
引用数据类型:Object , Array , Function
基本数据类型的值是存放在栈内存中的,而引用数据类型的值是存放在堆内存中的,栈内存中仅仅存放着它在栈内存中的引用(即它在堆内存中的地址),就和它的名字一样,引用数据类型
内存访问机制
基本数据类型可以直接从栈内存中访问变量的值,而引用数据类型要先从栈内存中找到它对应的引用地址,再拿着这个引用地址,去堆内存中查找,才能拿到变量的值。当我们在对引用类型赋值的时候,复制的是该对象的引用地址。
浅拷贝
1、Object.assign 会拷贝所有的属性值到新的对象中,当子属性值是对象的话,拷贝的是仍然是引用。
2、扩展运算符也同样可以达到浅拷贝的作用
浅拷贝只能解决第一层的问题,当子属性是对象的时候,那么,就又回到上面的问题了,两者享有共同的引用地址。
深拷贝
1、Object.assign(target, source1, source2)
es6新增的方法,可用于对象合并,将源对象的所有可枚举属性,复制到目标对象上。
2、JSON.parse(JSON.stringify())
弊端:会忽略 undefined;会忽略 symbol;不能序列化函数;不能解决循环引用的对象;
3、普通递归函数实现深拷贝
function deepClone(source) {
if (typeof source !== 'object' || source == null) {
return source;
}
const target = Array.isArray(source) ? [] : {};
for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (typeof source[key] === 'object' && source[key] !== null) {
target[key] = deepClone(source[key]);
} else {
target[key] = source[key];
}
}
}
return target;
}
垃圾回收机制
当一个变量不被需要了以后,它就会把这个变量所占用的内存空间所释放,这个过程就叫做垃圾回收
引用计数法
变量引用的次数,你可以认为它就是对当前变量所引用次数的描述。
var obj={name:'jack'};
当我们在给 obj
赋值的同时,其实就创建了一个指向该变量的引用,引用计数为1,在引用计数法的机制下,内存中的每一个值都会对应一个引用计数。
而当我们给 obj
赋值为 null
时,这个变量就变成了一块没用的内存,那么此时, obj
的引用计数将会变成 0,所有引用计数为0的变量都将会被垃圾收集器所回收,那么, obj
所占用的内存空间将会被释放。
标记清除法
它采用的判断标准是看这个对象是否可抵达,主要分为两个阶段,标记阶段和清除阶段:
- 标记阶段
垃圾收集器会从根对象(Window对象)出发,扫描所有可以触及的对象,这就是所谓的可抵达
- 清除阶段
在扫描的同时,根对象无法触及(不可抵达)的对象,就是被认为不被需要的对象,就会被当成垃圾清除
内存泄漏
该释放的内存垃圾没有被释放,依然霸占着原有的内存不松手,造成系统内存的浪费,导致性能恶化,系统崩溃等严重后果,这就是所谓的内存泄漏
闭包
闭包是指有权访问另一个函数作用域中的变量的函数。因为作用域链的存在,所以内部函数才可以访问外部函数中定义的变量,而不能直接访问其他函数内的变量和方法。闭包的变量会常驻内存,滥用闭包容易造成内存泄漏。
柯里化
柯里化是一种函数的转换,它是指将一个接受n个参数的函数转换为n个只接受一个参数的函数的过程。 柯里化不会调用函数。它只是对函数进行转换。柯里化实际是把简答的问题复杂化了,但是复杂化的同时,我们在使用函数时拥有了更加多的自由度。
闭包应用-防抖,节流
防抖
将多次执行变为最后一次执行或立即执行,你可以理解为防止手抖。
- 搜索框搜索输入。只需用户最后一次输入完,再发送请求
- 手机号、邮箱验证输入检测
- 窗口大小Resize。只需窗口调整完成后,计算窗口大小。防止重复渲染
节流
将多次执行变为每隔一段时间执行一次
- 滚动加载,加载更多或滚到底部监听
单线程的JS
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
JS同步与异步
同步
任务从上往下按顺序执行,后一个任务必须等待前一个任务执行完
异步
前一个任务还没执行完, 也没关系,直接执行下一个任务
同步任务就是按顺序执行,从上往下。
异步任务里是从上往下,微任务比宏任务先执行。
- 值得注意的是,
Promise
是有一点特殊性的,因为Promise构造函数中函数体的代码都是立即执行的 , 而Promise.then()
和Promise.catch()
属于微任务,也就是resolve()
和reject()
console.log(1)
setTimeout(function() {
console.log(2)
})
new Promise(function (resolve) {
console.log(3)
resolve()
}).then(function () {
console.log(4)
}).then(function() {
console.log(5)
})
console.log(6)
1、3、6、4、5、2
函数
函数是可以执行的 javascript
代码块。
function fn(){
//to do something
}
方法
通过对象调用的 javascript
函数
obj = {
fn(){
//to do something
}
}
方法和函数类似,同样封装了独立的功能,但是方法是只能依靠类或者对象来调用的,表示针对性的操作。
构造函数
构造函数是一种特殊的函数,主要用来初始化对象,即为对象成员变量赋初始值,它总与 new 一起使用。我们可以把对象中一些公共的属性和方法抽取出来,然后封装到这个函数里面。
- 构造函数用于创建某一类对象,其首字母要大写
- 构造函数要和 new 一起使用才有意义
new
操作符,到底做了哪些事情
- 在内存中创建一个新的空对象。
- 让 this 指向这个新的对象。
- 执行构造函数里面的代码,给这个新对象添加属性和方法。
- 返回这个新对象(所以构造函数里面不需要 return )
实现一个 new
方法
// const xxx = _new(Person,'cooldream',24) ==> new Person('cooldream',24)
function _new(fn,...args){
// 新建一个对象 用于函数变对象
const newObj = {};
// 将空对象的原型地址 `_proto_` 指向构造函数的原型对象
newObj.__proto__ = fn.prototype;
// this 指向新对象
fn.apply(newObj, args);
// 返回这个新对象
return newObj;
}
每一次 new
操作,都会开辟新的内存,所以每一个实例对象,都有自己的属性和方法的副本。这不仅无法做到数据共享,也是极大的资源浪费。
原型
function Person(){
}
let person = new Person();
构造函数 Person
和 实例对象 person
。
- 每一个构造函数都有一个prototype属性(
Person.prototype
),指向实例原型; - 每一个实例对象都有一个__proto__属性(
person.__proto__
),这个属性会指向该对象的原型;
Person.prototype === person.__proto__
- 每个原型都有一个
constructor
属性指向关联的构造函数 原型指向构造函数;
Person.prototype.constructor === Person
function Person() {
}
Person.prototype.name = 'Kevin';
var person = new Person();
person.name = 'Daisy';
console.log(person.name) // Daisy
delete person.name;
console.log(person.name) // Kevin
- 创建构造函数 Person
- 在原型
Person.prototype
上新增name
属性,赋值为Kevin
- 通过
new
操作符新增一个继承自构造函数Person
的实例对象person
- 在实例对象
person
新增一个name
属性,赋值为Daisy
,这一步我们称之为自定义属性 - 输出实例对象
person
上的name
属性,会查找实例对象本身,优先找到自定义属性,所以值为Daisy
(所以自定义属性优先级高于原型上的自有属性,这也是为什么有了属性和方法的重写的概念) - 将实例对象
person
上的自定义属性name
删除 - 输出
person.name
,还是先查找实例对象本身,因为自定义属性被删除了,那么就去原型上面找,找到了之前定义在原型上的值,所以,输出Kevin
原型链
原型链的概念呢,其实有点类似于作用域链,就是由于一层一层的嵌套,和链条一样,一节接一节,所以,原型产生的链条,称为原型链。
我们都知道对象的 __proto__
就是所谓的原型,而原型又是一个对象,它又有自己的 __proto__
,原型的 __proto__
又是原型的原型,就这样可以一直通过 __proto__
向上找,这就是原型链,当向上找找到 Object
的原型的时候,这条原型链就算到头了。
打印 person.__proto__.__proto__
,原型链查找就算到头了,也就是再无 __proto__
,一个简单的 person
实例对象,也有两层原型。
类
Promise
Promise
本质上是一个构造函数,因为使用 new
关键字创建,它是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大,属于 ES6
中的一个概念。
封装Http请求:
// 向后台发送数据
export const postData= (params) => {
return new Promise((resolve, reject) => {
axios.post('/xxxUrl', params).then(res => {
resolve(res)
})
.catch(error => {
reject(error)
});
})
};
-
Promise.prototype.then()
then 方法简单理解就是 resolve() 回调之后的产物,即 已成功 状态下的回调函数。
-
Promise.prototype.catch()
catch 方法简单理解就是 reject() 回调之后的产物,即 已失败 状态下的回调函数。
-
Promise.prototype.finally()
finally 方法简单理解就是 无论成功或失败 ,都会执行的回调函数。
链式调用
Promise
本来就是用来解决回调地狱所带来的问题的,所以,其实可以用链式调用的方式,来解决层层嵌套所带来的问题。
它的原理主要是 .then()
回调返回的也是一个 Promise
,并且是一个全新的 Promise
,如果你在 then
中 使用了 return
,那么 return
的值会被 Promise.resolve()
包装。
getData1().then(res=>{
console.log(res) // data1
return getData2();
}).then(res=>{
console.log(res) // data2
return getData3();
}).then(res=>{
console.log(res) // data3
})
异步解决方案
Generator
Generator
函数是 ES6 提供的一种异步编程解决方案,主要原因是它可以在执行中被中断、然后等待一段时间再被我们唤醒。
它内部有一个 yield
关键字,而它的主要作用,是将函数进行分割,你可以理解为将函数分为多个步骤,而每次函数的调用只执行一个步骤。这也就是上面所说的被中断,等下次继续唤醒。
function* helloWorldGenerator() {
yield 'result1';
yield 'result2';
return 'result3';
}
Generator
函数的声明和普通函数还是存在区别的,不过不仅如此,在函数的调用上也会有所不一样。在我们调用 Generator
函数之后,函数并不会执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,通过调用这个指针对象的 next()
方法就会从头开始执行,直到遇到第一个 yield
才会停止。等到下一次 next()
方法,又会从上一次停止的地方继续执行,再遇到下一个 yield
停止。
function getResult1(){
return getData1().then(res=>{
console.log('step1');
// ....
})
}
function getResult2(){
return getData1().then(res=>{
console.log('step2');
// ....
})
}
function getResult3(){
return getData1().then(res=>{
console.log('step3');
// ....
})
}
function* getGenerator(){
console.log('start');
yield getResult1();
yield getResult2();
yield getResult3();
}
let hw = getGenerator(); // 仅生成指针对象,并不执行
hw.next(); // 先输出 start ,然后输出 step1
hw.next(); // 输出 step2
hw.next(); // 输出 step3
完成整个 Generator
函数的执行。
async / await
async
/await
是ES7
提出的一种异步解决方案,它相当于Generator
+ 执行器的语法糖,就目前来说,是最佳的异步解决方案,真正实现了异步代码,同步表示。
async function hw(){
console.log('start');
await getResult1();
await getResult2();
await getResult3();
}
回调函数
回调函数是JS异步编程的最基本、最原始的方式,例如事件回调、setTimeout/setInterval、ajax等
函数内部this的指向
HTTP请求原理
- 解析URL: 客户端解析所需访问的URL,通过DNS解析找到服务器的IP地址
- 建立TCP连接: 客户端使用解析得到的服务器地址建立一个TCP连接,完成三次握手后,连接通道建立。若使用HTTPS协议,则还会进行SSL握手,建立加密通道。使用SSL握手时,会确定是否使用HTTP2。
- 发送请求报文: 客户端构建HTTP请求报文,其中包括请求行、请求头和请求体。请求行包含请求方法(如GET、POST、PUT、DELETE等)、URL和HTTP版本。请求头包含与请求相关的附加信息,如用户代理(User-Agent)、Accept、Cookie等。请求体是可选的,通常用于POST请求发送表单数据或JSON数据。
- 服务器响应: 服务器接收到请求后,会解析请求报文并进入后端流程。服务器执行相应的操作,并构建一个HTTP响应报文作为响应。
- 接收响应报文: 服务器将构建好的HTTP响应报文发送回客户端。客户端接收到响应报文后,通过TCP连接接收数据。
- 解析响应报文: 客户端解析响应报文,提取响应行、响应头和响应体。响应行包含响应状态码(如200表示成功、404表示未找到等)和HTTP版本。响应头包含与响应相关的附加信息,如响应内容的类型、响应长度等。响应体包含实际的响应数据,如HTML页面、JSON数据等。
- 渲染页面或执行操作: 客户端根据响应的内容进行处理操作。对于HTML页面,客户端会解析并渲染页面,展示给用户。对于其他资源,如图片、CSS文件、JavaScript文件等,客户端可能会进一步发起请求以获取这些资源,并在页面中进行加载和使用。
- 关闭TCP连接: 浏览器根据使用协议的版本,以及Connectionn字段的约定,决定是否要保留TCP连接。不保留,一旦HTTP请求和响应过程完成,客户端和服务器之间的TCP连接将会关闭。
浏览器渲染原理
- 解析 HTML,构建DOM树:
- 样式计算:
- 布局:
- 分层:
- 绘制:
- 分块:
- 格栅化:
- 回流、重绘、合成: