一、编程思想
-
面向过程编程
面向过程就是分析出解决问题需要的步骤,然后用函数把这些步骤一步一步实现,实现的时候再一个一个地依次调用就可以了。
-
面向对象(oop)
面向对象是把事务分解成为一个个对象,然后由对象之间分工合作。面向对象以功能来划分问题。
-
面向对象的特点
面向对象编程具有灵活、代码可复用、容易维护和开发的优点,更适合多人合作的大型软件项目。
特性:封装性、继承性、多态性。
-
面向过程和面向对象的优缺点
- 面向过程:
- 优点:性能比面向对象搞,适合跟硬件联系很紧密的东西,例如单片机就采用的面向过程编程
- 缺点:没有面向对象易维护、易复用、易扩展
- 面向对象:
- 易维护、易复用、易扩展,由于面向对象有封装、继承、堕胎性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护
- 性能比面向过程低
- 面向过程:
-
封装性
JS中使用构造函数实现封装。构造函数实例创建的对象彼此独立、互不影响。
但是构造函数存在浪费内存的问题(为了解决这个问题,引入了原型)。
二、原型
原型可以解决多个对象实例之间资源共用的问题,原型可以理解成是一块共享内存块,在这个内存块中存储着一份数据,所有的实例需要的时候,只需要访问该内存块即可,从而达到共用共享的问题。
-
原型对象(
prototype
)-
构造函数通过原型分配的函数是所有对象所共享的;
-
JavaScript 规定,每一个构造函数都有一个
prototype
属性,指向另一个对象,所以我们也称为原型对象; -
这个对象可以挂载函数,对象实例化必会多次创建原型上的函数,节约内存;
-
我们可以把那些不变的方法,直接定义在
prototype
对象上,这样所有对象的实例就可以共享这些方法; -
构造函数和原型对象中的
this
都指向实例化对象。const arr = [1, 2, 3]; Array.prototype.max = function() { // 使用展开运算符 // this 指向实例化对象 return Math.max(...this) } console.log(arr.max()); // 输出:3
-
-
constructor
属性 每个原型对象里面都有个
constructor
属性(构造函数),该属性指向该原型对象的构造函数。也就是构造函数名.prototype.constructor === 构造函数名
为true
。 -
对象原型(
__proto__
) 对象都会有一个属性
__proto__
指向构造函数的prototype
原型对象,之所以实例对象可以使用构造函数原型对象(prototype
)的属性和方法就是因为有对象原型(__proto__
)的存在。__proto__
是 JS 非标准属性;[[prototype]]
和__proto__
意义相同;__proto__
对象原型里面也有一个constructor
属性,指向创建该实例对象的构造函数。
-
原型继承
继承是面向对象编程的另一个特征,通过继承进一步提升代码封装的程度,JavaScript 中大多是借助原型对象实现继承的特性。
/** // 定义一个对象 const fruit = { apple: 1, banana: 2 } // 定义两个不同的构造函数,他们有公共部分,都继承自同一个对象 function Woman(){}; function Man(){}; // 通过原型继承fruit对象 Woman.prototype = fruit; Man.prototype = fruit; // 指回原来的构造函数 Woman.prototype.constructor = Woman; Man.prototype.constructor = Man; const p1 = new Woman(); const p2 = new Man(); console.log(p1); console.log(p2); // 上面写法有一个问题:由于指向的是同一个对象,当其中一个修改了另一个也会跟着修改 Woman.prototype.name = 'Woman'; console.log(p1, p2); // 两个都变了 **/ // 使用构造函数,定义一个构造函数 function Fruit () { this.apple = 1, this.banana = 2 } // 定义两个不同的构造函数,他们有公共部分,都继承自同一个对象 function Woman(){}; function Man(){}; // 通过原型继承Fruit构造函数 Woman.prototype = new Fruit; Man.prototype = new Fruit; // 指回原来的构造函数 Woman.prototype.constructor = Woman; Man.prototype.constructor = Man; const p1 = new Woman(); const p2 = new Man(); console.log(p1); console.log(p2); Woman.prototype.name = 'Woman'; console.log(p1, p2); // 只有p1变了
-
原型链
基于原型对象的继承使得不同构造函数的原型对象关联在一起,并且这种关联的关系是一种链状的结构,我们将原型对象的链状结构关系称为原型链。
查找规则:
- 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性(方法);
- 如果没有就查找它的原型(也就是
__proto__
指向的prototype
原型对象); - 如果还没有就查找原型对象的原型;
- 一次类推一直找到
Object
为止(null
); __proto__
对象原型的意义就在于为对象查找机制提供一个方向;- 可以使用
instanceof
运算符用于检测构造函数的prototype
属性是否出现在某个实例对象的原型链上。语法:某个实例对象 instanceof 某个构造函数
。
三、深浅拷贝
深浅拷贝只针对引用数据类型。
-
浅拷贝
浅拷贝拷贝的是地址,常见方法有:拷贝对象(
Object.assign、{...obj}
)、拷贝数组(Array.prototype.concat()、[...arr]
)。 如果是简单数据类型拷贝的是值,引用数据类型拷贝的是地址(如果是单层对象没有问题,如果是多层对象还会出现问题)。
-
深拷贝
深拷贝拷贝的是对象,不是地址。常见方法:递归实现、
lodash的_.cloneDeep()
、JSON.stringfy()
。1)递归
如果一个函数在内部可以调用其本身,那么这个函数就是递归函数。
-
递归函数的作用和循环效果类似;
-
由于递归很容易发生“栈溢出”错误(stack overflow),所以必须加退出条件
return
。const obj = { name: '张三', age: 18, hobby: ['唱歌', '跳舞'], info: { math: 80, chinese: 90 } } // 利用回调函数实现深拷贝 function deepCopy(newObj, oldObj) { for (let k in oldObj) { // 处理引用数据类型,先判断数组再判断对象 // 因为 [2, 3] instanceof Object 的结果为 true if (oldObj[k] instanceof Array) { // 判断旧对象中属性值是否是数组 // 是数组给新对象赋值空数组 newObj[k] = []; // 再次调用函数依次将旧对象数组的值赋值给新对象中的数组 deepCopy(newObj[k], oldObj[k]) } else if (oldObj[k] instanceof Object) { // 判断旧对象中属性值是否是对象 // 是对象给新对象赋值空对象 newObj[k] = {}; // 再次调用函数依次将旧对象中对象的值赋值给新对象中的对象 deepCopy(newObj[k], oldObj[k]) } else { // 简单类型,依次赋值 newObj[k] = oldObj[k] } } } const o = {} deepCopy(o, obj); obj.hobby.push('aas') console.log(obj); console.log(o);
2)JS 中的 lodash 库
在lodash里面cloneDeep实现了深拷贝。
使用方法:
旧对象 = _.cloneDeep(新对象)
。3)利用
JSON.stringfy
实现深拷贝
JSON.stringfy(obj)
会将可以将对象转换为 JSON字符串,字符串是简单字符串,可以直接赋值,然后再使用JSON.parse(JSON字符串)
将字符串转为对象,也就是新对象 = JSON.parse(JSON.stringfy(旧对象))
。 -
四、异常处理
异常处理是指预估代码执行过程中可能发生的错误,然后最大程度地避免错误发生导致整个程序无法继续运行。
-
throw
抛出异常throw
抛出异常信息,程序也会终止执行;throw
后面跟的是错误提示信息,如throw '发生错误,参数不能为空'
、throw new Error('发生错误,参数不能为空')
;- 配合
Error对象
使用,能设置更详细的错误信息。
-
try……catch……
捕获错误信息 通过
try……catch……
可以捕获浏览器提供的错误信息,有try、catch、finally
三个关键字。function fn(arr) { try { // 可能会发生错误的代码 arr.push(arr.length); console.log(arr); } catch (error) { // 拦截错误,但是不中断程序的执行 console.error(error); // 中断程序需要加 return return } // 这个关键字按需求使用 finally { // 不管程序对错,一定会执行的代码 console.log(111); } // 如果try中的代码出错且 catch中加了return,这里的代码不会执行 console.log(222); } fn('0', '6'); // 报错 fn(['0', '6']) // 继续输出:['0', '6', 2]
-
debugger
debugger 语句用于停止执行 JavaScript,并调用 (如果可用) 调试函数。
使用 debugger 语句类似于在代码中设置断点。
五、处理 this
-
普通函数
普通函数的调用方式决定了
this
的值,一般是谁调用指向谁。 普通函数没有明确调用者时
this
指向window
,严格模式下没有调用者时this
指向undefined
。 -
箭头函数
实际上箭头函数中并不存在
this
,箭头函数会默认绑定外层this
的值。箭头函数中的this
引用就是最近作用域中的this
。 事件回调函数使用箭头函数时,
this
为全局的window
,因此,DOM 事件回调函数如果需要DOM对象的this
,不推荐使用箭头函数。 构造函数,原型函数,字面量对象中函数也不推荐采用箭头函数。
-
改变
this
指向-
call()
方法 使用 call 方法调用函数,同时指定被调用函数中
this
的值。 语法:
fun.call(指定的this值, 其他参数1, 其他参数2, ...)
,返回值就是函数的返回值。 -
apply()
方法 使用 call 方法调用函数,同时指定被调用函数中
this
的值。 语法:
fun.apply(指定的this值, [其他参数1, 其他参数2, ...])
,返回值就是函数的返回值。 -
bind()
方法 bind 方法不会调用函数,但是能改变函数内部
this
指向。 语法:
fun.bind(指定的this值, 其他参数1, 其他参数2, ...)
,返回是一个函数,是一个拷贝自原函数但是已经改变了this
指向的新函数。
const obj = { name: 'Lily', } function fn (a, b) { console.log(this); console.log(a + b); } fn(1, 2); // this 指向window,输出 window对象和 3 // 使用 call() fn.call(obj, 1, 2); // this 指向obj,输出 obj对象和 3 // 使用 apply(),其他参数必须包在数组里 fn.apply(obj, [1, 2]); // this 指向obj,输出 obj对象和 3 // 使用 bind 方法 fn.bind(obj, 1, 2); // 没有调用函数,所以没有输出 // 定义一个参数接收改变了this指向的返回值,并调用新函数 const fun = fn.bind(obj, 1, 2); fun(); // this 指向obj,输出 obj对象和 3 // 打印原函数,this指向没有变 fn(1, 2); // this 指向window,输出 window对象和 3
- 异同点
- 相同点:都可以改变
this
指向。 - 不同点:
call()和apply()
都会调用函数,但这两者传递参数形式不一样;bind()
不会调用函数。
- 相同点:都可以改变
-
六、性能优化——防抖(debounce
)
防抖是指单位时间内,频繁触发事件,只执行最后一次。
在一个代码执行未完成时,如果再次被触发,则会取消上次未完成的,重新开始准备执行,循环往复,只执行最后一次。
防抖的实现方式:lodash
提供的防抖、手写防抖函数。
-
lodash
提供的防抖 语法:
_.debounce(func, [wait=0], [options=])
,创建一个 debounced(防抖动)函数,该函数会从上一次被调用后,延迟wait
毫秒后调用func
方法。 使用参考:lodash.debounce | Lodash中文文档 | Lodash中文网 (lodashjs.com)。
使用场景:
- 搜索框搜索输入。只需用户最后一次输入完,再发送请求。
- 手机号、邮箱验证输入检测。
- 窗口大小
resize
。只需窗口调整完成后,计算窗口大小。防止重复渲染。
-
手写防抖函数
防抖的核心就是利用定时器(
setTimeout
)来实现。-
声明一个定时器变量;
-
当再次触发时先判断是否已经有定时器了,如果有定时器先清除以前的定时器;
-
如果没有定时器则开启定时器。
const box = document.querySelector('.box'); let i = 1; function mouseMove() { box.innerHTML = i++; } // box.addEventListener('mousemove', mouseMove) function debounce(fn, t) { let timer; // 返回一个匿名函数 return function() { if (timer) { clearTimeout(timer); } timer = setTimeout(function() { fn(); // 加小括号调用 fn 函数 }, t) } } box.addEventListener('mousemove', debounce(mouseMove, 500));
-
七、性能优化——节流(throttle
)
节流是指单位时间内,频繁触发事件,只执行一次。
在一个代码执行未完成时期间,如果再次被触发,不会执行。
防抖的实现方式:lodash
提供的节流、手写节流函数。
-
lodash
提供的节流 语法:
_.throttle(func, [wait=0], [options=])
,创建一个节流函数,在 wait 秒内最多执行func
一次的函数。 使用参考:lodash.throttle | Lodash中文文档 | Lodash中文网 (lodashjs.com)。
使用场景:
- 滚动加载,加载更多或滚到底部监听。
- 搜索框,搜索联想功能。
- 高频点击提交,表单重复提交。
-
手写节流函数
节流的核心就是利用定时器(
setTimeout
)来实现。-
声明一个定时器变量;
-
当再次触发时先判断是否已经有定时器了,如果有定时器则不开启新定时器;
-
如果没有定时器则开启定时器。
const box = document.querySelector('.box'); let i = 1; function mouseMove() { box.innerHTML = i++; } // box.addEventListener('mousemove', mouseMove) function debounce(fn, t) { let timer = null; // 返回一个匿名函数 return function() { if (!timer) { timer = setTimeout(function() { fn(); // 清除定时器 timer = null; }, t) } } } box.addEventListener('mousemove', debounce(mouseMove, 500));
-
防抖是单位时间内频繁触发只执行最后一次,而节流是只执行一次。