S05-03 JS高级-对象增强、原型、继承、手写
[TOC]
对象增强▸
Object.defineProperty
对属性操作的控制
Object.defineProperty(obj, prop, descriptor):在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
- obj:``,要定义属性的对象
- prop:``,要定义或修改的属性名或 Symbol
- descriptor:``,要定义或修改的属性描述符
- 返回值
- :``,被传递给函数的对象
在前面我们的属性都是直接定义在对象内部,或者直接添加到对象内部的:
- 但是这样来做的时候我们就不能对这个属性进行一些限制:比如这个属性是否是可以通过 delete 删除的?这个属性是否在 for-in 遍历的时候被遍历出来呢?
如果我们想要对一个属性进行比较精准的操作控制,那么我们就可以使用属性描述符。
通过属性描述符可以精准的添加或修改对象的属性;
属性描述符需要使用 Object.defineProperty() 来对属性进行添加或者修改;
属性描述符的类型有两种:
数据属性(Data Properties)描述符(Descriptor);
存取属性(Accessor 访问器 Properties)描述符(Descriptor);
数据属性描述符
数据数据描述符有如下四个特性:
Configurable
[[Configurable]]:boolean
,表示属性是否可以通过 delete删除属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符;
当我们直接在一个对象上定义某个属性时,这个属性的[[Configurable]]默认为 true;
当我们通过属性描述符定义一个属性时,这个属性的[[Configurable]]默认为 false;
用法:
▸ 不可删除
▸ 不可重新配置
▸ 通过属性描述符定义一个属性时,这个属性的[[Configurable]]默认为 false
Enumerable
[[Enumerable]]:表示属性是否可以通过 for-in
或者 Object.keys()
遍历该属性;
当我们直接在一个对象上定义某个属性时,这个属性的[[Enumerable]]为 true;
当我们通过属性描述符定义一个属性时,这个属性的[[Enumerable]]默认为 false;
Writable
[[Writable]]:表示是否可以修改属性的值;
当我们直接在一个对象上定义某个属性时,这个属性的[[Writable]]为 true;
当我们通过属性描述符定义一个属性时,这个属性的[[Writable]]默认为 false;
value
[[value]]:属性的 value 值,读取属性时会返回该值,修改属性时,会对其进行修改;
- 默认情况下这个值是 undefined;
测试代码
存取属性描述符
存取属性描述符
存取属性描述符有如下四个特性:
[[Configurable]]:表示属性是否可以通过 delete 删除属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符;
和数据属性描述符是一致的;
当我们直接在一个对象上定义某个属性时,这个属性的[[Configurable]]为 true;
当我们通过属性描述符定义一个属性时,这个属性的[[Configurable]]默认为 false;
[[Enumerable]]:表示属性是否可以通过 for-in 或者 Object.keys()返回该属性;
和数据属性描述符是一致的;
当我们直接在一个对象上定义某个属性时,这个属性的[[Enumerable]]为 true;
当我们通过属性描述符定义一个属性时,这个属性的[[Enumerable]]默认为 false;
[[get]]:获取属性时会执行的函数。默认为 undefined
[[set]]:设置属性时会执行的函数。默认为 undefined
存储属性描述符测试代码
Object.defineProperties
Object.defineProperties()方法直接在一个对象上定义多个新的属性或修改现有属性,并且返回该对象。
对象其他方法
获取对象的属性描述符:
getOwnPropertyDescriptor
getOwnPropertyDescriptors
禁止对象扩展新属性:
- preventExtensions:给一个对象添加新的属性会失败(在严格模式下会报错);
密封对象,不允许配置和删除属性:
seal
实际是调用 preventExtensions
并且将现有属性的 configurable:false
冻结对象,不允许修改现有属性:
freeze
实际上是调用 seal
并且将现有属性的 writable: false
示例:
1、getOwnPropertyDescriptor
2、getOwnPropertyDescriptors
3、preventExtensions
4、seal:preventExtensions
+ configurable: false
5、freeze:seal
+ writable: false
原型
对象和函数的原型
对象的原型
JavaScript 当中每个对象都有一个特殊的内置属性*[[Prototype]]*,这个特殊的对象可以指向另外一个对象。
那么这个对象有什么作用呢?
当我们通过引用对象的属性 key 来获取一个 value 时,它会触发*[[Get]]*的操作;
这个操作会首先检查该对象是否有对应的属性,如果有的话就使用它;
如果对象中没有该属性,那么会访问对象[[prototype]]内置属性指向的对象上的属性;
那么如果通过字面量直接创建一个对象,这个对象也会有这样的属性吗?如果有,应该如何获取这个属性呢?
- 答案是有的,只要是对象都会有这样的一个内置属性;
获取对象原型的方式有两种:
方式一:通过对象的 __proto__ 属性可以获取到(但是这个是早期浏览器自己添加的,存在一定的兼容性问题);
方式二:通过 Object.getPrototypeOf() 方法可以获取到;
示例:
1、获取对象的原型
2、对象属性查找顺序
函数的原型
那么我们知道上面的东西对于我们的构造函数创建对象来说有什么作用呢?
- 它的意义是非常重大的,接下来我们继续来探讨;
1、将函数看做一个普通的对象,它具备*__proto__
*(隐式原型)属性
作用:查找 key 对应的 value 时,会找到原型身上
2、将函数看做一个函数时,它具备*prototype
*(显式原型)属性(注意:不是__proto__
或[[Prototype]]
)
作用:当通过 new 创建对象实例时,对象实例的隐式原型会指向这个构造函数的显式原型:foo.__proto__ = Foo.prototype
注意: 箭头函数没有原型prototype
你可能会问题,老师是不是因为函数是一个对象,所以它有 prototype 的属性呢?
不是的,因为它是一个函数,才有了这个特殊的属性;
而不是它是一个对象,所以有这个特殊的属性;
new、constructor
new 操作原型赋值
我们前面讲过 new 关键字的步骤如下:
1、在内存中创建一个新的对象(空对象);
2、这个对象内部的*[[prototype]]属性会被赋值为该构造函数的prototype 属性*;
那么也就意味着我们通过 Person 构造函数创建出来的所有对象的[[prototype]]属性都指向 Person.prototype:
constructor 属性
事实上原型对象上面是有一个属性的:constructor
- 默认情况下原型上都会添加一个属性叫做 constructor,这个 constructor 指向当前的函数对象;
实例方法-构造函数和原型结合
我们在上一个构造函数的方式创建对象时,有一个弊端:会创建出重复的函数,比如 running、eating 这些函数
那么有没有办法让所有的对象去共享这些函数呢?
可以,将这些函数放到 Person.prototype 的对象上即可;
分析:
- 1、p1 的隐式原型是 Person.prototype 对象
- 2、p1.running 查找规则:
- 先在自己身上查找,没有找到
- 再去原型上查找,找到了
作用:
当多个对象拥有共同的值时,可以将该值放到构造函数的对象的显式原型上;由构造函数创建出来的所有对象,都会共享这些方法
内存图-创建实例对象
function Person(name, age) {
this.name = name;
this.age = age;
}
var p1 = new Person("mr", 18);
var p2 = new Person("tom", 20);
内存图-添加原型属性
function Person(name, age) {
this.name = name
this.age = age
}
var p1 = new Person("mr", 18)
var p2 = new Person("tom", 20)
// 添加原型属性
+ Person.prototype.message = "中国"
+ p1.__proto__.info = "中国很美丽"
内存图-添加原型方法
function Person(name, age) {
this.name = name
this.age = age
}
var p1 = new Person("mr", 18)
var p2 = new Person("tom", 20)
Person.prototype.message = "中国"
p1.__proto__.info = "中国很美丽"
// 添加原型方法
+ Person.prototype.running = function() {}
内存图-新增实例属性
function Person(name, age) {
this.name = name
this.age = age
}
var p1 = new Person("mr", 18)
var p2 = new Person("tom", 20)
Person.prototype.message = "中国"
p1.__proto__.info = "中国很美丽"
// 修改p1.message,p2.message是否改变
+ p1.message = "美国"
+ console.log(p2.message) // => "中国"
重写原型对象
在原有的原型对象上添加新的属性
如果我们需要在原型上添加过多的属性,通常我们会重写整个原型对象
前面我们说过, 每创建一个函数, 就会同时创建它的 prototype 对象, 这个对象也会自动获取 constructor 属性;
而我们这里相当于给 prototype 重新赋值了一个对象, 那么这个新对象的 constructor 属性, 会指向 Object 构造函数, 而不是
Person 构造函数
手动添加 constructor
如果希望 constructor 指向 Person,那么可以手动添加 constructor:
上面的方式虽然可以, 但是也会造成 constructor 的[[Enumerable]]特性被设置了 true
默认情况下, 原生的 constructor 属性是不可枚举的
如果希望解决这个问题, 就可以使用我们前面介绍的 Object.defineProperty()函数了
1、手动添加 constructor,指向 Person
2、通过 defineProperty 设置 constructor 属性为不可枚举
构造函数的类方法
添加在构造函数本身的方法,叫类方法
类方法可以在没有实例对象的情况下,调用函数
继承
继承
面向对象有三大特性:封装、继承、多态
封装:我们前面将属性和方法封装到一个类中,可以称之为封装的过程;
继承:继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态前提(纯面向对象中);
多态:不同的对象在执行时表现出不同的形态;
那么这里我们核心讲继承。
那么继承是做什么呢?
继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可;
在很多编程语言中,继承是多态的前提;
那么 JavaScript 当中如何实现继承呢?
不着急,我们先来看一下 JavaScript 原型链的机制;
再利用原型链的机制实现一下继承;
示例:
Student 类
Teacher 类
将共同的属性和方法抽取到父类中
原型链
JS 原型链
在真正实现继承之前,我们先来理解一个非常重要的概念:原型链。
我们知道,从一个对象上获取属性,如果在当前对象中没有获取到就会去它的原型上面获取:
const obj = {
name: "mr",
age: 18,
};
obj.__proto__ = {
// message: 'hello aaa'
};
obj.__proto__ = {
// message: 'hello bbb'
};
obj.__proto__.__proto__ = {
message: "hello ccc",
};
原型链查找顺序图:
const obj = {}
相当于 const obj = new Object()
,所以 obj 是有原型对象的,它的原型对象就是 Object 对象
Object 的原型
那么什么地方是原型链的尽头呢?比如第三个对象是否也是有原型__proto__属性呢?
我们会发现它打印的是 [Object: null prototype] {}
事实上这个原型就是我们最顶层的原型了
从 Object 直接创建出来的对象的原型都是 [Object: null prototype] {}。
那么我们可能会问题: [Object: null prototype] {} 原型有什么特殊吗?
特殊一:该对象有原型属性,但是它的原型属性已经指向的是null,也就是已经是顶层原型了;
特殊二:该对象上有很多默认的属性和方法;
内存图-创建 Object 对象
内存图-原型链关系
Object 是所有类的父类
从我们上面的 Object 原型我们可以得出一个结论:原型链最顶层的原型对象就是 Object 的原型对象
实现继承-原型链-继承方法
通过原型链实现继承
如果我们现在需要实现继承,那么就可以利用原型链来实现了:
目前 stu 的原型是 p 对象,而 p 对象的原型是 Person 默认的原型,里面包含 running 等函数;
注意:步骤 3 和步骤 4 不可以调整顺序,否则会有问题
定义父类 Person
///1、定义父类构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.running = function () {};
Person.prototype.eating = function () {};
实现继承: 创建一个父类的实例对象new Person()
, 用这个实例对象作为子类的原型对象
///2、定义子类构造函数
function Student(sno) {
this.sno = sno
}
// 3、创建父类实例,用它作为子类的原型对象
+ const p = new Person("mr", 18)
+ Student.prototype = p
// 4、为子类添加原型方法
Student.prototype.studying = function() {}
~~错误的实现继承做法:~~父类的原型直接赋值给子类的原型
问题:父类和子类共享同一个原型对象,修改了任意一个,另外一个也被修改了
原型链继承的弊端
但是目前有一个很大的弊端:某些属性其实是保存在 p 对象上的;
第一,我们通过直接打印对象是看不到这个属性的;
第二,这个属性会被多个对象共享,如果这个对象是一个引用类型,那么就会造成问题;
第三,不能给 Person 传递参数(让每个 stu 有自己的属性),因为这个对象是一次性创建的(没办法定制化);
实现继承-构造函数-继承属性
借用构造函数继承
为了解决原型链继承中存在的问题,开发人员提供了一种新的技术: constructor stealing(有很多名称: 借用构造函数或者称之为经典继承或者称之为伪造对象):
- steal 是偷窃、剽窃的意思,但是这里可以翻译成借用;
借用继承的做法非常简单:在子类型构造函数的内部调用父类型构造函数
- 因为函数可以在任意的时刻被调用;
- 因此通过 apply()和 call()方法也可以在新创建的对象上执行构造函数;
组合借用继承的问题
组合继承是 JavaScript 最常用的继承模式之一:
如果你理解到这里, 点到为止, 那么组合来实现继承只能说问题不大;
但是它依然不是很完美,但是基本已经没有问题了;
组合继承存在什么问题呢?
组合继承最大的问题就是无论在什么情况下,都会调用两次父类构造函数。
- 一次在创建子类原型的时候;
- 另一次在子类构造函数内部(也就是每次创建子类实例的时候);
另外,如果你仔细按照我的流程走了上面的每一个步骤,你会发现:所有的子类实例事实上会拥有两份父类的属性
- 一份在当前的实例自己里面(也就是 person 本身的),另一份在子类对应的原型对象中(也就是 person.__proto__里面);
- 当然,这两份属性我们无需担心访问出现问题,因为默认一定是访问实例本身这一部分的;
实现继承-寄生组合
原型式继承函数
原型式继承的渊源
这种模式要从道格拉斯·克罗克福德(Douglas Crockford,著名的前端大师,JSON 的创立者)在 2006 年写的一篇文章说起:Prototypal Inheritance in JavaScript(在 JavaScript 中使用原型式继承)
在这篇文章中,它介绍了一种继承方法,而且这种继承方法不是通过构造函数来实现的.
为了理解这种方式,我们先再次回顾一下 JavaScript 想实现继承的目的:重复利用另外一个对象的属性和方法.
最终的目的:student 对象的原型指向了 person 对象;
创建中间原型对象的方法:
方法一: 创建父类实例,用它作为子类的原型对象
方法二: 创建空对象,该对象的隐式原型指向父类的原型对象,同时子类的原型对象指向该对象
方法三:
方法四:
封装 1:
封装 2: 寄生组合式继承(最终方案):考虑兼容问题
寄生式继承函数
寄生式(Parasitic)继承
寄生式(Parasitic)继承是与原型式继承紧密相关的一种思想, 并且同样由道格拉斯·克罗克福德(Douglas Crockford)提出和推广的;
寄生式继承的思路是结合原型类继承和工厂模式的一种方式;
即创建一个封装继承过程的函数, 该函数在内部以某种方式来增强对象,最后再将这个对象返回;
寄生组合式继承
现在我们来回顾一下之前提出的比较理想的组合继承
组合继承是比较理想的继承方式, 但是存在两个问题:
问题一: 构造函数会被调用两次: 一次在创建子类型原型对象的时候, 一次在创建子类型实例的时候.
问题二: 父类型中的属性会有两份: 一份在原型对象中, 一份在子类型实例中.
事实上, 我们现在可以利用寄生式继承将这两个问题给解决掉
你需要先明确一点: 当我们在子类型的构造函数中调用父类型.call(this, 参数)这个函数的时候, 就会将父类型中的属性和方法复制一份到了子类型中. 所以父类型本身里面的内容, 我们不再需要.
这个时候, 我们还需要获取到一份父类型的原型对象中的属性和方法.
能不能直接让子类型的原型对象 = 父类型的原型对象呢?
不要这么做, 因为这么做意味着以后修改了子类型原型对象的某个引用类型的时候, 父类型原生对象的引用类型也会被修改.
我们使用前面的寄生式思想就可以了.
寄生组合继承的代码
终极方案:寄生组合式继承▸
使用到的知识点:原型链、借用构造函数、原型式继承(对象之间)、寄生式函数
1、寄生组合式继承
2、使用寄生组合式继承实现继承
3、打印结果
4、内存图
实现继承-ES6
对象的方法补充
- Object.prototype.hasOwnProperty(prop):
返回:boolean
,对象是否有某一个只属于自己的属性(不是在原型上的属性)- 参数
- prop:``,要测试的属性的字符串名称或者 Symbol
- 返回值
- 如果对象有指定属性作为自有属性,则返回
true
;否则返回false
。
- in:
返回:
,判断某个属性是否在某个对象自己或者对象的原型链上 - for...in:
返回:
,遍历某个对象自己上或者其原型链上所有可枚举的属性(除 Symbol 外) - instanceof:
返回:
,用于*检测构造函数*(Person、Student 类)的 pototype,是否出现在某个实例对象的原型链上 - Object.prototype.isPrototypeOf(obj):
返回:boolean
,用于*检测某个对象*,是否出现在某个实例对象的原型链上- 参数
- obj:``,要搜索其原型链的对象。
示例:
1、hasOwnProperty
2、in 操作符
3、for...in 操作符
4、instanceof
5、isPrototypeOf
原型继承关系
内存图
class 类-定义类
我们会发现,按照前面的构造函数形式创建 类,不仅仅和编写普通的函数过于相似,而且代码并不容易理解。
在ES6(ECMAScript2015)新的标准中使用了 class 关键字来直接定义类;
但是类本质上依然是前面所讲的构造函数、原型链的语法糖而已;
所以学好了前面的构造函数、原型链更有利于我们理解类的概念和继承关系;
那么,如何使用 class 来定义一个类呢?
- 可以使用两种方式来声明类:类声明和类表达式;
class 类-构造函数
如果我们希望在创建对象的时候给类传递一些参数,这个时候应该如何做呢?
每个类都可以有一个自己的构造函数(方法),这个方法的名称是固定的constructor;
当我们通过new操作符,操作一个类的时候会调用这个类的构造函数constructor;
每个类只能有一个构造函数,如果包含多个构造函数,那么会抛出异常;
当我们通过new关键字操作类的时候,会调用这个 constructor 函数,并且执行如下操作:
1、在内存中创建一个新的对象(空对象);
2、这个对象内部的[[prototype]]属性会被赋值为该类的 prototype 属性;
3、构造函数内部的 this,会指向创建出来的新对象;
4、执行构造函数的内部代码(函数体代码);
5、如果构造函数没有返回非空对象,则返回创建出来的新对象;
class 类-实例方法
在上面我们定义的属性都是直接放到了 this 上,也就意味着它是放到了创建出来的新对象中:
在前面我们说过对于实例的方法,我们是希望放到原型上的,这样可以被多个实例来共享;
这个时候我们可以直接在类中定义实例方法;
类中定义多个方法,不需要用
,
分割
class 类-访问器方法▸
我们之前讲对象的属性描述符时有讲过对象可以添加setter和getter函数的,那么类也是可以的
class 类-静态方法▸
静态方法通常用于定义直接使用类来执行的方法,不需要有类的实例,使用static 关键字来定义:
class 类和构造函数的异同
我们来研究一下类的一些特性:
- 你会发现它和我们的构造函数的特性其实是一致的;
1、构造函数定义的类
2、class 定义的类
3、相同点
4、不同点
构造函数可以当做普通的函数调用,而 class 类不能
ES6 类的继承-extends
前面我们花了很大的篇幅讨论了在 ES5 中实现继承的方案,虽然最终实现了相对满意的继承机制,但是过程却依然是非常繁琐的。
- 在 ES6 中新增了使用extends 关键字,可以方便的帮助我们实现继承:
ES6 类的继承-super
class 为我们的方法中提供了super关键字
- 执行*super.method(...)*来调用一个父类方法
- 执行*super(...)*来调用一个父类 constructor(只能在子类的 constructor 中执行 super)
我们会发现在上面的代码中我使用了一个 super 关键字,这个 super 关键字有不同的使用方式:
注意:在子(派生)类的构造函数中使用this或者返回默认对象之前,必须先通过 super 调用父类的构造函数!
super 的使用位置有三个:子类的构造方法、实例方法、静态方法;
示例:
1、在子类的构造方法中使用 super
2、在子类的实例方法中使用 super(方法重写)
3、在子类的静态方法中使用 super(方法重写)
继承内置类
我们也可以让我们的类继承自内置类,比如 Array:
类的混入-mixin▸
JavaScript 的类只支持单继承:也就是只能有一个父类
那么在开发中我们我们需要在一个类中添加更多相似的功能时,应该如何来做呢?
这个时候我们可以使用混入(mixin);
~~应用:~~React 中的高阶组件
Babel
babel-ES6 转 ES5(源码)
1、简单类 Person 转 ES5
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
running() {}
eating() {}
static radomPerson() {}
}
const p1 = new Person("tom", 18);
2、继承类 Person 转 ES5
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
running() {}
eating() {}
static radomPerson() {}
}
class Student extends Person {
constructor(name, age, sno, score) {
super(name, age);
this.sno = sno;
this.score = score;
}
studyding() {}
static radomStudent() {}
}
const stu1 = new Student("mr", 18, 110, 100);
ES5 代码
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: { value: subClass, writable: true, configurable: true },
});
Object.defineProperty(subClass, "prototype", { writable: false });
if (superClass) _setPrototypeOf(subClass, superClass);
}
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf
? Object.setPrototypeOf.bind()
: function _setPrototypeOf(o, p) {
o.__proto__ = p; // Student.__proto__ = Person
return o;
};
return _setPrototypeOf(o, p);
}
function _createSuper(Derived) {
var hasNativeReflectConstruct = _isNativeReflectConstruct();
return function _createSuperInternal() {
var Super = _getPrototypeOf(Derived),
result;
if (hasNativeReflectConstruct) {
var NewTarget = _getPrototypeOf(this).constructor;
result = Reflect.construct(Super, arguments, NewTarget);
} else {
result = Super.apply(this, arguments);
}
return _possibleConstructorReturn(this, result);
};
}
function _possibleConstructorReturn(self, call) {
if (call && (_typeof(call) === "object" || typeof call === "function")) {
return call;
} else if (call !== void 0) {
throw new TypeError(
"Derived constructors may only return object or undefined"
);
}
return _assertThisInitialized(self);
}
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError(
"this hasn't been initialised - super() hasn't been called"
);
}
return self;
}
function _isNativeReflectConstruct() {
if (typeof Reflect === "undefined" || !Reflect.construct) return false;
if (Reflect.construct.sham) return false;
if (typeof Proxy === "function") return true;
try {
Boolean.prototype.valueOf.call(
Reflect.construct(Boolean, [], function () {})
);
return true;
} catch (e) {
return false;
}
}
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf
? Object.getPrototypeOf.bind()
: function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}
function _typeof(obj) {
"@babel/helpers - typeof";
return (
(_typeof =
"function" == typeof Symbol && "symbol" == typeof Symbol.iterator
? function (obj) {
return typeof obj;
}
: function (obj) {
return obj &&
"function" == typeof Symbol &&
obj.constructor === Symbol &&
obj !== Symbol.prototype
? "symbol"
: typeof obj;
}),
_typeof(obj)
);
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
Object.defineProperty(Constructor, "prototype", { writable: false });
return Constructor;
}
function _toPropertyKey(arg) {
var key = _toPrimitive(arg, "string");
return _typeof(key) === "symbol" ? key : String(key);
}
function _toPrimitive(input, hint) {
if (_typeof(input) !== "object" || input === null) return input;
var prim = input[Symbol.toPrimitive];
if (prim !== undefined) {
var res = prim.call(input, hint || "default");
if (_typeof(res) !== "object") return res;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return (hint === "string" ? String : Number)(input);
}
var Person = /*#__PURE__*/ (function () {
function Person(name, age) {
_classCallCheck(this, Person);
this.name = name;
this.age = age;
}
_createClass(
Person,
[
{
key: "running",
value: function running() {},
},
{
key: "eating",
value: function eating() {},
},
],
[
{
key: "radomPerson",
value: function radomPerson() {},
},
]
);
return Person;
})();
var Student = /*#__PURE__*/ (function (_Person) {
_inherits(Student, _Person);
var _super = _createSuper(Student);
function Student(name, age, sno, score) {
var _this;
_classCallCheck(this, Student);
_this = _super.call(this, name, age);
_this.sno = sno;
_this.score = score;
return _this;
}
_createClass(
Student,
[
{
key: "studyding",
value: function studyding() {},
},
],
[
{
key: "radomStudent",
value: function radomStudent() {},
},
]
);
return Student;
})(Person);
var stu1 = new Student("mr", 18, 110, 100);
多态
JavaScript 中的多态
面向对象的三大特性:封装、继承、多态。
- 前面两个我们都已经详细解析过了,接下来我们讨论一下 JavaScript 的多态。
JavaScript 有多态吗?
维基百科对多态的定义:多态(英语:polymorphism)指为不同数据类型的实体提供统一的接口,或使用一个单一的符号来表示多个不同的类型。
非常的抽象,个人的总结:不同的数据类型进行同一个操作,表现出不同的行为,就是多态的体现。
那么从上面的定义来看,JavaScript是一定存在多态的。
1、JS中的多态
2、严格语言中的多态条件:
1、必须有继承或接口
2、必须有父类引用指向子类对象
对象字面量增强
1、属性的简写
2、方法的简写
3、计算属性名
解构
1、数组的解构
- 基本使用
- 顺序问题:有严格的顺序
- 解构出数组
- 解构的默认值
2、对象的解构
- 基本使用
- 顺序问题:对象的解构没有顺序,是根据key来解构的
- 重命名变量
- 默认值
- 对象的剩余内容
3、解构的应用
手写
手写 call,aplly,bind
函数对象原型关系
函数 foo 对象的隐式原型 === Function 的显式原型
// 函数foo对象的隐式原型 === Function的显式原型
console.log(foo.__proto__ === Function.prototype); // true
console.log(Function.prototype.apply); // f apply()
console.log(Function.prototype.call); // f call()
console.log(Function.prototype.bind); // f bind()
console.log(Function.prototype.apply === foo.apply); // true
结论:
- foo对象中的某些属性和方法是来自 Function.prototype 的
- 在 Function.prototype 中添加的属性和方法,可以被所有的函数获取
在 Function 的原型中添加方法 bar
手写 apply 方法
给函数对象添加方法
function foo() {
console.log("foo", this);
}
Function.prototype.mrapply = function (mrthis) {
// 相当于 mrthis.fn = this
Object.defineProperty(mrthis, "fn", {
configurable: true,
value: this,
});
// 隐式调用fn,可以让fn函数的this指向 mrthis
mrthis.fn();
// 删除多出来的临时函数fn
delete mrthis.fn;
};
foo.mrapply({ name: "Tom" });
如果传入的参数是一个 String 或者 Number 的类型,需要将其包裹成对象类型,才能在它上面添加属性
调用 mrapply 时,传递参数
function foo (age, height) {
console.log('foo', this, age, height)
}
+ Function.prototype.mrapply = function(mrthis, args) {
// 当this不是对象时,需要用Object包裹
mrthis = (mrthis === null || mrthis === undefined) ? window : Object(mrthis)
// 相当于 mrthis.fn = this
Object.defineProperty(mrthis, 'fn', {
configurable: true,
value: this
})
// 隐式调用fn,可以让fn函数的this指向 mrthis
+ mrthis.fn(...args)
// 删除多出来的临时函数fn
delete mrthis.fn
}
+ foo.mrapply({name: "Tom"}, [18, 1.88])
foo.mrapply(null, [18, 1.88])
foo.mrapply(undefined, [18, 1.88])
foo.mrapply(true, [18, 1.88])
foo.mrapply(123, [18, 1.88])
foo.mrapply('aaaa', [18, 1.88])
手写 call 方法
function foo(age, height) {
console.log('foo', this, age, height)
}
+ Function.prototype.mrcall = function(mrthis, ...args) {
mrthis = (mrthis === null || mrthis === undefined) ? window : Object(mrthis)
Object.defineProperty(mrthis, 'fn', {
configurable: true,
value: this
})
+ mrthis.fn(...args)
delete mrthis.fn
}
+ foo.mrcall({ name: "张飞" }, 20, 1.77)
抽取封装公共函数
/* 抽取封装的函数 */
+ Function.prototype.mrexec = function(mrthis, args) {
mrthis = (mrthis === null || mrthis === undefined) ? window : Object(mrthis)
// mrthis.fn = this
Object.defineProperty(mrthis, 'fn', {
configurable: true,
value: this
})
mrthis.fn(...args)
delete mrthis.fn
}
/* 手写apply */
Function.prototype.mrapply = function(mrthis, args) {
this.mrexec(mrthis, args)
}
/* 手写call */
Function.prototype.mrcall = function(mrthis, ...args) {
this.mrexec(mrthis, args)
}
// 测试
function foo(age, height) {
console.log('foo', this, age, height)
}
foo.mrapply({name: "Tom"}, [19, 1.66])
foo.mrcall({name: "Jack"}, 22, 1.99)
手写 bind 方法
和 apply, call 不同,bind 执行后是返回一个新的函数 newFoo
基础实现
思路:想办法实现如下:
// 伪代码
{ name: "why" }.foo(name, age)
/* 手写bind */
Function.prototype.mrbind = function(mrthis, ...args) {
+ return (...moreArgs) => {
mrthis = (mrthis === null || mrthis === undefined) ? window : Object(mrthis)
Object.defineProperty(mrthis, 'fn', {
configurable: true,
value: this
})
+ const allArgs = [...args, ...moreArgs]
+ mrthis.fn(...allArgs)
+ delete mrthis.fn // 可以删除fn,因为每次调用newFoo,都会重新生成一个mrthis.fn
}
}
// 测试
function foo(name, age, height, address) {
console.log('foo', this, name, age, height, address)
}
const newFoo = foo.mrbind({name: "Jerry"}, '张飞', 45)
console.log(newFoo)
+ newFoo(1.88, '成都')
+ newFoo(1.88, '成都')
浅拷贝,深拷贝
引用赋值
浅拷贝
方式:
- 解构赋值:
const info = {...obj}
浅拷贝修改 info2.name 后,obj 的 name 依然是"why",被修改的只是 info2
浅拷贝的内存图
如果 obj 对象中有**其他对象(或数组)**时的内存图
深拷贝
方式:
- 1、借助第三方库:
underscore
- 2、利用现有 JS 机制:
JSON
- 3、自己实现:
2、利用现有 JS 机制:JSON
语法:
const info3 = JSON.parse(JSON.stringify(obj));
缺点: 该方法不能实现方法的深拷贝,会忽略 obj 对象中的方法
const obj = {
name: 'Tom',
age: 18,
friend: {
name: 'Jack'
},
run: function() {
console.log(this.name + '在跑步~');
}
}
// 利用JSON机制实现深拷贝
+ const info = JSON.parse(JSON.stringify(obj))
// 测试
console.log(info)
// 修改info的深度属性,obj的深度属性保持不变
+ info.friend.name = '张飞'
+ console.log('obj', obj.friend.name); // obj Jack
+ console.log('info', info.friend.name); // obj 张飞
// 不能实现方法的深拷贝,会忽略obj对象中的方法
+ info.run() // ncaught TypeError: info.run is not a function