JavaScript高级

1 数据类型判断

  • 基本数据类型(5种)
    String
    Number
    Boolean
    null(常用于引用变量赋初值和垃圾回收)
    undefined
  • 对象(引用)类型(2种)
    Object(普通对象{},数组对象[],正则对象/d/,Math,Date都是属于对象的)
    Function函数
    ES6新增:Symbol、Map

判断数据类型方法:

  1. typeof:返回数据类型的字符串表达,原理是判定内存中数据类型二进制,用于除null外的基本数据类型判断(null的二进制为000,而typeof判断以000开头的类型为对象类型,所以null这种类型被判定为对象),对于引用数据类型:除函数返回"function",其余都返回"object"(类型是小写)
// typeof检测情况:
// 基本数据类型:string boolean number undefined 正确检测
// null:object
// 函数:function
  1. instanceof:只能对象进行比较,判断的是当前对象是否是其子类实例或本身创建出来的对象,原理是:判断当前对象的原型链上是否包含该类型,包含则判定为true,否则为false。 只能比较引用对象(Function和Array的实例都是Object的子对象)
// instanceof检测情况:
// 引用类型可以正确检测
// 对于被手动修改了原型的的对象来说,就不一定准确了,如:函数A.prototype=Object.create(Array.prototype);A instanceof Array 为true

手动实现instanceof方法:

function instance_of(example,clazz){
	let proto = Object.getPrototypeOf(example)
	let cProto = clazz.prototype
	while(true){
		if(proto === null){
			return false
		}
		if(proto===cProto){
			return true
		}
		proto = Object.getPrototypeOf(proto)
	}
}
// 注意:考虑ie兼容性问题,使用Object.getPrototypeOf方法而不是对象.__proto__去获取实例对象的原型
  1. constructor
    可以用来判定基本数据类型,也可以判定是否是对象的细分类型,但由于该属性也可以随便改,所以也不算准确
var a = []
var b = 1
console.log(a.constructor === Array) // true
console.log(b.constructor === Number) // true

Number.prototype.constructor = "AA" // 内置类的原型都随便改了,所以也不是万能的
  1. Object.prototype.toString.call
    标准的数据类型检测方法!由于Object的toString返回的是当前对象所属类型,所以可以调用Object.prototype.toString.call去改变this所属,以检测对象类型。
    在这里插入图片描述
    但是,该方法比较麻烦,还需要对返回的结果进行处理才能拿到类型,所以一般而言,结合typeof检测基本数据类型+toString检测类数据类型比较好(instanceof和constructor可以被改变,所以不适用)
// 思路:使用typeof检测基本数据类型,除null外,使用Object.prototype.toString检测引用数据类型,将返回的奇怪格式结果封装成映射表
// JQuery源码里toType全能方法

const class2Type = {};
const toString = class2Type.toString;
[
  "String",
  "Number",
  "Boolean",
  "Object",
  "Array",
  "Function",
  "Symbol",
  "Date",
  "RegExp",
  "Error"
].forEach((name) => (class2Type[`[object ${name}]`] = name.toLowerCase()));

function toType(obj) {
  // null和undefined对于==来说是相等的
  if (obj == null) {
    return obj + "";
  }
  const t = typeof obj;
  if (t === "object") return class2Type[toString.call(obj)];
  return t;
}

console.log(toType(1));
console.log(toType("1"));
console.log(toType(true));
console.log(toType([]));
console.log(toType({}));
console.log(toType(null));
console.log(toType(undefined));
console.log(toType(new Date()));
console.log(toType(new RegExp()));
console.log(toType(() => {}));

// 结果如下
number 
string 
boolean 
array 
object 
null 
undefined 
date 
regexp 
function 

===:对于值判定是否是相等值,对于对象判断引用地址是否相同,特殊的:null===null true undefined===undefined true null==undefined true

2 三类循环性能分析

前提知识点:

  1. console.timeconsole.timeEnd,用于启动计时器和终止计时器,计算程序运行时间,传入的值来确定是哪个计时器
console.time("abc");
...
console.timeEnd("abc");

for循环 vs while循环

let arr = new Array(9999999).fill(0);
console.time("abc");
for(let i=0;i<arr.lenght;i++){}
console.timeEnd("abc");

console.time("aaa");
let i=0;
while(i<arr.length){
	i++;
}
console.timeEnd("aaa");

结论:for循环速度快一倍。但是当使用var声明i变量时,两者性能相同。结合for适用于确定次数,而while适用于不定次数,使用时需要灵活选择。

测试环境在chrome浏览器中,非node环境,js循环在浏览器中调用的是浏览器的C++库
可以这么理解,while中的i声明在外部,为全局变量占用空间,所以操作时比较费时间,而for是块中,当使用var时,两者都是全局所以差不多时间消耗

数组中的循环

Array.prototype.forEach/map/reduce/filter/find/some/every
包含多种方法

forEach(回调函数,this指向):第二个参数为this的指向,用于调用回调函数,如果不传则默认为window

forEach:性能比for和while都差,但是优点在于函数式编程方法

函数式:what:传入需要处理什么,但只关注结果,无法控制过程,不能中途打断退出,由于会做一些和循环无关的操作,性能也会消耗
命令式:how:对过程控制精细,无关代码少,性能比较高

for in

for in:性能极差,速度比forEach慢很多,功能:迭代当前对象中所有可枚举的属性(私有属性大部分可枚举,公有属性部分可枚举(原型链))

for(let key in arr){}

特点:

  • 遍历顺序以数字key优先
  • 无法遍历Symbol属性key(解决办法:if(typeof Symbol !== "undefined") Object.keys(obj).concat(Object.getOwnPropertySymbols(obj))获取到obj中所有key)
  • 可以遍历到共有可枚举(解决办法:if(!obj.hasOwnProperty(key)) break;)

for of

迭代器:代表一种规范,实现了该规范就会有属性:Symbol.iterator
for of原理是按照迭代器规范来遍历的

数组、Set、Map实现了,Object没有实现不可被迭代

原理:在需要迭代的对象上获取Symbol.iterator属性,该属性对应一个函数,调用该函数返回一个对象,该对象包含next属性,是一个函数,每次迭代,调用next属性,返回一个对象:{done:false,value:xxx}如果迭代完毕返回{done:true,value:undefined}即可

// 迭代器规范
某个对象.[Symbol.iterator] = function(){
	return {
		next(){
			if(终止条件){
				return {done:true,value:undefined}
			}
			// ...
			return {done:false,value:xxx}
		}
	}
}

类数组对象:{0:‘aa’,1:‘bb’,2:‘cc’,length:3}

如果想给 类数组 对象添加迭代器,可以不用自己写,直接使用Array的迭代器:

var a={0:'aa',1:'bb',2:'cc',length:3}
a[Symbol.iterator]=Array.prototype[Symbol.iterator]
for(let v of a){
	console.log(v);
}

3 this分析

this代表执行主体,该指向和在哪创建或执行无关,也不是执行上下文

情况分五种:

  1. 函数前面有点,this就是前面的对象;函数前无点,this就是window(严格模式下是undefined)
  2. 事件绑定时,回调函数中的this是,被绑定元素本身
  3. 构造函数中的this指new出来的实例
  4. 箭头函数中的this绑定的是上下文中的this
  5. 可以基于Function.prototype.call/apply/bind去改变this指向
// 自己动手写一个call方法
Function.prototype.call = function (obj, ...params) {
  let self = this,
    key = Symbol("KEY"),
    result;
  obj == null ? (self = window) : null;
  /^(object|function)$/.test(typeof obj) ? null : (obj = Object(obj));
  obj[key] = self;
  result = obj[key](...params);
  delete obj[key];
  return result;
};

function abc(p) {
  console.log(this, p);
}
let obj = { name: "pp", age: 11 };
abc.call(obj, 1);


// 自己动手写bind方法
Function.prototype.bind = function (obj, ...args) {
  let self = this;
  return function (...args2) {
    return self.apply(obj, args2.concat(args));
  };
};

function a(...p) {
  console.log(this, p);
}

let obj = { name: "pp", age: 11 };

a.bind(obj, 9)("abc");

鸭子类型(狸猫换太子)

Array.from() 可以通过以下方式来创建数组对象:
伪数组对象(拥有一个 length 属性和若干索引属性的任意对象)
可迭代对象(可以获取对象中的元素,如 Map和 Set 等)

鸭子类型指的是:该对象大部分结构与另一个类型相同,则可以借用另一个类型已有的方法执行,如arguments与数组类型很相似,所以可以借用数组的很多方法,如:

function func1(){
	// use Array.prototype.slice
	let args = Array.prototype.slice.call(arguments)
	// use Array.prototype.forEach
	[].forEach.call(arguments,x=>console.log(x))
}

func1(10,20,30)

4 HTTP网络层的前端性能优化

前端优化点:

  1. HTTP网络层优化
  2. 代码编译层优化(webpack)
  3. 代码运行层优化(html css js vue react)
  4. 安全优化(xss csrf)
  5. 数据埋点及性能监控

CRP:关键路径渲染

七层网络模型:

  1. 物理层:比特、电气特性
  2. 数据链路层:帧、MAC地址
  3. 网络层:报文
  4. 传输层:流量控制、错误检测
  5. 会话层
  6. 表示层
  7. 应用层

TCP/IP四层网络模型:
a. 网络接口层:ARP/RARP
b. 网络层:ICMP/IP
c. 传输层:TCP/UDP
d. 应用层:HTTP/DNS/FTP/SMTP

url从输入到展示结果经历

1. url解析

在这里插入图片描述
在前后端请求时需要对一些特殊字符进行编码,如: %等,前端有两种编码方式:
对整个url编码:encodeURI/decodeURI:这种方式只对中文空格进行编码
对参数编码:encodeURIComponent/decodeURIComponent:对中文空格%:/ 都会编码

URI统一资源标识符
URL统一资源定位符
URN统一资源名称
这是一个URL(绝对地址):https://blog.csdn.net/qq_40321119/article/details/102856789
这是一个URN(相对地址):qq_40321119/article/details/102856789
这是一个URI:可以是URL,也可以是URN

2. 缓存

属于产品优化的重点,服务端返回缓存头字段相关配置,浏览器自动实现缓存机制

缓存分为:强缓存/协商缓存。
缓存位置:内存缓存、硬盘缓存。
检测步骤:先检测是否有强缓存,没有或失效就检测是否有协商缓存,没有就去获取最新数据
html页面一般不做缓存,js、css及其他资源会

  • 强缓存:Cache-Control:max-age=表示多长时间后再次请求新数据HTTP1.1,Expires指定过期时间HTTP1.0,优先级低于前者

打开新网页:(此时直接检查硬盘缓存,没有则发送请求新数据)
F5刷新:先查内存再查硬盘,都没有就请求数据
Ctr+F5:不使用缓存,请求头会加字段:Cache-control:no-cache,直接请求最新数据

问题:如果页面更新,之前请求设置了强缓存还能获取最新数据吗?
答案:可以,页面更新后webpack打包的资源名称hash值被改变,浏览器发现新资源未做缓存,所以会重新请求。还有一种方法,如果不使用webpack,则将引入的资源加后缀也可以,后缀名不同也不走缓存。<script src='xxx.js?12345'></script>

  • 协商缓存
    Last-Modified (HTTP1.0) / ETag (HTTP1.1)
    协商缓存就是强缓存失效后,浏览器携带缓存标志向服务器请求,服务器根据缓存标志决定是否使用缓存的过程

处理过程:
携带缓存标志:If-Modified-Since=Last-Modified 值 If-None-Match=ETag 值
服务器没更新:304状态,浏览器直接读缓存
服务器更新了:200状态及最新资源以及Last-Modified / ETag,将结果和缓存标志写入本地

Last-Modified 资源文件最后更新时间,秒
ETag 记录的是一个标识,更新资源文件会生成新的ETag

  • LocalStorage数据缓存
    一般使用该对象存储数据缓存,缓存格式建议:time:xxx, data:[]
    请求时先检测本地缓存对象中是否存在该数据且未过期,否则就直接请求新的

3. DNS解析

域名解析,也是带缓存的
先是走本地hosts文件
没有则走本地DNS解析缓存
没有则走本地DNS服务
没有则走远程域名解析服务器

递归查询、迭代查询

递归
在这里插入图片描述
DNS优化:1 减少DNS请求次数(一个页面资源都放到相同的服务器上) 2 预获取prefech
第一种不推荐:实际往往会将不同资源放到不同服务器上,有利于提高资源服务器的性能利用,同时HTTP同源一次大概4-7个请求,资源分布后有利于实现高并发
注意:必须要使用Link标签!
在这里插入图片描述

4. 建立连接通道TCP三次握手

三次握手:
client:SYN=1 seq=x
server:SYN=1 ACK=1 seq=y ack=x+1
client:ACK=1 seq=x+1 ack=y+1
在这里插入图片描述

四次挥手:
client:FIN=1 seq=u
server:ACK=1 seq=v ack=u+1
服务器继续与客户端传递数据
server:FIN=1 ACK=1 seq=w ack=u+1
client:ACK=1 seq=u+1 ack=w+1

HTTP1.0、1.1、2.0区别

HTTP/1.0 每次请求建立一个TCP连接,用完即关闭,长连接默认关闭
HTTP/1.1 默认开启【长连接keep-alive】:若干请求会串行化处理,前一个请求超时会导致后面所有请求阻塞,新增断点续传(返回码206),新增支持host头域,如果没有会报400(通过host处理一台服务器上有多个共享IP地址的虚拟服务器,1.0默认一个IP对应一个服务器)
HTTP/2.0 新增【多路复用】:多个请求在同一个连接上并行执行,某个请求耗时严重不会影响其他请求

HTTP2.0相比于1.x新增特性
新的二进制格式binary format:1.x基于文本解析,需要考虑的场景很多,二进制不同,健壮性高
header压缩:1.x的header带有大量信息,每次都重复发送,2.0中使用encoder减少header大小,通讯双方各自缓存一份header fields表,避免重复传输,减少大小
服务端推送:可以在HTTP响应头中设置Link命令来让浏览器接收服务器的推送,无需多次请求(页面有个css请求,客户端收到css时,服务端会将js等文件也推送过来,客户端请求时从缓存中读取)
Link: </styless.css>; rel=preload; as=style, <example.png>; rel=preload; as=image

5 对象深拷贝

浅拷贝:拷贝后生成的对象不同,对象中的值指向原来的数据(只拷贝了一级)
深拷贝:拷贝后生成的对象和对象中的值都是新的

浅拷贝

数组浅拷贝方法:

  • ...展开运算符:let a = [...b]
  • concat运算:let a = b.concat([])
  • slice运算:let a = b.slice()

对象浅拷贝:

  • ...展开运算符:let a ={...b}
  • Object.assign运算:let a = Object.assign({},b)
  • 写循环赋值,Object.keys(obj)无法获取Symbol类型的属性, 因为该属性不可枚举,所以需要再加上Object.getOwnPropertySymbols(obj)获取所有obj上的属性

获取对象的Symbol属性名称:Object.getOwnPropertySymbols(obj)

深拷贝

实现原理:每次浅克隆一级,如果有下一级继续浅克隆下一级,实现所有深克隆
但是深拷贝中有个问题:循环引用:let a={};a.a=a,此时循环拷贝会出现栈溢出问题,解决方法是使用缓存,即判断当前需要拷贝的对象是否之前拷贝过,如果拷贝过则直接从缓存中取,否则就创建一个新的


/**
 * 深克隆(深拷贝)+ 解决深拷贝函数中循环引用时导致的栈溢出的问题
 * @param {object} origin 
 * @param {*} hashMap WeakMap数据,用于缓存克隆过的对象
 * @returns origin / 克隆的origin
 */
function deepCloneCycle(origin, hashMap = new WeakMap()) {
  let result = null;
  if (hashMap.has(origin)) return hashMap.get(origin); // 查缓存字典中是否已有需要克隆的对象,有的话直接返回同一个对象(同一个引用,不用递归无限创建进而导致栈溢出了)
  if (typeof origin === 'object' && origin !== null) { // 【类型判断】引用类型,进行递归拷贝(用typeof判断类型要剔除null的情况)
    if (Object.prototype.toString.call(origin) === '[object Array]') {
      // 【类型判断】数组类型,创建一个新数组
      result = [];
      hashMap.set(origin, result); // 哈希表缓存新值
      // 【遍历赋值】
      origin.forEach(el => {
        result.push(deepCloneCycle(el, hashMap)); // 【递归】
      });
    } else {
      // 【类型判断】对象类型,创建一个新对象
      result = {};
      hashMap.set(origin, result); // 哈希表缓存新值
      for (const key in origin) {
        // 【遍历赋值】对象这里特殊处理了,不遍历拷贝原型链上的属性
        if (origin.hasOwnProperty(key)) {
          result[key] = deepCloneCycle(origin[key], hashMap); // 【递归】
        }
      }
    }
  } else { // 【类型判断】原始类型直接返回
    return origin;
  }
  return result;
}

当然对于对象上存在Symbol属性的还需要在遍历key的时候把Object.getOwnPropertySymbols(obj)也算上去

6 对象merge合并

两个对象合并在业务中非常有用,一般替合并规则为:一个原对象A,一个新对象B,将B合并到A上,对于其中的某个属性:

1. A和B该属性为基本数据类型,B直接覆盖A的该属性
2. A该属性为对象,而B为基本数据类型,报错!
3. A是基本数据类型,B为对象,直接覆盖
4. A和B该属性都是对象,递归合并
5. 特殊的,对于属性是数组的,合并中看作基本数据类型!!

示例程序:

// 定义一个函数用于判断如惨是否为对象
function isObj(obj){
	if(typeof obj === "object" && obj instanceof Object && !(obj instanceof Array)) return true;
	return false;
}
// merge函数,将后者合并到前者
function mergeObj(objo,objn){
	let isObjo = isObj(objo);
	let isObjn = isObj(objn);
	if(isObjo && !isObjn) Throw new TypeError("merge function param must be object!");
	if(!isObjo && !isObjn) return objn
	if(!isObjo && isObjn) return objn
	Object.keys(objn).forEach(key=>{
		objo[key] = mergeObj(objo[key],objn[key])
	})
}

7 函数柯里化(bind&currying)

简单理解即:调用一个函数返回一个新函数
思想:利用闭包,将目标函数需要处理的一些参数预先处理,并返回该函数
借助工具:bind/call/apply
自己实现一个bind:

function bind(func,context,...args){
	return function proxy(){
		return func.call(context,...args)
	}
}

对于IE8及以下的浏览器,由于不支持bind,需要自己定一个放在function原型上
示例函数:

~ function(proto){
	function bind(context,...args){
		let that = this;
		return function(){
			return that.call(context,...args)
		}
	}
	proto.bind = bind;
}(Function.prototype)

8 AOP切面编程

POP:面向过程
OOP:面相对象
AOP:面相切面

示例代码:

// 需要实现的功能
function f(a){console.log('f')}
f = f.before(()=>{console.log('before'}).after(()=>{console.log('after'})
f(a) // before f after

// 实现
// 分析:
// 1. 需要在函数上调用方法,所以方法必须写在函数原型上
// 2. 调用后返回的是一个函数,该函数可以接受参数
Function.prototype.before = function(callback){
	if(typeof callback !== "function") throw new TypeError("callback类型错误")
	let that = this
	return function(...params){
		callback.call(this,...params)
		return that.call(this,...params)
	}
}
Function.prototype.after = function(callback){
	if(typeof callback !== "function") throw new TypeError("callback类型错误")
	let that = this
	return function(...params){
		let res = that.call(this,...params)
		callback.call(this,...params)
		return res
	}
}

9 设计模式

单例设计模式singleton

单独的实例,来管理模块中内容,使模块之间独立划分

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>