一. 认识继承
- 如果 实例化对象(s1) 内部 具有一个 name 属性
- 并且 s1 还可以使用 init 方法
- 那么 我们就可以说 Stu 这个构造函数 继承了 Person
- Stu 是 Person 的子类
- Person 是 Stu 的父类
// 构造函数1
function Person(name) {
this.name = name
}
Person.prototype.init = function () {
console.log('我是 Person 原型上的方法')
}
// 构造函数2
function Stu(age) {
this.age = age
}
Stu.prototype.sayHi = function () {
console.log('你好')
}
const s1 = new Stu(18)
console.log(s1)
console.log(s1.__proto__)
二. 原型继承
什么是原型继承 ?
- 利用自定义原型的继承方式, 实现继承
- 子类,prototype = new 父类()
缺点 :
- 原本原型上的方法不能使用, (因为原型被改变了)
- 继承到的属性并不是在自己身上, 而是在原型上, (不过并不影响使用)
// 构造函数 1 :
function Person(name) {
this.name = name
}
Person.prototype.init = function () {
console.log('我是 Person 原型上的方法');
}
// 构造函数 2 :
function Stu(age) {
this.age = age
}
Stu.prototype.sayhi = function () {
console.log('你好');
}
Stu.prototype = {
a: 1,
b: 2
} // 修改对象的原型对象
Stu.prototype = new Person('张三') // '张三'
console.log('stu原型对象',Stu.prototype); // {a:1,b:2}
const s1 = new Stu(18)
console.log('实例对象',s1); // age :18 name : '张三'
console.log(s1.name); // 张三
s1.init() // 我是 Person 原型上的方法
s1.sayhi() // 报错
/*
访问 对象 s1 的name 属性
1. 先在对象本身查找, 找到就用, 但是自身就只有一个 age属性 所以没找到
2. 所以会去__proto__ 对象中查找, 也就是自己构造函数的原型对象
2-1 : 但是现在 自己构造函数的原型对象 已经被我们修改为 Person 这个构造函数的 实例化对象
2-2 : 这个实例化对象对象上是具有 name 属性的, 并且还有一个 init方法
3. 所以在 实例化对象中找到了name 属性, 值为 张三
*/
构造函数继承
核心 :
- 把 父类函数 当做 子类函数调用时, 并利用 this 修改了这个函数内部的 this 指向
- 如果不修改的话, 函数的 this 指向了 其他的 对象
优点 :
- 把 父类 属性全都继承在 自己身上
缺点 :
- 只能继承父类的 属性, 不能继承弗雷德方法
- 每次调用 Stu 时, Stu 内部还会自动调用一次 Person 函数
// 构造函数 1 :
function Person(name) {
this.name = name
}
Person.prototype.init = function () {
console.log('我是 Person 原型上的方法');
}
// 构造函数 2 :
function Stu(age) {
// 1. 自动创建出来一个对象 (这个函数内部的this 就指向了这个对象, 所以你可以通过 this 向这个对象上添加属性)
// 2. 手动向对象上添加属性
this.age = age
// 将 Person 内部的this 修改为了 第一步被自动创建出来的对象,并传递一个参数 name 给 Person 函数使用
Person.call(this,'这是一个名字')
// 3.自动返回这个对象
}
Stu.prototype.sayhi = function () {
console.log('你好');
}
const s1 = new Stu(18,'张三')
console.log(s1);
s1.init() // 没有继承到, 无法使用(构造函数继承 继承不到方法)
/*
分析 : 通过new 关键字 调用 Stu 这个构造函数, 得到一个实例化对象,存储在了 常量 s1 内部
调用 Stu 函数时, 发生的 事情 :
1. 给对象上添加一个 age 属性, 并将形参的值 赋值给他
2. 调用 Person 并通过 call 方法 改变了 this 就相当于Stu 构造函数内部 被自动创建出来的 对象
2-1 : 所以现在 Person 函数内部的 this 就相当于是 Stu 构造函数内部被自动创建出来的对象
调用 Person 函数时, 发生的事情 :
this.name = 形参
3. 代码执行完毕之后, new Stu 时内部被自动创建出来的对象被添加了两个属性
3-1 : 是在 Stu 函数内部添加 age属性
3-2 是在 Person 函数内部添加的 name 属性
*/
组合继承
核心 : 把原型继承 与 借用构造函数继承结合起来使用
优点 : 实例化对象上 具有继承到的属性, 并且能够继承到 父类原型 上的方法
缺点 : 实例化对象上,都有父类的属性( 锁了一套属性, 但是并不影响使用 )
// 构造函数 1 :
function Person(name) {
this.name = name
}
Person.prototype.init = function () {
console.log('我是 Person 原型上的方法');
}
// 构造函数 2 :
function Stu(age) {
this.age = age
// 1. 借用构造函数继承, 得到弗雷德属性(放在了对象上, 并且没有继承类型院系能上的方法)
Person.call(this, name)
}
// 利用原型继承, 得到父类的属性(原型上)与方法
Stu.prototype = new Person('这个字符串没有意义')
Stu.prototype.sayhi = function () {
console.log('你好');
}
// 创建实例化对象
const s1 = new Stu(18, '张三')
console.log('Stu的实例化对象', s1);
console.log('Stu的原型对象',Stu.prototype);
console.log(s1.name);
s1.init()
/*
访问 s1 对象 name 属性:
去对象内部自身查找, 找到了, 直接使用, 并且停止查找
*/
拷贝继承
- for... in遍历, 可以遍历原型对象上的方法
// 构造函数 1 :
function Person(name) {
this.name = name
}
Person.prototype.init = function () {
console.log('我是父类方法');
}
// const p1 = new Person('张三') // name :张三
// for(let key in p1){
// console.log(key,p1[key]); // init
// }
// 构造函数 2 :
function Stu(age) {
this.age = age
const p1 = new Person('张三')
for (let key in p1) {
Stu.prototype[key] = p1[key]
}
}
Stu.prototype.sayhi = function () {
console.log('我是子类方法');
}
// 3. 创建实例化对象
const s1 = new Stu(22, '传名字')
console.log('Stu实例化对象', s1);
console.log('Stu原型对象', s1.__proto__);
ES6类继承
语法 :
class 子类名 extends 父类名{
constructor(age){
super('父类需要的参数, 都写在这里')
his.age = age
}
}
语法要求 :
- 书写 子类时, class子类名 extends 父类名{...}
- 书写 consturctor 时, 内部需要 书写 super('父类需要的参数')
注意 :
- extends 和 super 必须同时出现 才能完成继承
- super 必须出现在 consturctor 内第一行
- 额外扩展 : ES6 类也能继承 ES5 的构造函数
- 验证方法 : 将 Person 更改为 ES5 的构造函数
// 父类
class Person {
constructor(name) {
this.name = name
}
init() {
console.log('我是父类方法');
}
}
// 子类
class Stu extends Person {
constructor(age) {
super('父类需要的参数, 都写在这里')
this.age = age
}
sayhi() {
console.log('我是子类方法');
}
}
const s1 = new Stu(18)
console.log(s1); // age : 18 name : '父类需要的参数, 都写在这里'
console.log(s1.name); // '父类需要的参数, 都写在这里'
s1.init() // '我是父类方法'
s1.sayhi() // '我是子类方法'
深浅拷贝
含义 :
- 将一个 引用数据类型, 拷贝到另外一个变量中, 但是根据拷贝的方法不同, 展示的效果也有差异
浅拷贝 :
- 将一份数据拷贝到另外一个变量中, 修改第一层数据时不会被影响, 但是修改第二层数据时会被影响
// 浅拷贝 :
let obj = {
name: '张三',
age: 18,
init: {
width: 100,
height: 200
}
}
let newobj = {}
for(let key in obj){
// console.log(key); // name age init
newobj[key] = obj[key]
}
newobj.age = 99
newobj.init.width = 999
console.log('newobj' ,newobj); // age : 99 init : width :999
console.log('obj', obj); // age : 18 init : width :999
深拷贝 :
- 将一份数据拷贝到另外一个变量中, 不管修改哪一层数据, 两个对象之间都不会互相影响
// 深拷贝 :
let obj = {
name: '张三',
age: 18,
init: {
width: 100,
height: 200
}
}
let newobj = {}
// 针对面试 :
function deepClone(target, origin) {
/*
target : 目标对象
origin : 原始对象
需求 : 将原始对象 origin 内部的所有属性, 拷贝到目标对象 target 中
*/
// 1. 通过 for...in 遍历对象, 拿到对象对象所有的key 与对应的 value
for (let key in origin) {
// 2. 判断 遍历到的属性的属性值 是什么类型
if (Object.prototype.toString.call(origin[key]) === 'object Object') {
// 表明当前这个 key 的值为一个对象
target[key] = {}
// deepClone('目标对象','原始对象')
deepClone(target[key], orinig[key])
} else if (Object.prototype.toString.call(origin[key]) === 'object Array') {
// 表明当前这个 key 的值为一个数组
target[key] = []
deepClone(target[key], orinig[key])
} else {
// 表明当前这个key 的值 一定不是对象 或者 数组
target[key] = origin[key]
}
}
}
deepClone(newobj, obj)
console.log(deepClone);
newobj.age = 99
newobj.init.width = 999
console.log('newobj',newobj);
console.log('obj',obj);
// 针对工作 :
newobj = JSON.parse(JSON.stringify(obj)) // 把obj装成json 格式 字符串
赋值 :
赋值
let obj = {
name : '张三',
age : 18,
init : {
width : 100,
height : 200
}
}
let obj2 = obj
obj2.age = 20
console.log(obj); // 20 因为直接赋值,对象存的是地址 , 所以修改obj2 会影响 obj, 所以它内部的 name 值也就改成了 20
函数的定义与调用
定义 :
- 在堆内存中 开启一个空间
- 将函数的函数体保存到内存中
- 将堆内存的地址保存在变量名(函数名), 最后将这个变量名存储在 栈内存中
调用 :
- 根据变量名(函数名) 中的地址, 找到对应的函数
- 然后再调用 栈中 开一个新的空间(函数的执行空间)
- 在执行空间中, 对函数形参进行赋值
- 在执行空间中, 进行变量得预解析
- 在执行空间中, 执行函数的代码, 销毁当前函数的执行空间
永不销毁的执行空间
- 正常写一个函数
- 在这个函数内, 向外否会一个 引用数据类型( 复杂数据类型)
- 当满足上述条件时, 这个函数的执行空间将不会被销毁
function fn() {
const obj = {
name: 'fn函数的name',
age: '不知道'
}
return obj
}
// 变量名 newobj 内部保存着fn 函数中声明的一个对象 obj 地址,所以fn 函数就不会被销毁掉, 如果销毁了, 那么对象也无法访问了
const newobj = fn()
// newobj.name = '张三' // 张三
console.log(newobj);
// 将 newobj 的值修改后就与函数内部的 对象切断了练习, 那么这个函数的执行空间就会被销毁
// newobj = null
闭包
什么是闭包 ?
- 需要直接 或者 间接 返回一个函数
- 内部函数需要访问外部函数的局部变量
好处 :
- 延长变量得生命周期
弊端 :
执行空间不会被销毁, 如果大量使用 会造成 内存泄漏
function outter() {
let a = 100
let obj = {
name: 'outter函数',
age: '99'
}
function inner() {
// console.log(a); // 100
// console.log(obj);
return a
}
//1. 直接返回一个函数
return inner
}
const newfn = outter()
// console.log(newfn); // 得到一个函数(inner)
//newfn() // 没有输出 控制台什么也没有
console.log(newfn());
// let num = newfn()
// console.log(num);