目录
一. 手写原理实现类
1. 防抖和节流
控制一些持续触发的事件执行的时机的一种方法。 比如: resize, scroll, mousemove等。
防抖(debounce)
防抖: 触发事件后指定时间内其函数只执行一次, 若在指定的时间内又触发了事件,则会重新计算函数的执行时间。(防抖函数也可分为:立即执行和非立即执行两种类型)
- 非立即执行:
function debounce(fun, wait) {
let timeout
return function() {
let context = this;
let args = arguments;
!!timeout && clearTimemout(time)
timeout = setTime(()=>{
fun.apply(context, args)
}, wait)
}
}
非立即执行: 在事件触发后等待自定义时间后才执行, 在等待时间内如果又触发了事件,则清空之前的计时器并重新计算函数执行时间。
- 立即执行:
function debounce(fun, wait) {
let timeout;
return function() {
let context = this;
let args = arguments;
!!timeout && clearTimeout(timeout);
let callNow = !timeout;
timeout = setTimeout(()=>{
timeout = null;
}, wait)
!!callNow && fun.apply(context, args)
}
}
立即执行: 触发函数后立即执行, 然后在设定的时间内不会触发事件。当超过设定时间时计时器将会重置计时器,允许后续第一个调用函数的事件执行。
节流(throttle)
节流: 连续触发事件但在指定时间内,只执行一次函数。 节流可以稀释函数的执行频率。(节流包含两种实现:时间戳和定时器)
- 事件戳版
function throttle (func, wait){
var previous = 0;
return function(){
let now = Date.now();
let context = this;
let args=arguments;
if(now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}
在持续触发事件的过程中,函数会立即执行且每个自定义时间执行一次。
- 定时器版
function throttle(func, wait) {
let timeout;
return function() {
let context = this;
let args = arguments;
if (!timeout) {
timeout = setTimeout(() => {
timeout = null;
func.apply(context, args)
}, wait)
}
}
}
该方式在持续触发事件的过程中, 函数不会立即执行,并且每个指定时间执行一次,在停止触发事件后,函数还会再执行一次
2. 深拷贝
堆和栈的区别
其实深拷贝和浅拷贝的主要区别就是其在内存中的存储类型不同。堆和栈都是内存中划分出来用来存储的区域
栈(stack) 为自动分配的内存空间, 它由系统自动释放; 而堆(heap)则是动态分配的内存, 大小不定也不会自动释放。
- 引用类型存放在堆中
引用类型变量实际上是一个存放在栈内存的指针, 这个指针指向堆内存中的地址。每个空间大小不一样,要根据情况进行特定的分配。 - 基本类型存放在栈中
基本类型的两个变量是两个独立互不影响的变量。
深拷贝简单实现
- 浅拷贝实现
function cloneShallow(source) {
var target = {}
for(var key in source) {
if(Object.prototype.hasOwnProperty.call(source. key)) {
target[key] = source[key];
}
}
return target
}
- 简单深拷贝(在浅拷贝基础上加递归)
function cloneDeep(source) {
var target = {}
for(let key in source) {
if(Object.prototype.hasOwnProperty.call(source, key)) {
// typeof null === 'object'
if(!!source[key] && typeof source[key] === 'object) {
target[key] = cloneDeep(source[key])
} else {
target[key] = source[key]
}
}
}
return target
}
hasOwnProperty: 所有继承了Object 的对象都会继承到hasOwnProperty 方法。 该方法判断对象自身是否存在指定属性, 且该方法会忽略掉那些从原型上继承来的属性
3. 手写call, apply, bind 的实现
- call(context, param1, param2,…)
// 一个用于生成唯一值的Symbol 方法
function mySymbol(obj) {
let unique = (Math.random() + new Date().getTime()).toString(32).slice(0, 8)
if(obj.hasOwnProperty(unique)) {
return mySymbol(obj)
} else {
return unique
}
}
Function.prototype.myCall = function(context) {
context = context || window
let fnName = mySymbol(context)
context[fnName] = this // 给context 添加一个方法,指向this (this就是 “ obj.fnName.myCall() ” 中的fnName方法)
// 处理参数, 去掉第一个参数this, 其它传入fn函数
let args = [...arguments].slice(1) // 把[...arguments] 转成一个数组, slice 返回一个新数组
let result = context[fnName](...args) // 执行fn
delete context[fnName] // //删除挂载在借用对象上的fn属性
return result
}
- apply(context, [.,.,…])
// 一个用于生成唯一值的Symbol 方法
function mySymbol(obj) {
let unique = (Math.random() + new Date().getTime()).toString(32).slice(0, 8)
if(obj.hasOwnProperty(unique)) {
return mySymbol(obj)
} else {
return unique
}
}
Function.prototype.myApply = function(context) {
context = context || window
let fnName = mySymbol(context)
context[fnName] = this // 给context 添加一个方法,指向this (this就是 “ obj.fnName.myCall() ” 中的fnName方法)
// 处理参数, 去掉第一个参数this, 其它传入fn函数
let args = [...arguments].slice(1) // 把[...arguments] 转成一个数组, slice 返回一个新数组
let result = context[fnName](...args) // 执行fn
delete context[fnName] // //删除挂载在借用对象上的fn属性
return result
}
- bind(context, ,)
特点: 函数调用改变this; 返回一个绑定this 的函数; 接收多个参数; 支持柯里化形式参数 fn(1)(2)
Function.prototype.bind = function(context) {
// 返回一个绑定this的函数, 我们需要在此保存this
let fnName= this; // 这里this 就是需要bind的函数
// 可以支持柯里化参数, 保存参数
let arg = [...arguments].slice(1)
// 返回一个函数
return function(){
let newArgs = [...arguments]
// 返回函数绑定this, 传入两次保存的参数
// 考虑返回函数有返回值做了return
return fnName.apply(context, arg.concat(newArgs))
}
}
// 使用:
let fn = Person.say.bind(Person1) // 返回一个绑定多个作用域的函数
fn()
fn(name, age, xxxx, .....)
4. jsonp 的实现
JSONP(JSON with Padding)它是一个非官方的协议。其跨域利用script的src属性, 这个属性不受同源策略影响,可以访问不同服务下的资源。
- 先来看一个jsons 的client to server 端的简单实现:
// 客户端
<scritp>
function callback(data){
console.log(data);
}
var scriptDom = document.createElement('script');
scriptDom.src = "http://localhost:8082/getdata?cb=callback";
document.body.appendChild(scriptDom);
</script>
// server 端(基于express):
app.get('/getdata',function(req,res){
//同步读取json文件
var data = fs.readFileSync('server2/data.json').toString();
var qs = url.parse(req.url).query;
var cb = querystring.parse(qs).cb;
var jsonp = cb+"("+data+")";
res.send(jsonp);
}
如上例子允许后会在浏览器控制台输出: 服务端返回的 Object:{} 数据对象
- JSONP 实现
// JSONP 实现
function JSONP({
url,
params,
callbackKey, // cb参数名
callback // 回调函数
}){
// 唯一id, 不存在则初始化
JSONP.callbackId = JSONP.callbackId || 1
params = params || {}
// 传递的callback 名,和厦门预留的一致
params[callbackKey] = `JSONP.callbacks[${JSONP.callbackId}]`
// 避免全局污染
JSONP.callbacks = JSONP.callbacks || []
// 按照id放置 callback
JSONP.callbacks[JSONP.callbackId] = callback
const paramsKeys = Object.keys(params)
const paramString = paramKeys.map(key => `${key}=${params[key]}`).json('&')
const script = document.createElement('script')
script.setAttribute('src', `${url}?${paramString}`)
document.body.appendChild(script)
// id 占用,自增
JSONP.callbackId ++
}
// 使用JSONP
JSONP({
url: 'http://xxxxx/ajax/jsonp/xxxxxx',
params: {
key: 'test1',
},
callbackkey: '_cb',
callback(result) {
console.log(result.data)
}
})
JSONP({
url: 'http://xxxxx/ajax/jsonp/xxxxxx',
params: {
key: 'test2',
},
callbackkey: '_cb',
callback(result) {
console.log(result.data)
}
})
JSONP 调用后通过控制台可以卡到请求都是:
http://xxxxxxx/ajax/jsonp/xxxxx?key=test1&_cb=JSON.callbacks[1]这样的,得到的 js 也是 JSON.callbacks1, 这样就避免回调命名冲突问题,也避免了全局域的污染
注: == 上面代码存在一个问题那就是参数部分如下情况:
params: {
a: ‘545&b=3’
b: ‘5’,
},
参数 a 中包含了b的值,其实用户这是希望a的值为: 545&b=3。 解决的办法,进行URI编码==, encodeURIComponent(‘trdgd&b=2’) 的结果为: trdgd%26b%3D2。
JSONP部分实现可改为:
const paramString = paramKeys
.map(key => `${key}=${encodeURIComponent(params[key])}`)
.join('&')
这里值得一提的是,由于最终的 URL 不能包含 ASCII 码以外的字符,所以其实当使用中文或者特殊字符时其实会被自动编码。而 +,空格,/,?,%,#,&,= 等字符在 URL 中则会出现歧义,只有手动编码后才能让服务器端正确解析
5. 实现一个New
new 操作符都做了些什么? 可以用四步来总结:
- 创建一个空对象;
- 链接到原型;
- 绑定this值;
- 返回新的对象
模拟一个new方法的实现:
// 一个new 的实现
function myNew(){
// 创建一个空对象
let obj = new Object()
// 获取构造函数
let Constructor = [].shift.call(arguments);
// 链接到原型
obj.__proto__ = Constructor.prototype;
// 绑定this值
let result = Constructor.apply(obj.arguments); // 使用apply, 将构造函数中的this指向新对象, 这样新对象就可以访问构造函数中的属性和方法。
// 返回新对象
return typeof result === 'object' ? result : obj // 如果返回值是一个对象就返回该对象,否则返回构造函数的一个实例对象
}
// 使用测试
function Product(name, type) {
this.name = name;
this.type = type;
}
// 通过new 创建构造实例
let pro1 = new Product('礼盒', '水果');
console.log(pro1.name, pro1.type) // 礼盒, 水果
// 通过myNew方法创建实例
let pro2 = myNew(People, '礼盒', '水果');
console.log(pro2.name, pro2.type); // 礼盒, 水果
二. 基础原理概念类
JS原型与原型链
JavaScript 原型
ES2019规范是当前最新的语言规范,可以作为本主题的权威素材来源。
- prototype 的定义
1. 定义描述
在规范里, prototype被定义为:给其它对象提供共享属性的对象(prototype自己也是对象,只是被用以承担某个职能)。
这里有个概念:prototype对象这种说法是一个简略的描述, 真实的描述是:“xxxx对象的prototype对象”(如果不跟其它对象产生关联,
就构不成prototype这个称谓)。
2. 所有object对象都有一个隐式引用
规范中明确描述了所有对象,都有一个隐式引用,它被称之为这个对象的prototype原型。
什么叫隐式引用:
如上图所示,我们控制台输出了一个空对象,但可以发现有 proto 属性, 这意味这这个空对象被隐式挂载了另一个对象的引用,
置于 proto 属性中。也就是说所谓隐式:是指不是由开发者亲自创建/操作。
prototype chain 原型链
在ECMAScript2019规范里,只通过短短的一句话,就介绍完了prototype chain。
原型链概念只是在原型这个概念基础上所作的直接推论。
也就是说,prototype 对象也有自己的隐式引用,有自己的prototype对象。
如此, 构成了对象的原型的原型的原型的…链条, 直到某个对象的隐式引用为null, 整个链条终止
三. 运转机制类
Html 请求到渲染展示运转流程
上图运转流程简述:
- 浏览器中输入rul ,回车(浏览器会开启一个线程来处理这个请求)
- DNS(可以理解为:域名与ip对应的库)解析。
解析优先顺序:
- 查找浏览器自身的DNS
- 查找本地Host文件
- 查找无线路由器
- 发起DNS的系统调用,让快带运营商帮忙找
只要上述4个步骤中有一个能通过域名解析到IP地址, 就会跳出DNS解析这步
- 通过IP地址定位到服务器并发起TCP连接,进行“三次握手”、
- 建立完成TCP/IP连接, 浏览器发送HTTP请求
- 服务器处理请求,并返回结果(HTML页面)
- 浏览器下载HTML文件, 设置缓存,关闭TCP连接。
此过程中可定会有css, js, 图片等静态资源也会经过上述步骤 - 最终根据HTML 生成界面。
TCP连接与关闭原则流程
-
TCP三次握手
-
TCP四次挥手
HTML渲染流程运转机制
- HTML渲染流程传送门
- 一些页面元素渲染优先级
- 遇到外联JS加载,渲染会停止并处于阻塞状态(等js 加载完成后继续后续加载及渲染)。
- CSS 文件下载过程中, 可以通过已加载的js 打印出标签,所以CSS文件的加载阻塞了DOM渲染,但没有阻塞DOM加载。
- 如果为外部Javascript添加defer或async属性,其下载就不会阻塞DOM其它内容的加载。
defer(延迟脚本): 被标注defer属性的脚本需要立即下载,但要等到整个页面解析完毕后在按先后顺序执行。
async(异步脚本): 和 defer 类似, 只是不保证脚本按顺序执行。
四. 常用算法
快速排序
function quickSort(arr) {
if(arr.length<1) return arr;
var poivtIndex = Math.floor(arr.length/2)
var poivt = arr.splice(poivtIndex, 1)[0]
var left = []
var right = []
for(var i=0; i<arr.length; i++) {
if(arr[i]<poivt) {
left.push(arr[i])
} else {
right.push(arr[i])
}
}
return quickSort(left).concat([poivt], quickSort(right))
}
var arr = [2,3,4,6,1,5]
console.log(quickSort(arr)) // [1,2,3,4,5,6]
未完待续…