1-1.使用场景
当需要创建多个对象的时候,如果循环会「重复创建」很多变量,占用内存。 如果用new生成,那么里面重复的属性是在「原型」上,就不用占用内存。
1-2.意义
节省代码,属于语法糖,可以拥有使用构造函数里面的所有「属性和方法」,并且还可以「拓展」。
1-3.实现步骤
传入参数为:构造函数和函数参数
创建1个空对象
使空对象的__proto__指向构造函数的原型(prototype)
将this绑定到空对象上,执行构造函数
判断构造函数的返回值,如果是对象,直接返回这个值。如果不是,就返回开始创建的对象
1-4.代码实现
文字代码(「文字代码和下面的图片代码是一样的,只不过图片代码高亮更强大」):
// 新建构造函数--用来new的函数
// 构造函数是一种特殊的方法:用来在创建对象时为对象成员变量赋初始值
function Dog(name){
// 这里的this指向window,所以下面才需要重新将this指向自身,这样才能将属性挂载成功
this.name = name
this.say = function() {
console.log("my name is" + this.name);
}
}
// 手写new函数
function _new(fn,...arg){
const obj = {};
// 将构造函数的原型挂载到新对象上
Object.setPrototypeOf(obj, fn.prototype)
// 将this指向新对象,执行构造函数
let result = fn.apply(obj, arg);
return result instanceof Object ? result : obj
}
// 验证
var dog = _new(Dog, "caigou")
dog.say();// my name is caigou
图片代码:
2-1.使用场景
测试一个对象是否为一个类的实例,常用于判断一个引用类型的类型。 即对象的隐士原型上是否与构造函数的显示原型匹配
2-2.用法
如 :
console.log(1 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log('str' instanceof String); // false
console.log([] instanceof Array); // true
console.log(function(){} instanceof Function); // true
console.log({} instanceof Object); // true
console.log(null instanceof Object); // false
2-3.优缺点
「优点」:能够区分Array、Object和Function,适合用于判断自定义的类实例对象
「缺点」:Number,Boolean,String基本数据类型不能判断
2-4.实现步骤
传入参数为左侧的实例L,和右侧的构造函数R
处理边界,如果要检测对象为基本类型则返回false
分别取传入参数的原型
判断左侧的原型是否取到了null,如果是null返回false;如果两侧原型相等,返回true,否则继续取左侧原型的原型。
2-5.代码实现
「文字代码」(文字代码和下面的图片代码是一样的,只不过图片代码高亮更强大):
// 手写instanceof函数
function instance_of(L, R){
// 验证如果为基本数据类型,就直接返回false,因为instanceof只能判断引用类型
const baseType = ['string', 'number','boolean','undefined','symbol']
if(baseType.includes(typeof(L)) || L === null) { return false }
let Lp = L.__proto__;
let Rp = R.prototype//函数才有prototype属性
while(true){
if(Lp === null){//找到最顶层还是没找到,说明不匹配
return false
}
if(Lp === Rp){
return true
}
Lp = Lp.__proto__
}
}
// 测试
var a = instance_of([],Array)
console.log(a) //true
「图片代码:」
3-1.使用场景
改变this的指向。
一般来说,es5中this总会指向最后调用它的方法;
es6箭头函数中this指向箭头函数最近一层的非箭头函数。
所以我们看到,this经常让人感到困惑,于是js实现了一个新的函数来帮我们避免这种困惑。
使用call,apply,bind(这3个大体相同),this会指向传入的对象。 当对象不存在时:
严格模式下:this指向undefined
非严格模式下:this指向window
3-2.实现步骤
传入参数为:「this指向的对象」,和「调用函数」要传入的参数
处理边界:
对象不存在,this指向window;
调用的不是函数,抛出错误
将「调用函数」挂载到「this指向的对象」的fn属性上。
执行「this指向的对象」上的fn函数,并传入参数,删除fn属性,返回结果。
3-3.代码实现
「文字代码」(文字代码和下面的图片代码是一样的,只不过图片代码高亮更强大):
// 手写call函数
// call是在函数原型上的,所以我们这里也要挂载到原型上
Function.prototype.call2 = function(obj, ...ary) {
obj = obj || window; //obj为空则指向window
obj.fn = this; //this指被调用的函数,将函数挂载到当前对象的fn属性上
if (typeof this != "function") {
//边界处理
throw new TypeError("Erorr");
}
/**此时this指向的对象的结构
obj:{
fn:bar(){}
}
*/
//this指向最后一个调用函数(fn)的对象(obj),所以指向obj
var result = obj.fn(...ary);//this指向最后一个调用函数的对象,所以指向obj
// 执行完以后删除,因为对象原本没有这个fn属性
delete obj.fn;
return result;
};
// 测试
var value = 2;
var obj1 = {
value:1
}
function bar(name, age){
var myObj = {
name:name,
age:age,
value:this.value
}
console.log(this.value,myObj)
}
bar.call2(null) //打印 2 {name: undefined, age: undefined, value: 2}
bar.call2(obj1,"zi","shu")// 打印 1 {name: "zi", age: "shu", value: 1}
「图片代码:」
4-1.使用场景
当我们需要使用对象并修改对象值时,又不想改变原对象的情况下,如:
vue子组件接收父组件传递过来的props值时,并且子组件需要对这个值进行修改,直接改变props的值会报错。这时,我们就可以在data深拷贝一份props值,然后在data上对应的值进行修改。
4-2.各个方式的比较
JSON.parse(JSON.stringify(copyObj))
「优点」 使用简单
「缺点」
如果对象里的函数,正则,date无法被拷贝下来
无法拷贝copyObj对象原型链上的属性和方法
当数据的层次很深,会栈溢出
手写拷贝
「优点」 可以考虑到各种情况
「缺点」 实现较为复杂
4-3.赋值、浅拷贝与深拷贝的区别
4-4.实现步骤
如果传入的对象不存在,就返回null;如果是特殊对象,就new一个特殊对象。
创建一个对象objClone,来保存克隆的对象。
然后遍历对象,如果是基础数据,就直接放入objClone
如果是对象,就递归。
4-5.代码实现
「文字代码」(文字代码和下面的图片代码是一样的,只不过图片代码高亮更强大):
// 手写深拷贝函数
function deepClone(obj){
if(obj == null){
return null
}
if(obj instanceof RegExp){
return new RegExp(obj)
}
if(obj instanceof Date){
return new Date(obj)
}
var objClone = Array.isArray(obj) ? [] : {}
for(let key in obj){
if(obj.hasOwnProperty(key)){
//如果还是对象,就递归
if(obj[key] && typeof obj[key] === "object"){
objClone[key] = deepClone(obj[key])
}else{
objClone[key] = obj[key]
}
}
}
return objClone
}
// 测试
var dog = {
name: {
chineseName:"狗"
}
}
var newDog = deepClone(dog)
newDog.name.chineseName = "新狗"
console.log("newDog",newDog)//{ name: { chineseName:"新狗"}}
console.log("dog",dog)//{ name: { chineseName:"狗"}}
「图片代码:」
5-1.使用场景
我们平常开发的过程中,有很多场景会「频繁触发事件」,比如说搜索框实时发请求,onmousemove,resize,onscroll等等。
为了性能,有些时候,我们并不能或者不想频繁触发事件,函数防抖是在事件被触发n秒后再执行回调,如果在「n秒内又被触发」,则「重新计时」。
5-2.实现步骤
传入参数为执行函数fn,延迟时间wait。
定义一个定时器变量n,初始值为null。
返回一个函数,当n不为null的时候,意味着已经有了定时器,要清除它。
否则重新计时。
5-3.代码实现
「文字代码」(文字代码和下面的图片代码是一样的,只不过图片代码高亮更强大):
// 手写防抖函数
function debounce(fn, wait){
var timer = null;
return function() {
// 有定时器了,在规定时间内再触发就要清除前面的定时,重新计时
if(timer !== null){
clearTimeout(timer)
}
// 重新计时
timer = setTimeout(fn, (wait));
}
}
// 测试
function handle(){
console.log(Math.random())
}
// 窗口大小改变,触发防抖,执行handle
window.addEventListener("resize",debounce(handle,1000))
「图片代码:」
「怎么记住防抖和节流呢?」
防抖即抖音(「抖延」):防抖会延迟执行
节流即「一流」:节流时间内,只执行一次
6-1.使用场景
当事件触发时,保证「一定时间段」内只「调用一次」函数。例如页面滚动的时候,每隔一段时间发一次请求
6-2.实现步骤
传入参数为执行函数fn,等待时间wait。
保存初始时间now。
返回一个函数,如果超过等待时间,执行函数,将now更新为当前时间。
6-3.代码实现
「文字代码」(文字代码和下面的图片代码是一样的,只不过图片代码高亮更强大):
// 手写节流函数
function throttle(fn, wait,...args){
var pre = Date.now();
return function() {
// 函数可能会有入参
var context = this
var now = Date.now()
if(now - pre >= wait){
// 将执行函数的this指向当前作用域
fn.apply(context,args)
pre = Date.now()
}
}
}
// 测试
var name = "夏"
function handle(val){
console.log(val+this.name)
}
// 滚动鼠标,触发防抖,执行handle
window.addEventListener("scroll", throttle(handle,1000,"龙"))
「图片代码:」
7-1.new Set 方法
let arr = [1,2,2,2,33,33,4]
console.log([...new Set(arr)])//[1,2,33,4]
7-2. 手写去重函数(遍历获取唯一值到新数组)
let arr = [1,2,2,2,33,33,4]
let arrNew = []
arr.forEach((item,index)=>{
if(arrNew.indexOf(item)===-1){
//或者 if(!arrNew.includes(item)){
arrNew.push(item)
}
})
console.log(arrNew)//[1,2,33,4]
7-3. 利用map的键不能重复
// 利用map的键不能重复,去掉某个属性相同的项
function uniqBy(arr,key){
return [...new Map(arr.map(item=>[item[key],item])).values()]
}
const singers = [
{id:1,name:"lo"},
{id:1,name:"ming"},
{id:2,name:"li"}
]
console.log(uniqBy(singers,"id"))// [{id:1,name:"ming"},{id:2,name:"li"}]
「图片代码:」
8-1.concat
let arr = ["a","b"]
let arr1 = ["c","d"]
console.log(arr.concat(arr1))//["a","b","c","d"]
8-2.es6展示符
let arr2 = [...arr,...arr1]
console.log(arr2)//["a","b","c","d"]
「图片代码:」
9-1. flat
let arr = [1,2,[3,4],[5,6,[7,8,9]]]
console.log(arr.flat(Infinity))//[1, 2, 3, 4, 5, 6, 7, 8, 9]
9-2. join, split
console.log(arr.toString().split(",").map(Number))//[1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log(arr.join().split(",").map(Number))//[1, 2, 3, 4, 5, 6, 7, 8, 9]
「图片代码:」
❝let arr = [1,2]
❞
10-1. instanceof
console.log(arr instanceof Array)
10-2. constructor
console.log(arr.constructor === Array)
10-3. constructor
console.log(arr.constructor === Array)
10-4. 判断对象是否有数组的push等方法
console.log(!!arr.push && !!arr.concat)
10-5. constructor
console.log(Array.isArray(arr))
(1)冒泡排序
概念
从第一个元素开始,把当前元素和下一个索引元素进行比较。如果当前元素大,那么就交换位置,重复操作直到比较到最后一个元素
动图演示
冒泡排序
步骤
先遍历一共有多少个数要跟其它数比较
再遍历每个数要跟其它数比较多少次
如果前一个数小于后一个数,则交换位置
手写代码
function bubbleSort(arr) {
var len = arr.length;
//多少个数要跟其它数比较
for (var i = 0; i < len; i++) {
//每个数要跟其它数比较多少次
for (var j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j+1]) { //相邻元素两两对比
//元素交换
[arr[j+1],arr[j]] = [arr[j],arr[j+1]];
}
}
}
return arr;
}
//测试
let arr = [1,44,6,77,3,7,99,12]
console.log(bubbleSort(arr))// [1, 3, 6, 7, 12, 44, 77, 99]
(2)快速排序
概念
在数据集之中,找一个基准点,建立两个数组,分别存储左边和右边的数组,利用递归进行下次比较。
动图演示
步骤
先做边界处理
定义左右两侧的数组变量
取数组中间位置,通过这个位置找到中间值,同时在原数组上删除它
遍历数组,判断当前项的大小,放到对应的一边
遍历完后,对左右两侧数组进行递归,再拼接中间数。
手写代码
function quickSort(arr) {
if (!Array.isArray(arr)) return;
if (arr.length <= 1) return arr;
var left = [], right = [];
// 以中间位置为下标
var num = Math.floor(arr.length / 2);
// arr.splice(num,1)取下标为num的这个数(会改变原数组),结果是数组的形式,所以加[0]
var numValue = arr.splice(num, 1)[0];
for (var i = 0; i < arr.length; i++) {
if (arr[i] > numValue) {
right.push(arr[i]);
} else {
left.push(arr[i]);
}
}
return [...quickSort(left), numValue, ...quickSort(right)]
}
//测试
let arr = [1,44,6,77,3,7,99,12]
console.log(quickSort(arr))// [1, 3, 6, 7, 12, 44, 77, 99]
(1)原型链继承
简介
「核心:」
将父类的「实例」作为子类的「原型」
「优点:」
实现简单,容易理解
「缺点:」
包含引用类型值的原型属性会被所有实例共享,这会导致对一个实例的修改「会影响」另一个实例。
要想为子类新增属性和方法,必须在new Cat()这样的语句后执行,不能放在构造器中
如:在创建 Child 的实例时,不能向Cat传参。如果要加,只能在new Cat()以后加
由于这两个问题的存在,实践中很少单独使用原型链。
手写代码
//父类
function Cat () {
this.name = '橘猫';
}
Cat.prototype.getName = function () {
console.log(this.name);
}
//子类
function Child () {}
// 将父类的实例作为子类的原型
Child.prototype = new Cat();
//测试
var child1 = new Child();//我们要取出里面的getName,需要通过对象的属性的方式取出来(obj.fn),所以要new出一个对象
console.log(child1.getName()) // 橘猫
解惑
可能有人不解为什么要定义一个Child 函数:
因为继承是子类继承父类,我们的父类是Cat构造函数。那么还缺一个子类, 所以要创建一个子类,注意:我们说的父类,子类都是指构造函数
已经有了Child 构造函数,为什么还要new Child():
实际上这一步是为了测试子类上是不是有了父类的方法。Child本身只是一个构造函数,我们需要拿到它的实例,查看它的实例上是不是有这个方法,来判断Child是不是完成了继承。
(2)借用构造函数继承
简介
「核心:」
用.call()和.apply()将「父类构造函数」引入子类函数,等于是「复制」父类的实例属性给子类,「没用到原型」。
「优点:」
只继承了父类构造函数的属性,没有继承父类原型的属性
解决了原型链继承的2个缺点
可以继承多个构造函数属性(call多个)。
在子实例中可向父实例传参
「缺点:」
只能继承父类构造函数的属性。
无法实现构造函数的复用。(每次用都要重新调用)
每个新实例都有父类构造函数的副本,臃肿。
因此这种技术很少单独使用。
手写代码
//父类
function SuperType(name){
this.name=name;
}
//子类
function Child(name,age){
//将SuperType中的this指向了Child,从而将父类构造函数SuperType引入子类函数Child
//Child继承了SuperType,同时还传递了参数
SuperType.call(this,name);
//实例属性
this.age=age;
}
//测试
var Child1=new Child("mary",22);
console.log(Child1.name); //mary
console.log(Child1.age); //22
console.log(Child1 instanceof SuperType); //false
解惑
优点1怎么理解:
由于继承使用的是「call」,相当于在原来的SuperType函数里面,把「this」换成了「Child」,所以Child也有了name属性。注意:「我们全程没有用到原型」。
优点2怎么理解:
由于「没有用到原型」,所以属性不会被所有实例共享;可以在构造器中新增属性和传参:如SuperType.call(this,name),向父类传了「name」值(属性值为「动态」的)。而「原型继承」中,父类只能「写死」:this.name = '橘猫'。另外,Child中this.age=age也在构造器中直接添加了「属性」。
(3) 组合继承
简介
「核心:」
原型链继承和构造继承双剑合璧,结合了两种模式的优点,「传参」和「复用」
通过借用「构造函数」来实现对「实例属性的继承」
使用「原型链」实现对「原型方法的继承」
这样,既通过在原型上定义方法实现了函数的复用,又能够保证每个实例都有它自己的属性。
是 JavaScript 中最常用的继承模式。
「优点:」
可以继承父类原型上的属性,可以传参,可复用。
每个新实例引入的构造函数属性是私有的。
「缺点:」
调用了两次(call一次,new一次)父类构造函数(耗内存),子类的构造函数会代替原型上的那个父类构造函数。
手写代码
//父类
function SuperType(name){
this.name=name;
this.colors=["red", "blue", "green"];
}
SuperType.prototype.sayName=function(){
console.log(this.name);
};
//子类
function Child(name, age){
//继承【属性】 构造继承
SuperType.call(this,name);
this.age=age;
}
//继承【方法】 原型继承
Child.prototype=new SuperType();
Child.prototype.constructor=Child;
//测试
Child.prototype.sayAge=function(){
console.log(this.age);
};
var Child1=new Child("mary", 22);
Child1.colors.push("black");
console.log(Child1.colors); //red,blue,green,black
Child1.sayName(); //mary
Child1.sayAge(); //22
(4) 原型式继承
简介
「核心:」
就是 ES5 Object.create 的模拟实现,将「传入的对象」作为「创建的对象」的原型。
类似于复制一个对象,用函数来包装。
「优点:」
参考原型链继承
「缺点:」
参考原型链继承
手写代码
function createObj(o) {
function F(){}
F.prototype = o;
return new F();
}
//测试
var person = {
name: 'kevin',
friends: ['daisy', 'kelly']
}
var person1 = createObj(person);
var person2 = createObj(person);
person1.name = 'person1';
console.log(person2.name); // kevin
person1.firends.push('taylor');
console.log(person2.friends); // ["daisy", "kelly", "taylor"]
注意:修改person1.name的值,person2.name的值并未发生改变,并不是因为person1和person2有独立的 name 值,而是因为person1.name = 'person1',给person1添加了 name 值,并非修改了原型上的 name 值。
(5) 寄生式继承
简介
「核心:」
就是给原型式继承外面套了个壳子。
创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象。
「优点:」
没有创建自定义类型,因为只是套了个壳子返回对象(这个),这个函数顺理成章就成了创建的新对象。
「缺点:」
跟借用构造函数模式一样,每次创建对象都会创建一遍方法。
没用到原型,无法复用。
手写代码
//在原型式的外面套了个函数
function createObj (o) {
//这一步可以看做是原型式继承的简写
var clone = object.create(o);
//新增属性
clone.sayName = function () {
console.log('hi');
}
return clone;
}
(6) 寄生组合式继承
简介
「核心:」
实际上是3中继承方式的组合:寄生式,原型链继承,构造继承
寄生:在函数内返回对象然后调用
1、函数的原型等于另一个实例。
2、在函数中用apply或者call引入另一个构造函数,可传参
「优点:」
修复了组合继承的问题
「缺点:」
跟借用构造函数模式一样,每次创建对象都会创建一遍方法。
没用到原型,无法复用。
手写代码
//父类
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
//子类
function Child (name, age) {
//构造继承
Parent.call(this, name);
this.age = age;
}
// 关键的三步
// 套了一层函数,寄生式继承
var F = function () {};
F.prototype = Parent.prototype;
//原型链继承
Child.prototype = new F();
var child1 = new Child('kevin', '18');
console.log(child1);
本文使用 mdnice 排版