文章目录
一、深浅拷贝
1.1 浅拷贝
浅拷贝会创建一个新的对象,这个对象有着原始对象属性值的一份精准拷贝:
- 如果原始对象属性类型为基本类型,拷贝的就是基本类型的值,因此修改原、新对象的基本类型属性互不影响
- 如果原始对象属性类型为引用类型,拷贝的就是内存地址(或指向该地址的指针),因此修改原、新对象的引用类型属性会相互影响
function clone(origin) {
let result = {}
for (let prop in origin) {
if (origin.hasOwnProperty(prop)) {
result[prop] = origin[prop]
}
}
return result
}
const person1 = {
name: 'jack',
age: 18,
hobby: {h1: 'sing'}
}
let person2 = clone(person1)
person2.age = 20
person2.hobby.h1 = 'jump'
// 同时被改变,说明是同一个引用
console.log(person1)//{ name: 'jack', age: 18, hobby: { h1: 'jump' } }
console.log(person2)//{ name: 'jack', age: 20, hobby: { h1: 'jump' } }
实现浅拷贝的方式:
- Object.assign():该方法将所有可枚举的自身属性从一个或多个源对象复制到目标对象。它返回目标对象。
Object.assign(target, source)
- Array.Prototype.concat():该方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。
let arr2=arr.concat(); //将arr浅拷贝,生成arr2
- Array.Prototype.slice():该方法返回一个新的副本对象,该对象是一个由 begin和end决定的原先的浅拷贝(包括begin,不包括end,左闭右开)。原始序列不会被改变。
let arr2=arr.slice(); //将arr浅拷贝,生成arr2
1.2 深拷贝
完全复制另外一个对象,引用也是自己创建。即完整复制舒服的值(而非引用)。目的在于避免拷贝后数据对原数据产生影响。
实现深拷贝的方式:
- JSON方法实现:利用JSON的parse()和stringfy()实现对某一个对象的深拷贝(无法处理源对象中的函数)
let arr = [1, 3, {username: 'kobe'} ]; let arr4 = JSON.parse(JSON.stringify(arr)); arr4[2].username = 'james'; console.log(arr, arr4) //[1,3,{username:'kobe'}] [1,3,{username:'james'}]
- 手写递归方法:递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝
//定义检测数据类型的功能函数 function checkedType(target) { return Object.prototype.toString.call(target).slice(8, -1) } //实现深度克隆---对象/数组 function clone(target) { //判断拷贝的数据类型 //初始化变量result 成为最终克隆的数据 let result, targetType = checkedType(target) if (targetType === 'object') { result = {} } else if (targetType === 'Array') { result = [] } else { return target } //遍历目标数据 for (let i in target) { //获取遍历数据结构的每一项值。 let value = target[i] //判断目标结构里的每一值是否存在对象/数组 if (checkedType(value) === 'Object' || checkedType(value) === 'Array') { //对象/数组里嵌套了对象/数组 //继续遍历获取到value值 result[i] = clone(value) } else { //获取到value值是基本的数据类型或者是函数。 result[i] = value; } } return result }
- 函数库lodash:该函数库也有提供 _.cloneDeep用来做深拷贝。
var _ = require('lodash'); var obj1 = { a: 1, b: { f: { g: 1 } }, c: [1, 2, 3]}; var obj2 = _.cloneDeep(obj1); console.log(obj1.b.f === obj2.b.f); // false
二、防抖节流
2.1 防抖
对于高频触发的函数,我们并不想频发触发事件,比如说搜索框实时发请求,onmousemove, resize, onscroll等等,这个时候就需要对函数增加防抖功能了。
<div id="content" style="height:150px;line-height:150px;text-align:center; color: #fff;background-color:#ccc;font-size:80px;"></div>
<script>
let num = 1;
let content = document.getElementById('content');
function count() {
content.innerHTML = num++;
};
content.onmousemove = fangdou(count,1000,true);
// //fn:防抖的函数 //wait:需要延迟的时间
// function debounce(fn, wait) {
// //定时器,用来setTimeout
// let timeout;
//
// //返回一个函数,这个函数会在一个时间区间结束后的wait毫秒时执行fn函数
// return function () {
//
// //保存函数调用时候的上下文和参数,传递给fn
// let context = this;
// let args = arguments;
//
// // 每次这个返回的函数被调用,就清除定时器,以保证不执行 fn
// if (timeout){
// clearTimeout(timeout);
// }
//
// // 当返回的函数被最后一次调用后(也就是用户停止了某个连续的操作),
// // 再过 delay 毫秒就执行 fn
// timeout = setTimeout(() => {
// fn.apply(context, args)
// }, wait);
// }
// }
// //未立即执行版本
// function fangdou(fn,wait){
// let timeout;
// return ()=>{
// let context=this;
// let args=arguments;
// clearTimeout(timeout);
// timeout=setTimeout(()=>{
// fn.apply(context,args)
// },wait)
// }
// }
//完整版本(未立即执行和立即执行都在里边)
function fangdou(fn,wait,immediate){
let timeout;
return ()=>{
let context=this;
let args=arguments;
clearTimeout(timeout);
if(immediate){
let callNow=!timeout;
if(callNow){
fn.apply(context,args);
}
timeout=setTimeout(()=>{
timeout=null
},wait)
}else {
timeout=setTimeout(()=>{
fn.apply(context,args)
},wait)
}
}
}
</script>
2.2 节流
在某个规定的时间内,节流函数至少执行一次。
<div id="content" style="height:150px;line-height:150px;text-align:center; color: #fff;background-color:#ccc;font-size:80px;"></div>
<script>
let num = 1;
let content = document.getElementById('content');
function count() {
content.innerHTML = num++;
};
content.onmousemove = throttle(count,1000);
//options为可传入的参数,默认传入{leading=true;trailing=true}
//如果传入leading=false,则在wait时间内没有触发的话,wait秒后自动执行func函数;
//如果传入trailing=false,在wait时间内没有触发的话,下一次触发会执行func函数。
function throttle(func, wait, options) {
let time, context, args, result;
let previous = 0;
if (!options) options = {};
let later = function() {
previous = options.leading === false ? 0 : new Date().getTime();
time = null;
func.apply(context, args);
if (!time) context = args = null;
};
return ()=> {
let now = new Date().getTime();
if (!previous && options.leading === false) previous = now;
let remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
if (time) {
clearTimeout(time);
time = null;
}
previous = now;
func.apply(context, args);
if (!time) context = args = null;
} else if (!time && options.trailing !== false) {
time = setTimeout(later, remaining);
}
};
}
</script>
三、函数高级
3.1 原型与原型链
显式原型与隐式原型
每个函数都有一个prototype属性,它默认指向一个Object实例空对象(原型对象),原型对象中有一个属性constructor,它指向函数对象。
每个函数function都有一个prototype,即显式原型(属性);每个实例对象都有一个__proto__
,可称为隐式原型(属性)。
实例对象的隐式原型的值为其构造函数的显式原型的值。
原型链
访问一个对象的属性时,先在自身属性中查找, 如果找到就返回,如果没找到,沿着__proto__
这条链向上找,直到找到就返回,如果没找到,返回undefined。
- 别名:隐式原型链
- 作用:查找对象属性(方法)
- Function的prototype与
__proto__
是指向一个地方 - 所有函数的
__proto__
都是相等的,因为都是New Function()创建的,都等于Function.prototype。 - 函数的显式原型指向的对象默认是空的Object实例对象(Object不满足)
- Object的原型对象是原型链尽头!(
Object.prototype.__proto__=null
)
属性问题
- 当我们为对象设置属性的时候,是不看原型链的,如果原型链中也有此属性,在读取该属性的时候,会读取属性内部的属性而不是原型对象的属性。
- 读取对象属性的时候会自动到原型链中寻找
变量提升
<script>
console.log(fn) //fn 以函数形式声明
console.log(fn2) //undefined 以变量形式定义
console.log(a) //undefined
var a = 2
function fn(){
console.log('fn')
}
var fn2 = function (){
console.log(fn2)
}
</script>
3.2 执行上下文与执行上下文栈
全局执行上下文
在执行全局代码前将window确定为全局执行上下文
对全局数据进行预处理
- var定义的全局变量 ===> undefined,添加为window属性
- unction声明的全局函数 ===> 赋值(fun),添加为window的方法
- this ===> 赋值为window
函数执行上下文
1、在调用函数,准备执行函数体之前,创建对应的函数执行上下文对象
2、对局部数据进行预处理:
- 形参变量 ====> 赋值(实参数据) ==> 添加为执行上下文的属性
- arguments ====> 赋值(实参列表),添加为执行上下文的属性
- var定义的局部变量 ===> undefined,添加为执行上下文的属性
- function声明的函数 ===> 赋值(fun),添加为执行上下文的属性
- this ===> 赋值(调用函数的对象)
3、开始执行函数体代码
执行上下文栈
- 在全局代码执行前,JS引擎就会创建一个栈来存储管理所有的执行上下文对象
- 在全局执行上下文(window)确定后,将其添加到栈中(压栈)
- 在函数执行上下文创建后,将其添加到栈中(压栈)
- 在当前函数执行完后,将栈顶的对象移除(出栈)
- 当所有的代码执行完后,栈中只剩下window
3.3 闭包
如何产生闭包?
当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时,就产生了闭包
闭包的个数 = 调用外部函数的次数
闭包到底是什么?
- 闭包是嵌套的内部函数
- 闭包是包含被引用变量(函数)的对象
注:执行函数定义就会产生闭包(不用调用内部函数)
function fn1() {
let a = 1
let b = 'a'
function fn2() {
console.log(a)
}
}
fn1()
常见的闭包
- 将函数作为另外一个函数的返回值
function fn1() { let a = 1 function fn2() { a++ console.log(a) } return fn2 } const f = fn1() f()//2 f()//3
- 将函数作为实参传递给另一个函数调用
function showDelay(msg, time) { setTimeout(() => { console.log(msg) }, time) } showDelay('hello', 1000)
闭包的作用
- 使函数内部的变量在函数执行完后,仍然存活在内存中(延长了局部变量的生命周期)
- 让函数外部可以操作(读写)到函数内部的数据(变量/函数)
两个问题
Q1:函数执行完后,函数内部声明的局部变量是否还存在?
A1:一般是不存在,存在于闭包中的变量才可能存在,但也必须要有引用指向该函数。
Q2:在函数外部能直接访问函数内部的局部变量吗?
A2:不能,但我们可以通过闭包的形式让外部操作它
闭包的生命周期
- 产生:在嵌套内部函数定义执行完时就产生了(不是在调用)
- 死亡:在嵌套的内部函数成为垃圾对象时
闭包应用:自定义JS模块(具有特定功能的JS文件)
将所有的数据和功能都封装在一个函数内部,只向外暴露一个包含n个方法的对象或函数。
模块的使用者,只需要通过模块暴露的对象调用方法来实现相应功能
方式1:
function myModule() {
//私有数据
let msg = 'hello'
//操作数据的函数
function do1() {
console.log(`do1-${msg}`)
}
function do2() {
console.log(`do2-${msg}`)
}
//向外暴露
return { do1, do2 }
}
let module2 = myModule()
module2.do1()//do1-hello
module2.do2()//do2-hello
方式二:
<script>
(function myModule() {
//私有数据
let msg = 'hello'
//操作数据的函数
function do1() {
console.log(`do1-${msg}`)
}
function do2() {
console.log(`do2-${msg}`)
}
//向外暴露
window.module2 = {do1, do2}
})()
module2.do1()//do1-hello
module2.do2()//do2-hello
</script>
内存泄漏
闭包缺点:
- 函数执行完后,函数内的局部变量没有释放,占用内存时间会变长
- 容易造成内存泄漏(内存被垃圾对象占用)
解决:
- 能不用闭包就不用
- 及时释放
常见的内存泄漏:
- 意外的全局变量
- 没有及时清理的计时器或回调函数
- 闭包
<script>
function fn1(){
var arr = new Array[100000]
function fn2(){
console.log(arr.length)
}
return fn2
}
var f = fn1()
f()
f = null; //让内部函数称为垃圾对象 --> 回收闭包,解决内存泄漏
</script>
内存溢出
内存溢出是一种程序运行出现的错误,当程序运行需要的内存超过了剩余的内存时,就会抛出内存溢出的错误。
内存泄漏是指没有及时释放占用的内存,内存泄漏积累多了就容易导致内存溢出。
<script>
var obj = {}
for(var i = 0; i < 10000; i++){
obj[i] = new Array(10000000)
}
</script>
四、面向对象高级
4.1 对象创建模式
① Object构造函数模式
- 先创建空Obeject对象,然后动态添加属性、方法
- 使用场景:起初不确定对象内部数据
- 问题:语句太多
② 对象字面量模式
- 使用{}创建对象,同时指定属性、方法
- 使用场景:起初时对象内部数据是确定的
- 问题:如果创建多个对象,有重复代码
③ 工厂模式
- 通过工厂函数动态创建对象并返回
- 使用场景:需要创建多个对象
- 问题:对象没有一个具体的类型,都是Object类型
function createPerson(name,age){
var obj = {
name:name,
age:age,
setName: function(name){
this.name=name
}
}
return obj;
}
④ 自定义构造函数模式
- 自定义构造函数,通过new创建对象
- 使用场景:需要创建多个类型确定的对象
- 问题:每个对象都有相同的数据,浪费内存
function Student(name,price){
this.name=name
this.price=price
}
var s = new Student('Bob',13000)
console.log(s instanceof Student)
⑤ 构造函数+原型的组合模式
- 自定义构造函数,属性在函数中初始化,方法添加到原型上
- 使用场景:需要创建多个类型确定的对象
function Person(name,age){
this.name=name;
this.age=age;
}
Person.prototype.setName = function(name) {
this.name = name;
}
4.2 继承模式
① 原型链继承
- 定义父类的构造函数,给父类的原型添加方法
- 定义子类的构造函数
- 创建父类的对象赋值给子类的原型
- 将子类的原型的构造属性设置为子类型
- 给子类型原型添加方法
- 创建子类型的对象:可以调用父类型的方法
- 关键:子类型的原型为父类型的实例
//父
function Father() {
this.superProp = 'father property'
}
Father.prototype.show = function () {
console.log(this.superProp)
}
//子
function Son() {
this.subProp = 'son property'
}
Son.prototype = new Father() // 这是关键,子类型的原型为父类型的实例
Son.prototype.constructor = Son // 让子类的原型的constructor指向子类型
Son.prototype.show2 = function () {
console.log(this.subProp)
}
const son = new Son()
son.show()//father property
son.show2()//son property
② 借用构造函数继承
function Person(name,age){
this.name = name;
this.age = age;
}
function Student (name,age,price){
Person.call(this,name,age) //相当于 this.Person(name.age)
// this.name = name;
// this.age = age;
this,price = price;
}
var s = new Student('Tom',20,14000)
console.log(s.name,s.age,s.price);
③ 组合继承
function Person(name,age){
this.name = name;
this.age = age;
}
Person.prototype.setName = function(name){
this.name = name;
}
function Student (name,age,price){
Person.call(this,name,age) //相当于 this.Person(name.age)
// this.name = name;
// this.age = age;
this.price = price;
}
Student.prototype = New Person() // 为了能看到父类的方法
Student.prototype.constructor = Student //修正constructor属性
Student.prototype.setPrice = function (price){
this.price = price
}
var s = new Student('Tom',24,15000)
s.setName('Bob')
s.setPrice(16000)
console.log(s.name,s.age,s.price) // Bob 24 16000
五、线程机制与事件机制
5.1 进程与线程
进程:程序的一次执行,它占有一片独有的内存空间;可以通过windows任务管理器查看进程。
线程:进程内的一个独立执行单元,是程序执行的一个完整流程,是CPU的最小的调度单元。
相关知识:
- 应用程序必须运行在某个进程的某个线程上
- 一个进程中至少有一个运行的线程:主线程,进程启动后自动创建
- 一个进程中也可以同时运行多个线程,我们会说程序是多线程运行的
- 一个进程内的数据可以供其中的多个线程直接共享
- 多个进程之间的数据是不能直接共享的
- 线程池(Thread Pool):保存多个线程对象的容器,实现线程对象的反复利用
5.2 浏览器内核
内核模块:
运行在主线程上:
- JS引擎模块:负责js程序的编译与运行
- html,css文档解析模块:腐恶页面文本的解析
- DOM/CSS模块:负责DOM/CSS在内存中的相关处理
- 布局和渲染模块:负责页面的布局和效果的绘制(内存中的对象)
运行在分线程上:
- 定时器模块:负责定时器的管理
- 事件响应模块:负责事件的管理
- 网络请求模块:负责ajax请求
5.3 定时器引发的思考
定时器真是定时执行的吗?
定时器并不能保证真正定时执行;一般会延迟一丁点(可以接受),也有可能延迟很长时间(不能接受)。
定时器回调函数是在分线程执行的吗?
在主线程执行的,js是单线程的
定时器是如何实现的?
事件循环模型
<script>
document.getElementById('btn').onclick = function(){
var start = Date.now()
console.log('启动定时器前...')
setTimeout(function(){
console.log('定时器执行了',Date.now()-start) //582!
},200)
console.log('启动定时器后')
}
//时间长的任务
for(var a = 0;a < 1000000;a++){
}
</script>
5.4 JS是单线程执行的
如何证明js执行时单线程的?
- setTimeout()的回调函数是在主线程执行的
- 定时器回调函数只有在运行栈中的代码全部执行完之后才有可能执行
为什么js要用单线程模式,而不是多线程模式?
Javascript的单线程,与它的用途有关作为,浏览器脚本语言,Javascript的主要用途是与用户交互,以及操作DOM,这决定了它只能是单线程,否则会带来很复杂的同步问题。
代码的分类
- 初始化代码
- 回调代码
js引擎执行代码的基本流程
- 先执行初始化代码:包含一些特别的代码,比如设置定时器、绑定事件监听、发送ajax请求
- 后面在某些时刻才会执行回调代码
5.5 浏览器的时间循环(轮询)模型
模型原理图:
模型的2个重要组成部分:
- 事件(定时器/DOM事件/Ajax)管理模块
- 回调队列
模型的运转流程:
- 执行初始化代码,将事件回调函数交给对应模块管理
- 当事件发生时,管理模块会将回调函数及其数据添加到回调队列中
- 只有当初始化代码执行完后(可能要一定时间),才会遍历去回调队列中的回调函数执行。