JavaScript基础知识总结(五)面向对象与原型,深浅拷贝,防抖节流
一: 面向对象与原型链
(1)面向对象与封装:
// 创建一个"手机"对象
const phone = {// 属性brand: '华为',color: '黑色',// 方法call(number) {console.log(`正在拨打${number}`)},takePhoto() {console.log('咔嚓拍照')}
}
封装:构造函数就体现了js面相对象的封装性 将公共属性和方法封装到构造函数中 缺点是浪费内存.
构造函数的作装属性和用:创建对象 封装属性和方法 实例化对象 调用构造函数 必须使用new关键字
问题:为什么构造和函数实例化出的对象可以继承调用封装在构造函数中的方法和属性?
**回答:**构造函数是通过原型来实现继承的,构造函数中都存在prototype属性 这个属性是一个对象 这个对象中存放了构造函数的原型对象 原型对象中存放了构造函数的公共属性和方法 当我们实例化对象时 会将原型对象中的属性和方法复制到实例对象中 这样就实现了继承,
如果把构造函数当做实例对象的父母 那么作为构造函数属性的原型就是父母的特征,实例对象自然会遗传父母的特征 故而对象可以访问原型上定义的方法
(2)原型
原型对象属性 :
原型本身并不是一个“构造函数”,而是与构造函数相关联的一个对象。每个构造函数都有一个 prototype 属性,这个属性指向一个对象,这个对象就是通过该构造函数创建的所有实例的原型
简单来说:如果把构造函数当做实例对象的父母 那么作为构造函数属性的原型就是父母的特征,实例对象自然会遗传父母的特征 故而对象可以访问原型上定义的方法
- 构造函数与原型:
- 每个构造函数(如
function Person() {})都有一个prototype属性。 - 这个
prototype是一个对象,可以在这个对象上定义方法和属性。 - 通过构造函数创建的实例对象会共享这个
prototype上的方法和属性。
- 每个构造函数(如
- 实例与原型:
- 当你通过
new Person()创建实例时,这些实例会通过__proto__属性链接到构造函数的prototype。 - 这意味着所有实例都可以访问
prototype上定义的方法。
- 当你通过
- 继承机制:
原型链是 JavaScript 实现继承的核心机制。当访问一个对象的属性或方法时,如果对象本身没有,JavaScript 会沿着原型链向上查找,直到找到为止。
function Person(name) {this.name = name;
}// 在原型上定义方法
Person.prototype.sayHello = function() {console.log("Hello, " + this.name);
};// 创建实例
let person1 = new Person("Alice");
let person2 = new Person("Bob");// 两个实例都可以访问 sayHello 方法
person1.sayHello(); // 输出: Hello, Alice
person2.sayHello(); // 输出: Hello, Bob
对象原型属性:
每一个实例化的对象都有一个对象原型属性(__proto__)属性,该属性指向构造函数的原型对象
function Person(name,age) {}
const p1=new Person()
console.log(p1);
console.log(p1.__proto__);// p1.__proto__指向Person.prototype 也就是构造函数的原型对象
console.log(p1.__proto__===Person.prototype);//true 原型对象和实例对象的__proto__指向的是同一个对象 也就是原型对象
constructor属性
- 指向构造函数:
constructor属性指向用来创建对象的构造函数本身。 - 默认存在:当你定义一个构造函数时,JavaScript 会自动为通过该构造函数创建的实例添加
constructor属性。 - 用途:
- 可以用于检查对象是由哪个构造函数创建的。
- 在原型链中,可以用来重新引用构造函数(如果原型被替换)。
function Person(name) {this.name = name;
}const person1 = new Person('Alice');console.log(person1.constructor === Person); // true
简单的总结: 构造函数中存在原型对象属性(protoType属性) 实例对象中存在对象原型(__proto__)指向构造函数的原型属性 和constructor属性指向了构造函数本身 这就是构成js原型链的基础结构
例子:使用某个对象作为模版来创建新对象:
function User(name,age) {this.name = name;this.age = age
}
let hd=new User('后盾人');
function createObject(obj,...args){const constructor=Object.getPrototypeOf(obj).constructor;return new constructor(...args);
}//通过这个函数 我们可以根据已有对象的构造函数来创建新的对象
let xj=createObject(hd,'xj','18');
console.log(xj);
基于原型的继承模式
js中用过原型链来实现对象属性和方法的共享
- 基本原理
- 每个 JavaScript 对象都有一个内部属性
[[Prototype]](可通过__proto__访问),指向其原型对象 - 当访问对象属性时,若对象本身没有该属性,会沿着原型链向上查找
- 实现步骤:
- 定义父类构造函数:包含公共属性和方法
- 定义子类构造函数:包含特有属性
- 设置原型继承:将子类的
prototype指向父类实例 - 修正 constructor:手动将子类原型的
constructor指回子类本身 - 添加子类特有方法:在继承关系建立后扩展子类功能
// 父类
function Person() {this.eyes = 2this.head = 1
}// 子类
function Woman() {
}// 实现继承:子类原型指向父类实例
Woman.prototype = new Person()
// 修正构造函数指向
Woman.prototype.constructor = Woman
// 添加子类特有方法
Woman.prototype.baby = function() {console.log('宝贝')
}
特点
- 动态性:原型修改会实时影响所有实例
- 共享性:原型上的属性和方法被所有实例共享
- 链式查找:通过
__proto__形成原型链,实现属性和方法的逐级查找
检查原型链的方法
1.instanceof用来检查前者是否在后者的原型链上 后者必须是构造函数 不是原型对象
function Person(){
//1
}
const ldh = new Person()
console.log(ldh instanceof Person)
console.log(ldh instanceof Object)
2.isPrototypeOf()检查前者是不是当前对象的对象原型
console.log(Person.prototype.isPrototypeOf(ldh)) // true
console.log(Object.prototype.isPrototypeOf(ldh)) // true
3.in语法检测一个属性是否在这个对象上或者在这个对象的原型链上.
//hasOwnProperty 值检测一个属性是否在这个对象上,不会检测原型链上的内容 */
let a = { name: "李四" };
let b = { age: "19" };// in 语法检测属性是否在对象的原型链上
console.log("age" in a); // false
4.hasOwnProperty 检测一个属性是否在这个对象上 不会检测原型链上的内容
let a = { name: "李四" };
let b = { age: "19" };
// hasOwnProperty只会检测当前元素,不会检测原型上面的东西
console.log(a.hasOwnProperty("age")); // false
for (const key in a) {// 使用 for in 时加上Object.hasOwnProperty(key) 可以只遍历对象本身的元素if (a.hasOwnProperty(key)) {console.log(key); // name}// 没有hasOwnProperty会遍历对象本身的元素以及原型上的元素console.log("没加" + key); // name age
}
指定对象的原型
我们可以使用Object.setPrototypeOf(obj1,obj2) 把obj1的父亲原型指定为obj2
// 定义两个对象,现需要吧obj的父级原型设置为 objparent
let obj = {name: "lisi",};let objparent = {name: "张三",show() {console.log(this.name);},};
//指定父级原型 类似于继承
Object.setPrototypeOf(obj, objparent);//设置obj的父级原型为objparent
obj.show();//lisi // 读取对象原型 Object.getPrototypeOf ()
console.log(Object.getPrototypeOf(obj));//{ name: '张三', show: [Function: show] }
在原型上追加新的方法
当出现下面的情况的时候,我们建议将方法迁移到原型链上,避免占用过多内存
function Person(name){this.name = name;// this.showName = function(){// console.log(this.name)// }
}
let lisi=new Person('李四')
let zhangsan=new Person('张三')
console.log(lisi);
console.log(zhangsan);
//展开会发现 这个两个方法都是一样的 但是占用了两分内存 所以我们可以利用原型链 将方法挂在原型上
在构造函数的prototype属性上挂载方法
Person.prototype.showName=function(){console.log(this.name)
}//这是在原有原型上追加新方法
lisi.showName()
zhangsan.showName()
一次性在原型对象上挂载多个方法
//如果一次有要追加多个方法 可以利用对象的方式 一次性追加
Person.prototype={constructor:Person,showName:function(){console.log(this.name)},showAge:function(){console.log(this.age)}
}
需要注意: 不要在顶层原型,例如Object Number等等构造函数的原型上去挂载新方法,这种做法是相当危险的!
__proto__用法示例
1.利用__proto__为对象设置原型
// 定义一个初始对象
let User = {show() {console.log(this.name);},
};
// 利用__proto__设置原型
let newuse = {name: "王五",};
// 如果__proto__后面跟上了等于的值,表示设置原型
newuse.__proto__ = User;
// 如果__proto__后面没有值表示读取原型
console.log(newuse.__proto__);
newuse.show();
这种方法虽然可以设置原型但是还是不建议这么去使用__proto__设置对象的原型,还是建议使用上文中的setProtoType去代替
2.__proto__属性访问器
当我们使用__proto__去设置对象的原型的时候,proto必须赋值为一个对象,如果是其他数据类型就不会被成功设置原型
// 设置一个对象的__proto__时必须使其等于一个对象,否则不会被赋值
let hd={name:"hd"};
hd.__proto__={show(){console.log(this.name);}
}
hd.show();
hd.__proto__=1;
hd.show()//发现正常输出了hd
这说明__proto__不仅仅是一个具体的属性,也是一个属性访问器.原理很简单
let newuse = {active: {name: "lisi",},// 使用构造器声明protoget proto() {return this.active;},set proto(obj) {// 给__protp__赋值的时候判断值的类型if (obj instanceof Object) {this.active = obj;}return true;},};
原型的继承
方法
继承不是单纯的改变构造函数,这样会造成原型链的污染,如果想让构造函数B继承构造函数A 需要让B的prototype属性通过Object.create继承User.prototype 这样才能避免原型链的污染. 也可以使用(Object.setPrototypeOf()方法)
注意 继承之后 继承者本身的方法就不能够访问了 需要重新定义这些方法 在继承之后
// 正确的继承方式
function User(){}
User.prototype.show=function(){console.log("show");
}
function Admin(){}
// 正确继承:让 Admin.prototype 通过 Object.create 继承 User.prototype,避免原型链污染
// Admin.prototype.__proto__ = User.prototype;
Admin.prototype = Object.create(User.prototype);
Admin.prototype.constructor = Admin;
Admin.prototype.role = ()=>{console.log("role");
};
继承图解

继承后需要重新指定constructor的指向
先来看示例:
// 声明User构造函数
function User() {}
// 给User的原型添加show方法
User.prototype.show = function() {console.log("User show");
};// 声明Info构造函数
function Info() {}
// 使用Object.create()方法继承User的原型
Info.prototype = Object.create(User.prototype);// 再打印User的结构
console.log(User.prototype); // {show: ƒ, constructor: ƒ}
// 这个时候打印Info的结构
console.log(Info.prototype); // {}//所以如果改变了一个对象的原型,要重新填写constructor的指向
Info.prototype.constructor=Info;
console.log(Info.prototype); // {constructor: ƒ}
上面代码中 Info 的 prototype 原型中的 constructor 没有了,就是因为使用 Object.create 过程中吧 Info 的 constructor 弄丢了,此时去实例化两个构造函数,分别打印出实例化变量的 constructor,发现打印的结果都是
方法重写与父级属性访问
我们可以在子构造函数中重写父级的方法 但是不会修改父级本身的方法 父级可以正常调用原来的方法(多态)
// 定义基础函数
function User() {}
User.prototype.show = function() {console.log("User-show");
};
User.prototype.site = function() {return "User-site";
};// 定义子类
function Admin(){}
Admin.prototype=Object.create(User.prototype);
Admin.prototype.constructor=Admin;
//方法重写(类似于@override)
Admin.prototype.show=function(){console.log(User.prototype.site()+"Admin-show");//调用父级的方法(类似于super)
}
let admin = new Admin();
admin.show();
面向对象->多态
function User() {}
User.prototype.show = function() { console.log('我是普通用户'); }function Admin() {}
Admin.prototype = Object.create(User.prototype);
Admin.prototype.constructor = Admin;
// Admin 重写 show 方法
Admin.prototype.show = function() { console.log('我是管理员'); }//其实就是一个方法在不同状态(实例对象的原型不同)下,响应不同的结果
let user=new User();
let admin=new Admin();
user.show();//我是普通用户
admin.show();//我是管理员
使用父级的初始属性
就是js版的super语法,原理是使用apply方法 改变this的指向 从User的实例化对象改变为Admin的实例化对象 就套用了父级的属性
function User(name,age){this.name=name;this.age=age;this.show=function(){console.log(this.name+"-"+this.age);}
}
function Admin(...args){// 引用构造函数User,通过apply改变User的this指向为当前Admin实例// 并将接收到的参数传递给User,实现初始化属性在User当中完成User.apply(this, [...args]);// 或者使用 arguments 对象: User.apply(this, arguments);
}
Admin.prototype=Object.create(User.prototype);
Admin.prototype.constructor=Admin;let admin=new Admin("admin",18);
admin.show();
(3)原型链
原型链原理
想理解原型链我们需要搞清楚什么是构造函数 什么是prototype属性 什么是__proto__属性 我们不妨来设定几个前提
- 1.前提 每次声明对象都是 构造函数通过new 来实例化创建的
- 2.前提 每个函数天生自带一个
prototype属性 这个属性是一个对象 - 3.前提 每个对象天生自带一个
__proto__属性 这个属性是一个对象,prototype是对象也有__proto__属性 - 4.前提
__proto__属性指向构造函数的prototype属性 构造函数的方法都可以传递到prototype属性上 然后通过__proto__属性传递到实例对象上 实例对象就可以调用构造函数的方法了 也就是__proto__其实是一根链子 用来链接两个proytotype属性
那么就有问题了 构造函数的prototype是哪来的呢?prototype就是一个普通对象 那我们刨根问底 对象怎么来的? 对象是Object这个最原始的构造函数new出来的 既然都是构造函数了 那根据 前提2 Object这个构造函数也有一个prototype属性 这个属性就是一个普通对象 但是他已经是最原始的了 所以他的prototype通过__proto__这跟链子链接的上一个节点就是null 这就是原型链的尽头
所以我们要明白一点 原型链是通过prototype来链接的,通过这个链子我们可以传递链子上游节点的各种方法 而__proto__是实例对象和构造函数传递方法的桥梁,所以__proto__主要是用来传递方法的是和构造函数的prototype的关系最紧密的 通过__proto__和prototype的链接 实例对象就可以调用构造函数的方法了
原型链示意图
通过上面的关系可以画出下面的原型链

二: 浅拷贝与深拷贝
使用深浅拷贝的原因:
1.为什么使用深浅拷贝
当我们创建一个对象a,然后创建一个新变量b,将对象a赋值给变量b,当我们改变原对象a的属性的时候会发现新变量b的属性值也会跟着改变。这是因为我们将对象a赋值给新变量b的时候实质上是将对象a的引用赋值给了新变量b(这个引用a指向的就是对象a存储的内存地址),所以本质上直接赋值不是创造了一个新的内存地址来存储新对象,而是二者共用了一个内存地址 只是名字不一样。而深浅拷贝就是从该内存地址上拷贝一个对象然后赋值给新变量,拷贝的新对象与老对象是隔离的 没有任何关系。来看图解:
2.深拷贝与浅拷贝的区别
当我们拷贝的对象中没有对象属性或是数组属性的情况下,使用浅拷贝即可。但是如果拷贝的对象中存在一个新的引用类型的数据,在只拷贝外层的情况在,内部的引用还是没有被拷贝,就会造成新老对象在操作引用类型的属性时会影响彼此,因为本质上引用类型属性还是调用着同一个引用。所以就需要深拷贝,也就浅拷贝嵌套浅拷贝。
简单记忆:
浅拷贝 = 表面复制(外层独立,内层共享)
深拷贝 = 彻底复制(全部独立)
1.浅拷贝
let user = {name: "lisi",age: 18,};
浅拷贝有三种方案:
1.使用结构语法
// 方式一,解构语法
let newuser1 = { ...user };
newuser1.name = "wangwu";
console.log(newuser1, "newuser1"); // {name: "wangwu", age: 18}
console.log(user, "user"); // {name: "lisi", age: 18}
2.使用Object对象的assign属性实现对象合并
// 方式二,巧用对象合并 assign
let newuser2 = Object.assign({}, user);
newuser2.name = "zhaoliu";
console.log(newuser2); // {name: "zhaoliu", age: 18}
console.log(user); // {name: "lisi", age: 18}
3.使用for key in循环
// 方式三,for in 循环赋值
let newUser={}
for(let key in user){
newUser[key]=user[key]
}
console.log(newUser);
2.深拷贝
深拷贝有两种方法
-
递归函数法
//1.深拷贝函数递归实现 function deepCopy1(obj){let res={};for(let key in obj){if(typeof obj[key]==='Object'){res[key]=deepCopy1(obj[key])} else if(typeof obj[key]==='Array'){res[key]=deepCopy1(obj[key])}//这里的[key]在对象中是属性名 数组中是下标res[key]=obj[key]}return res; } -
JOSN暴力破解
//2.暴力拷贝法 function deelClone(obj) {return JSON.parse(JSON.stringify(obj)); }
三 : 异常处理与防抖节流
(1)异常处理
异常处理就是传统的try-catch 和 thorw抛出异常 ,作用就是提升代码的健壮性,在调试的时候更方便
1.抛出异常
function sum(x,y){if(!x||!y){throw new Error('用户没有传递参数')//当throw执行后程序终端}return x+y
}
2.try-catch捕获异常
try函数执行区域catch拦截代码 捕获错误finally无论对错最后都会执行
try{//可能发生错误的代码const p = document.querySelector('.p')p.style.color = 'red'}catch(err){//拦截代码,出现错误不再执行,但是不会中断代码执行console.error(err.message)return}finally{//不管程序对与错 一定会执行的代码 并且优先执行alert('代码出错了吗')}
}
(2)防抖
1.什么防抖
防抖,是在触发时间n秒内函数只能执行一次,如果在n秒内又触发了事件,则会重新计算函数执行时间。
案例类似于王者里面的回城按钮,点击回城被打断之后就会重新读条回城。
2.实现防抖函数
1.手写防抖处理
- 声明定时器
- 每次鼠标移动的时候都要先判断是否有定时器,如果有下去弄清楚以前的定时器
- 如果没有定时器,则开启定时器,存入到定时器变量里面
- 定时器里面写函数调用
function debounce(fn, delay) {let timer = null;// 返回一个匿名函数return function(...args) {// 保存当前this指向const context = this;// 如果已有定时器,清除它if (timer) {clearTimeout(timer);}// 设置新的定时器timer = setTimeout(function() {fn.apply(context, args);}, delay);};
}box.addEventListener('mousemove',debounce(boxMove,250))
**2.lodash库 实现防抖效果 **
导入lodash库
box.addEventListener('mousemove',_.debounce(boxMove,500))
(3)节流
1.节流:
就是确保函数在指定的时间间隔内最多只执行一次。即使函数被多次调用,也只会按照设定的频率执行。就是类似于王者里面触发一次技能之后,会触发冷却机制,在冷却中不能重复触发技能。
- 当事件触发时,如果距离上次执行的时间间隔已经超过了设定的阈值,则立即执行函数。
- 如果未超过时间间隔,则忽略此次调用,直到时间满足条件为止。
2.实现节流函数:
- 声明一个定时器变量
- 当鼠标滑动判断是否存在定时器。如果有就不创建新的定时器
- 如果没有定时器就开启定时器 定时器里面
function throttle(func, delay) {let lastExecTime = 0;return function (...args) {const now = Date.now();if (now - lastExecTime >= delay) {lastExecTime = now;func.apply(this, args);}};
}const throttledLog = throttle(() => {console.log("执行了!");
}, 1000);// 模拟频繁调用
throttledLog(); // 执行了!
throttledLog(); // 忽略
throttledLog(); // 忽略
setTimeout(throttledLog, 500); // 忽略
setTimeout(throttledLog, 1500); // 执行了!
