-
在该章节中,我们会对结合React中的高阶组件对类的混入进行了解
-
对JS中的多态进行分析,和传统面向对象的多态进行对比
-
学习新的字面量:对象字面量,研究延伸而出的属性增强与方法增强
-
新的数据处理方式解构又是怎么样的?解构的对象都有哪些?对我们的帮助在哪?解构都有几种方式?解构顺序是怎么样的?这些都是本章节中会学习掌握的内容
-
一、扩展继承内置类
-
事实上,在使用继承的时候,我们不仅可以继承自己写出来的类,还可以继承几乎所有的内置构造函数,包括但不限于以下几个:
-
**
Array
**:创建具有额外功能或自定义行为的数组类 -
**
String
**:创建扩展字符串处理功能的类 -
Map
和 **Set
**:添加额外的数据结构方法 -
**
Promise
**:创建具有额外行为(如记录、性能监测等)的Promise类 -
**
Error
**:创建自定义错误类型,这在管理大型应用程序的错误时非常有用
-
-
在默认的情况下,所有的类都继承自Object,也就是以下代码1与代码2所具备的含义是相同的
//代码1
class Person {
}
//代码2
class Person2 extends Object {
}
-
而这个扩展继承内置类有什么作用呢?
-
我们已知**
new
** 运算符允许开发人员创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例
,在这里我们所需要抓住的是后半句话 -
在以往,我们可以通过new运算符来调用
具有构造函数的内置对象的实例
,但这有一个缺点,在于内置对象的方法都是固定的,我们并不容易进行扩展。在下方的案例中,我希望在创建数组对象的时候,数组对象本身能够具备更多定制方法
-
var arr = new Array(1,2,3)
arr.方法()//该方法不是数组自带的方法
-
此时
扩展继承内置类
就有对应的作用了-
我们知道想要让数组对象具备更多的方法是可以直接添加在数组的原型链身上的,但这样做并不好
-
因为原型链上的内容方法太多,会造成使用上的负担和混淆。且我们是定制方法,可能只针对某一部分进行使用,而非所有数组通用
-
这时候在数组和new调用之间,我们需要一个中间缓冲层来进行定制化处理,这种装饰器思想体现在过往讲解的Object.create方法中也有所体现(创建新的对象来继承原型和添加额外的功能后返回新对象,新对象即缓冲层,不会改变原对象内容)
-
-
在
扩展继承内置类
中,则是以类为中间层,基于继承来源进行二次的封装或者添加额外功能而不影响继承来源本身-
在使用的时候,就不需要
new 继承来源
,而是直接new调用改动后的类
-
class HYArray extends Array{//继承数组,然后进行扩展
//扩展内容
}
var arr = new HYArray(10,20,30)
console.log(arr);//HYArray(3) [10, 20, 30]
-
通过这个方式,我们就能够实现一些内置的方法,甚至在这些内置方法上做一些扩展。例如获取每个数组的第二个之类的,自己实现出来,将实现过程封装起来
-
通过
new HYArray
创建出来一个定制化的数组对象,可以在arr中调用定制化的lastItem方法
与firstItem方法
-
获取最后一个数据就不需要再使用
this.length -1
来确定数组索引最后一位,虽然在当下已经有对应的at方法可以以arr.at(-1)
就实现获取数组索引的最后一位,但定制化的需求是层出不穷的,基础的功能哪怕每年都在添加优化,也不能完全满足我们的需求,此时这种扩展方式就能够派上用场了
-
class HYArray extends Array{
get lastItem(){
//获取数组最后一个的数据
return this[this.length -1]
}
//获取数组的第一个数据
get firstItem(){
return this[0]
}
}
var arr = new HYArray(10,20,30)
console.log(arr.lastItem);//30
console.log(arr.firstItem);//10
//我们以前的做法,是直接在原型链上面进行扩展
Array.prototype.lastItem = function(){
return this[this.length -1]
}
二、类的混入mixin
-
类的混入(mixin)是一种在面向对象编程中用于实现代码复用的技术,它将一个类的方法和属性注入到另一个类中,从而达到功能组合和代码重用的目的,所以在这里的
混入mixin
是一种思想体现,而非新的语法 -
混入不同于传统的继承,它更加灵活,并且可以避免一些继承带来的问题,比如类层次结构过深或者类之间的紧耦合
-
之所以出现混入这种做法,是因为我们
extends
继承只能够继承一个父类,当需要多个父类结合继承的时候,就难以做到
class Person {
}
class Runner {
running(){
}
}
//无法同时继承两个类(错误写法)
class Student extends Person,Runner {
}
图19-1 子类只能有一个直接父类
-
在JS当中,一个子类只能有一个直接的父类。即一个子类只能继承一个父类的属性和方法,这种形式我们往往会称为:
单继承
-
这种继承关系清晰,易于理解,而且可以避免复杂的多重继承带来的问题
-
与之对应的
多继承
则是一个子类可以继承多个父类。即一个子类可以从多个父类继承属性和方法,在功能强大的同时,会带来更高的复杂度
-
-
JS这门语言为什么最终选择了单继承而非多继承,则需要从其他角度去看待:
-
最主要的原因在于JS实现继承很大一部分来源于
原型链
,每个对象通过其原型链继承属性和方法 -
单继承模型让原型链的管理更简单,避免了多重继承带来的复杂性。例如,单继承中,子类从一个父类继承特性,原型链上只需要处理一条路径,易于理解和管理
-
在原型链的基础上,如果选择了多继承,可能会导致方法和属性的冲突,特别是当多个父类定义相同的方法或属性时。这种情况下,子类如何解析这些冲突可能变得非常复杂,导致难以维护和调试
-
原型链的复杂度可能会指数型上升,原本的原型链对于初学者来说已经较为复杂了,如果再进一步,学习的陡峭程度会令人难以接收
-
不过也有历史的原因,JS早期的设计和实现并没有多继承的概念,后来引入的 ES6 类语法和继承机制都是建立在单继承的基础上。为了保持语言的一致性,JS 继续保持了单继承的设计,而非选择转移到多继承上
-
-
但在JS当中,可以通过另一种角度来实现
多继承
的效果,也就是我们所说的混入mixin-
通过编写
工具函数
,让工具来替我们从另一个角度实现混入 -
在下方这个案例中,我们在工具函数中已经有一个默认新类(NewClass)了,会继承我们传入工具函数形参的类
-
class Person {
}
function mixinRunder(BaseClass){
class NewClass extends BaseClass {
running(){
}
}
return NewClass
}
var newClass = mixinRunder(Person)
var runClass = new newClass()
-
不过目前这种基础方案肯定是不让人满意的,因为单纯这样进行使用,只是继承一个类,我直接extends就行了
-
但函数是可以嵌套调用的,而这才是混入的用法
-
在进行多层调用的时候,class是可以匿名形式的(mixinEater工具函数)
-
但这种调用方式依旧不是我们想要的,代码的复杂度依旧有点高,而且这从使用角度上已经改变了类的用法了,在每一个类的外面都套了一层函数,相当于工具函数需要写好多遍,每个工具函数的名称都不同,已经违背"工具"的初衷了,这种方式并不理想
-
class Person {
}
function mixinRunder(BaseClass){
class NewClass extends BaseClass {
running(){
}
}
return NewClass
}
function mixinEater(BaseClass) {
return class extends BaseClass {
eating() {
console.log('eating');
}
}
}
//嵌套调用
var newClass = mixinEater(mixinRunder(Person))
var runClass = new newClass()
-
正常来说,我们应该通过一个函数就能够确定到底要继承哪些类
-
通过对每个基类(父类)遍历其原型上的方法,并将这些方法添加到目标类的原型上,实现混入效果
-
但首先,这种混入效果会对原有的目标类,也就是子类造成影响,且混入的只有原型上的实例方法,没有类本身的静态方法,还无法自由的选择需要父类的哪些内容
-
所以想要实现真正能用的
多继承混入
需要考虑很多情况,是一件难度很高的事情
-
// 工具函数:实现类的混入(残缺 缺陷版本 仅供参考)
function applyMixins(derivedCtor, baseCtors) {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
derivedCtor.prototype[name] = baseCtor.prototype[name];
});
});
}
// 示例基类
class A {
methodA() {
console.log('Method A');
}
}
class B {
methodB() {
console.log('Method B');
}
}
// 目标类
class C {
methodC() {
console.log('Method C');
}
}
// 应用混入
applyMixins(C, [A, B]);
//更好的实践角度,不影响原有类
//const MixedClass = createMixedClass([A, B]);
// 测试
const instance = new C();
instance.methodA(); // 输出: Method A
instance.methodB(); // 输出: Method B
instance.methodC(); // 输出: Method C
三、React中的高阶组件
-
在React当中,类与函数是存在混合使用的情况(虽然并不常见),这和类的混入mixin思想是有类似之处的
-
connect
函数是一个典型的高阶组件(HOC),用于连接React组件与Redux数据层。这种模式常见于类组件和函数组件的混合使用中,尤其是在Hooks出现前,许多库使用类来实现更复杂的逻辑和生命周期管理 -
在大型项目中,完全重写所有组件为函数组件可能不现实。通过类似
connect
这样的HOCs使得可以逐步迁移,同时保持既有代码的功能性 -
HOCs(High-Order Components,高阶组件)是React的重要模式,用于增强和复用组件逻辑。高阶组件是一个函数,接收一个组件作为参数并返回一个新的组件(柯里化操作)。通过高阶组件,可以重用组件逻辑,增强组件功能,或者向组件注入额外的props和行为。在这里是类似的行为
-
总体来看,这是一种权衡,在理想(完全重构)和现实(时间、精力、成本...)之间做出的折中选择,在React18亦或者19版本之后,纯函数组件形式会越来越流行,也许未来的某一天,当
类组件
向函数组件
过渡结束之后,就不再需要这种操作了
-
图19-2 React中的高阶组件
四、JavaScript多态的定义与理解
4.1 多态的定义
-
面向对象的三大特性:封装、继承、多态中,我们已经学习过封装和继承了,接下来让我们来学习下最后一个部分,
多态
的内容-
维基百科对多态的定义:多态(英语:polymorphism)指为不同数据类型的实体提供统一的接口,或使用一个单一的符号来表示多个不同的类型
-
很精准,但非常的抽象,我们可以暂时理解为:
不同的数据类型进行同一个操作,表现出不同的行为,就是多态的体现
-
-
配合案例进行理解,会有一个更清晰的概念,在这里我们会分析多态的不同表现(传统面向对象语言与JS动态类型语言)
-
在静态类型语言(传统面向对象语言)如Java和C++中,多态通常通过继承和接口来实现
-
而JavaScript作为一种动态类型语言,支持的是更为灵活的多态形式
-
4.2 传统面向对象多态
-
传统面向对象的多态通过TypeScript也可以进行一定程度上的实现,通过该方式进行验证会更加轻松
-
在TS语言当中,我们设置一个父类,两个子类作为基本内容。其中两个子类都继承同一个父类,在父类当中有一个getArea方法
-
我们创建一个函数,该函数接收一个shape(形状)参数,通过TS设置类型校验为必须Shape类型才能通过。这个函数的作用在于实现
执行同样的操作
-
//Shape形状
class Shape {
getArea(){}
}
//子类
class Rectangle extends Shape {
}
//子类
class Circle extends Shape {
}
var r = new Rectangle()
var c = new Circle()
//多态:当对不同的数据类型执行同一个操作时,如果表现出来的行为(形态)不一样,那么就是多态的体现
function calcArea(shape:Shape) {
console.log(shape.getArea())
}
calcArea(123)
calcArea('123')
-
对传参类型进行限定后,不管是数字类型还是字符串类型都无法成功传递
-
这就让我们需要对多态的前半段解释有一个更具体的理解:
不同的数据类型
是怎么定义的 -
当我们谈论到
“不同的数据类型”
执行同一个操作时显示多态,特指的是不同的类实例类型,而不是基本数据类型如数字或字符串。这些类实例通常都是某个共同父类或接口的派生类(子类)或实现类
-
图19-3 TypeScript的类型限定
-
也就是说,我们所要传递进实参的部分是不同子类,如r与c两个实例对象
-
但在目前的情况下,只满足了多态的前半句定义:
不同的数据类型执行同一个操作
-
还有最为重要的部分没有实现:表现出来的行为不同
-
//...前置信息省略
var r = new Rectangle()
var c = new Circle()
function calcArea(shape:Shape) {
shape.getArea()
}
//产生结果相同
calcArea(r)
calcArea(c)
-
我们目前所调用的两次calcArea函数,虽然传递进不同子类,但产生的结果是相同的,因为都是调用来自父类的方法
-
想要产生不同的结果,就涉及到一个做法:
重写
-
在子类中进行重构原有的方法,直接遮蔽掉父类,在查找优先度上,就会优先找上子类的方法
-
//多态的表现
/*
在严格的面向对象语言中,多态是存在如下条件的:
1.必须有继承(实现接口)
2.必须有父类的引用指向子类对象
*/
//Shape形状(父类)
class Shape {
getArea(){}
}
//子类
class Rectangle extends Shape {
getArea(){
return '子类1'
}
}
//子类
class Circle extends Shape {
getArea(){
return '子类2'
}
}
-
在这种情况下重新执行以下代码的时候
-
传入r与c实参的calcArea函数会产生不同的结果,因为经过子类重写后,所调用的方法来自不同子类(多态第一条件:不同数据类型执行同一操作),而非同一父类
-
因此在
传统面向对象多态
上,准确的定义是:对不同的数据类型执行同一个操作时,表现出来的行为(形态)不一样
-
var r = new Rectangle()
var c = new Circle()
function calcArea(shape:Shape) {
shape.getArea()
}
//产生结果不同
calcArea(r)
calcArea(c)
-
因此,在这里多态的表现形式是
重写
,也被称为运行时多态
或者是动态多态,具备三个前提(基础):
-
必须有继承(多态的前提)
-
必须有重写(子类重写父类方法)
-
必须有父类引用指向子类对象
-
对于第三点会比较抽象,不好理解,对于这个我们需要先确定
父类引用
指向子类对象
体现在哪一处地方,这对我们理解有很大的帮助-
首先,我们在calcArea函数的形参当中进行绑定
Shape类型
,这是父类型。而我们在实参进行传递进去的r是子类型 -
如果把过程步骤都去掉,保留
实参 => 形参
的部分,就可以惊讶的发现相当于var shape:Shape = new Rectangle()
,其中左边是父类引用(通过TS我们实现了shape具备Shape父类的特质),右边是子类对象。完美的形成父类引用指向子类对象的情况
-
function calcArea(shape:Shape) {
shape.getArea()
}
calcArea(r)
-
重写的方法必须保持与原方法相同的签名(名称和参数)。返回类型可以是被重写方法的返回类型的子类型(称为协变返回类型)
4.3 JavaScript多态
-
而JS的多态和传统面向对象的多态是有所不同的,不需要满足多态的三个前提要求
-
依旧通过函数来完成同样的操作,但我们传递进函数的实参发生了不同
-
在传统面向对象多态中,传递进实参的
不同的数据类型
的范围被进一步扩大了,没有被束缚在同一个父类下的不同子类。只要我们传递进的数据类型能够实现同一个操作产生不同结果就行 -
在obj对象中存在
getArea方法
和Person类中的getArea实例方法
具备了同方法名、同参数的性质,但返回内容不同。从而实现属于JS的多态 -
在这个程度来说,这依旧符合多态的定义:不同的数据类型进行同一个操作,表现出不同的行为,就是多态的体现
-
但一定程度上,JS多态的关联不需要传统面向对象多态的前置条件,从而导致这个行为并不紧密也不明显,这也是有些人会争论在JS当中是否具备多态的原因
-
-
这种灵活性来自于JavaScript的动态类型特性和“鸭子类型”(duck typing)的概念,即“如果它走路像鸭子,叫声像鸭子,那么它就是鸭子”
function calcArea(foo) {
console.log(foo.getArea());
}
var obj1 = {
name:'xiaoyu',
getArea: function(){
return 1000
}
}
class Person {
getArea() {
return 100
}
}
var p = new Person()
//实现多态
calcArea(p)
calcArea(obj1)
五、对象字面量
5.1 属性增强
-
JS当中的字面量有很多种,例如数值、字符串、布尔、数组、正则表达式等形式的字面量,以及我们即将说明的强化版的
对象字面量
-
字面量在 JavaScript 中表示固定的值,而不是变量,这些值是直接在脚本中提供的。严格来说,指的是代码中直接固定书写的值。这强调了字面量的特性:当使用这些字面量时,它们确实是直接写入代码的,也就是说,在代码编写和保存时,它们的格式和值已经确定。这些字面量在程序中每次运行时都会创建相同的值或结构
-
然而,对于对象字面量和数组字面量来说,虽然它们的初始状态是固定的,但它们创建的是可变对象。这意味着对象或数组本身的属性或元素可以在程序运行期间被修改。例如,使用对象字面量定义一个对象时,虽然其属性的初始值是固定的,但这些属性的值在运行时是可以被改变的,如修改对象的
name
属性
-
var object = { name: "coderwhy", age: 30 };
object.name = "xiaoyu";//此时赋值的过程是运行时,改变了对象的状态
console.log(object); // 输出: { name: "xiaoyu", age: 30 }
-
对于属性的获取上来说,数据是可以拆分出去的,我们如果将name和age的具体内容拆分出去,在引入object对象字面量中,该对象字面量的内部会有一个赋值的操作,算不算运行时呢?
-
这个过程中,
name
和age
是外部变量,它们的值在创建object
时被“捕获”并赋给对象的属性。这个阶段发生在代码执行到这一行时,即运行时(实际的内存写入操作) -
这个赋值的操作发生在对象字面量创建时,我们需要考虑的是这个
创建时
在JS运行中处于一个什么阶段,这意味着name和age获取到第一次值会是undefined还是已经存在的值 -
对象字面量的创建和初始化通常发生在代码执行阶段,而不是在编译或预解析阶段,所以执行阶段是这样的:
-
执行环境(通常是某个函数或全局环境)中的代码开始执行
-
变量和函数声明被提升(hoisting),变量初始化为
undefined
,函数则已经完全定义 -
当执行流到达对象字面量定义的位置时,对象字面量创建的具体步骤包括:为对象字面量分配内存,根据字面量中的
键-值对
初始化对象的属性。这时,如果属性的值由变量提供,JavaScript 引擎会读取当前作用域中这些变量的实际值进行赋值
-
-
因此,对象字面量的创建和属性赋值是在脚本的运行时(执行阶段)进行的,具体是在代码执行到该对象字面量声明的地方时
var name = 'coderwhy'
var age = 30
var object = { name: name,age : age };
object.name = "xiaoyu";//此时赋值的过程是运行时,改变了对象的状态
console.log(object);
-
而这和我们要讲解的对象字面量加强有关
-
当我们想要将外部变量赋值给对象字面量的内部属性时,一旦重名可以进行省略写法(语法糖),这种写法被JS称为property shorthand(属性的简写)
-
这个简写的实现操作在代码层面上简化了
变量赋值属性
的赋值操作的过程,直接显示最终结果,减少了代码的冗余度,意图更明确 -
这种语法糖的实现主要是在 JS 引擎的解析阶段进行的,主要来自曾经我们学过的两个阶段:
词法分析
和语法分析
,属于在对象字面量创建之前就实现的 -
在 AST 的构建过程中,当解析器遇到对象字面量时,会检查每一个属性。如果属性的值未显式提供,并且当前作用域中有与属性名相同名称的变量存在,解析器就会自动将该变量的值赋给属性。这一过程是自动的,不需要任何额外的运行时支持
-
var name = 'coderwhy'
var age = 30
var object = { name,age };
object.name = "xiaoyu";//此时赋值的过程是运行时,改变了对象的状态
console.log(object);
5.2 方法增强
-
在对象内部的函数,我们称为方法
-
在ES6之前,我们通常采用第一种支持的普通函数写法
-
在ES6之后,我们可以实现箭头函数的写法,但我们知道这是不绑定this的,所以就连正常的写法,我们也可以省略前置的
function
关键字,从而简化方法的调用,这个过程被称为方法的简写:Method Shorthand
,也是一种语法糖
-
/*
2.方法的增强
*/
var name = "小余"
var age = 20
//通常我们是这么写对象字面量的,但是通常在我们写之前,外界可能已经有name跟age这两个重复的内容了
var obj = {
name,
age,
//1.正常的普通函数写法
running:function(){},
//2.箭头函数的写法(但是不会绑定this)
eating:()=>{},
//3.类似class的定义方法,省去了function。是属于第一种方法的简写
swiming(){}
}
obj.running()//{name: '小余', age: 20, running: ƒ, eating: ƒ, swiming: ƒ}
obj.eating()//Window {window: Window, self: Window, document: document, name: '小余', location: Location, …}
obj.swiming()//{name: '小余', age: 20, running: ƒ, eating: ƒ, swiming: ƒ}
5.3 计算属性名
-
我们说过,对象字面量属于
可变对象
,在对象中的内容分为两种,一种是键(属性),一种是值(数据)-
我们已经可以实现当
键值对相同时的合并简化
以及值是方法的简化
-
值可以从外部导入变量从而实现自定义动态,极大的提高自由度。而
键
也可以,但在操作上有所区别,需要用[]
括起来,对键的该操作被称为计算属性名
-
/*
3.计算属性名
*/
var key = "address"
var name = "小余"
var age = 20
var obj = {
name,
age,
//我们内部的属性名之前都是写死的,要是我想要上面key变量里面的内容address作为我的属性名,那需要怎么办
//错误的写法
// key:"广州"
//正确的写法,使用中括号圈起来,叫做计算属性名是因为会根据我们变量对应的名称来获取真正的值
[key]:"福建"
}
console.log(obj);//{ name: '小余', age: 20, address: '福建' }
-
直接使用变量作为属性名(不通过方括号)在 JavaScript 语法中是不支持的,因为在对象字面量的键位置,JavaScript 期望的是一个字符串字面量或符合标识符规则的名字。如果直接使用变量,解析器会将其视为字符串字面量
-
方括号
[]
内的内容会被视为一个表达式(这意味可以放任何有效的 JS 表达式,包括变量、函数调用等,JS 引擎会计算这个表达式的结果,并将结果用作属性名),这种运用方式在很多地方都有所体现,在Vue中为{{}}
模板语法,在React中则是{}
,掌握该方式的原理,在进行学习框架的时候可以做到快速理解掌握 -
对象的键(属性)默认情况下都会省略
字符串' '
的符号,但也可以手动加上,这在使用上是没有太大区别的,也说明的键与值在解析上的不同之处
-
let object = { name: name,age : age };
//JavaScript 期望的是一个字符串字面量或符合标识符规则的名字
let object2 = { 'name': name,'age' : age };
//计算属性名的能力展现
// 使用字符串连接生成属性名
var key = "address" + " city";
// 定义一个函数,用于生成带有前缀的属性名
function getKeyName(suffix) {
return "data-" + suffix;
}
// 实际业务场景中的动态属性名
var userField = "email";
var userValue = "1045098807@qq.com";
// 创建对象时使用计算属性名
var obj = {
[key]: "福建",
[getKeyName("name")]: "XiaoYu",
[userField]: userValue, // 直接使用变量作为属性名
[`${userField}_verified`]: true // 使用模板字符串生成复杂属性名
};
// 打印对象查看结果
console.log(obj);
-
但不管是属性增强、方法增强还是计算属性名,运行的所处阶段都是一样的:在对象字面量被解析和创建的时候。这些技术都是在对象初始化阶段执行的,都属于运行时(execution phase)的操作
-
它们促进了代码的模块化和可维护性,因为可以根据需要灵活调整对象的结构,而不必在多处修改固定的属性名或方法定义
表19-1 对象字面量特性总结
特性 | 描述 |
---|---|
属性增强 | 通过外部变量或计算结果直接在对象字面量中设置属性值 |
方法增强 | 允许在对象字面量内定义方法,这些方法可以直接使用外部作用域中的值或进行动态计算 |
计算属性名 | 使用表达式来动态生成属性名,这些表达式在对象创建时被求值 |
六、解构Destructuring
-
ES6 中新增了从数组或对象中方便获取值,并赋值给声明的变量的方法,称之为解构赋值(Destructuring),这是一种 JavaScript 表达式,能够做到从复杂数据结构中提取数据更加简洁和直观
-
解构主要分为两种模式:数组解构、对象解构
-
因为解构的目的是从复杂数据结构中提取数据,所以目标对象至少需要是一个复杂数据解构。主要指的是可以迭代或具有可枚举属性的结构,而数组和对象是最常用于解构的复杂数据结构,在ES7中的 Map 和 Set 等也可以进行解构,尽管它们通常需要先转换为数组
-
//数组解构
var names = ['coderwhy','xiaoyu']
var [name1,name2] = names
//等同以下代码(Babel转化)
var name1 = names[0],name2 = names[1];
//对象解构
var obj = {name:"小余",age:18,height:1.75}
var {name,age,height} = obj
//等同以下代码(Babel转化)
var name = obj.name,age = obj.age,height = obj.height;
-
对于数据的操作,最重要的是如何精准的获取想要的内容。而对于解构来说,则意味着如何在复杂的数据中解析出想要的内容
-
对于这点的理解,取决了我们要使用
数组解构
还是对象解构
来解决我们面对的问题 -
通过对象的键值对,可以非常精准的拿到内容,而通过数组获取内容大多使用索引。键本身有意义,但索引没有,所以数组通常是
同质数据
,对象是非顺序数据
-
因此在使用解构的时候,对象会比数组更在意数据的顺序问题
-
6.1 数组解构
-
解构的执行顺序很重要,能够看到清晰的数据流,在从复杂数据中解构内容出来时
-
有解构成功,解构错误,解构方式等多种情况,这些都是基于数据的流动
-
掌握解构的细节可以帮助我们合理利用默认值和跳过不需要的项
-
var names = ["小余","coderwhy","JS高级","XiaoYu","why","前端"]
//数组的解构(不使用解构方式)
// var name1 = names[0]
// var name2 = names[1]
// var name3 = names[2]
// var name4 = names[3]
// var name5 = names[4]
// var name6 = names[5]
//数组使用解构方式(会自动从头按顺序填入),[]是固定格式
var [name1,name2,name3,name4,name5,name6] = names
console.log(name1,name2,name3,name4,name5,name6);//小余 coderwhy JS高级 XiaoYu why 前端
//顺序问题:严格的顺序(对于我们中间不想要的属性需要留出位置,逗号间留出空白)
var [name1, ,name3, ,name5,name6] = names
console.log(name1,name3,name5,name6);//小余 JS高级 why 前端
//解构出数组(前面两个单独放,后面4个我希望放在一个数组里面),使用剩余参数形式
var [name1,name2,...newNames] = names
console.log(name1,name2,newNames);//小余 coderwhy [ 'JS高级', 'XiaoYu', 'why', '前端' ]
//解构的默认值(如果数组里面有undefined,解构不出来的时候,可以给出默认值),测试不生效,这里我跟coderwhy的情况发生了不一致的情况,需要明确的在某一个内容上进行赋值默认值,比如现在下面是obj2为undefined,那就需要将默认值写在obj2那里
var obj = ["XiaoYu",undefined,"coderwhy"]
var [obj1,obj2="小余",obj3="default"] = obj
console.log(obj1,obj2,obj3);
图19-4 数组解构的固定赋值顺序
var names = ["小余","coderwhy","JS高级"]
var [name1,name2,name3] = names
//数组解构本质上是一个顺序解构,顺序来自索引,只能跳过不能省略
var [name1,name2,name3] = ["小余","coderwhy","JS高级"]
-
可以总结出数组的四种使用方式
-
其中跳过元素这个方式很少使用到,因为对应的使用场景是
选取特定元素
,而通常面对这种需求的往往是对象,而非数组 -
对于解构出来的内容,为了避免解构失败,我们可以直接在解构过程中附加默认值,默认值在正常获取不到的情况触发
-
而通过对解构过程的Babel转化,可以明确解构只是一个语法糖,对于实际的赋值并没有产生本质改变,所以解构出来的变量的使用范围与数据变化过程和正常声明变量是一样的(取决于使用var还是let与const声明)
-
表19-2 数组解构的四种使用方式
方法 | 基础描述 | 使用场景 | 底层原理 |
---|---|---|---|
基本解构 | 将数组元素按顺序赋值给变量 | 快速提取数组中的多个元素 | JavaScript 引擎在解析数组字面量时按顺序赋值给变量 |
跳过元素 | 使用逗号留空位以跳过数组中的特定元素 | 当需要从数组中选取特定元素而忽略其他元素时 | 逗号分隔符在解构时作为占位符,忽略对应位置的元素 |
剩余参数形式 | 使用...操作符收集剩余数组元素到一个新数组 | 当需要将数组前几个元素单独处理,其余元素集中处理时 | ...操作符在解构时创建一个新数组,包含未被赋值的剩余元素 |
默认值 | 为解构赋值提供默认值 | 防止解构赋值时由于缺少元素或元素为undefined而导致错误或缺失 | 在解构时,如果目标元素为undefined,则使用默认值代替 |
6.2 对象解构
-
对象解构的使用频率会更高一些,这主要来自对象的键值对解构所带来的可操作性更强,延伸出的用法也更多更自由
-
基础使用
和设置默认值使用
差不多,但在对象解构当中是没有顺序的,是完全针对key(键)去解构对应的value(值)的 -
而且在对变量进行解构的时候,除了设置默认值的做法,还可以进行重命名
-
var obj = {name:"小余",age:18,height:1.75}
//对象的解构(不使用解构方式)
// var name = obj.name
// var age = obj.age
// var height = obj.height
//1.对象使用解构方式(基本使用),使用{}
var {name,age,height} = obj
console.log(name,age,height);//小余 18 1.75
//2.顺序问题:对象的解构是没有顺序的,根据key解构,也就是说解构的key是跟对象里面的键一一对应的。要是写错了,可就undefined了,不需要按顺序就意味着我们不用像数组解构那样要给中间不想要的属性留出空位了
var {age,height,name} = obj
console.log(age,height,name);//18 1.75 小余
//3.对变量进行重命名(这里对身高height部分进行重命名),然后我们在使用的时候,就可以使用新的命名了
var {age,height:xiaoyu_height,name} = obj
console.log(xiaoyu_height);//1.75
//4.默认值
var {
name,
age,
height,
address="福建"
} = obj
console.log(name,age,height,address);//小余 18 1.75 福建,我们对address设置了默认值为福建
-
这里的重命名单独进行分析,通过Babel的转化兼容,可以看出这里的重命名是直接替换了一个变量名,而非别名的方式
-
这意味着我们
height:xiaoyu_heigh
解构出来后的,height是用不了的,实际只有重命名的xiaoyu_heigh
。如果强行打印height属性就会报错 -
通过这种形式,其实也意味着将数据解出来的同时,是用新的变量进行承接,只不过变量名需要和对象的
键
相同才能进行解构出来,这是解构生效的前置条件 -
同时说明了解构出来的内容和他的对象来源已经不存在瓜葛了,解构出来的内容和对象属性一样,但属于两个不同的个体,不需要担心对该内容的改变影响到原有数据(数据不可变性)
-
var obj = {name:"小余",age:18,height:1.75}
var {age,height:xiaoyu_height,name} = obj
//重命名约等于(注意height的重命名):
var age = obj.age,xiaoyu_height = obj.height,name = obj.name;
-
在对象解构当中,等于号是用来设置默认值的,冒号是用来设置重命名的。这种区分方式较为直观,因为数组解构设置默认值也是同等方式,能够进行对照,且数组本身不存在重命名这种需求,可以轻易的推导出来
-
设置重命名这种方式是很有意义的,能够将数据设置得更为可读直观
-
这种方式在进行数据请求的时候尤为有用,因为有时候后端返回的数据结构或者对应属性名不满足我们的需求时,可以进行对应的处理
-
-
设置默认值和设置重命名是可以结合使用的,形成复合增益效果
var obj = {name:"小余",age:18}
var {age,name:name1='coderwhy'} = obj
//重命名约等于(注意name的重命名):
var age = obj.age,_obj$name = obj.name,name1 = _obj$name === void 0 ? "coderwhy" : _obj$name;
-
以上这些内容都是基础使用,在这一方面的对象解构是和数组解构差不多的
-
但还需要重复解构的主要的目的是
将复杂数据提取数据
-
所以对应的应用场景,存在于大部分能够用到复杂数据的地方,数组属于同质类型数据,所以我们很少会去精细化操作内部的某些内容。对象则不同
-
-
这个需要说到JS当中的数据类型,主要分为两类:原始类型(Primitive types)和对象类型(Object types)
-
原始类型的数据不包含任何方法,它们是最基本的数据类型,包含了Undefined、Null、Boolean、Number、String、Symbol(ES6 新增)、BigInt(ES11新增)
-
除了这些原始数据类型外,JavaScript 中的任何其他值都是对象(Object),包括:对象、函数、数组、日期、正则表达式,Map Set等
-
-
从这点来说,数组只是对象的一种特殊表达形式,其中的索引作为属性键来存储数据
-
并且我们如果不采用push方法,数组的索引也可以是非数字,在这种情况下,和对象的区别已经非常相似了
-
这让我们能够联想到,数组是需要存储固定的value数据,像对象这种需要进行确认
键名
的,用在同质数据上是很没必要的一种情况,因为同类型的内容不需要使用键
去区分,更多考虑的是"数量"问题,这也是数组索引采用数字的原因 -
从性能效率上来收,数组的设计是能够优化元素的存取效率,通过连续的内存布局和简单的索引计算来快速访问数据
-
var myArray = ["XiaoYu", "coderwhy", "JS高级"];
console.log(myArray[0]); // 访问第一个元素,输出 "XiaoYu"
console.log(myArray["0"]); // 也可以使用字符串形式的索引访问,输出 "XiaoYu"
// 数组的属性访问
console.log(myArray.length); // 输出 3,因为数组有三个元素
// 增加一个非数字键的属性
myArray["name"] = "coder";
console.log(myArray.name); // 输出 "coder"
// 查看数组对象的属性
console.log(Object.keys(myArray)); // 输出 ["0", "1", "2", "name"]
-
通过前面这些论述,可以清晰的知道,数组的应用场景比较固定,对象的使用场景更丰富。但数组在对应的使用领域,比对象的性能更高效,也更简洁
-
在对象中的解构方式和在函数的使用方式进行结合,使用的灵活度会指数型提升,且在类似循环、if判定等多种情况在也可以进行使用
-
这每一种使用方式都有对应的使用场景,在React中,这些使用方式尤为重要,因为非常契合React的自由特性,所以使用频率非常高
-
// 1.定义一个函数,其参数直接解构对象
function displayUserInfo({ name, age, job = '未知职业' }) {
console.log(`姓名: ${name}, 年龄: ${age}, 职业: ${job}`);
}
// 调用函数,传入对象
displayUserInfo({ name: '小余', age: 28, job: '前端开发' });
displayUserInfo({ name: 'coderwhy', age: 30 }); // 使用默认职业值
//基础数据
const user = {
id: 1,
name: '小余',
contact: {
email: '1045098807@qq.com',
phone: '1234567890'
},
address: {
city: '厦门',
zip: '361000'
}
};
// 2. 解构嵌套对象:从深层结构中一次性提取多个属性
const { name, contact: { email }, address: { city } } = user;
console.log(`姓名: ${name}, 邮箱: ${email}, 城市: ${city}`);
//基础数据
function getUser() {
return {
id: 1,
name: '小余',
age: 28,
job: '前端开发'
};
}
// 3. 直接从函数返回值解构
const { name, job } = getUser();
console.log(`姓名: ${name}, 职业: ${job}`);
// 4. 假设有一个模块 mathFunctions.js,导出多个数学函数(后面会学习到)
import { add, multiply } from './mathFunctions';
console.log(add(2, 3)); // 输出:5
console.log(multiply(2, 3)); // 输出:6
// 基础数据
const users = [
{ id: 1, name: '小余', age: 28 },
{ id: 2, name: 'coderwhy', age: 30 }
];
// 5. 在for...of循环中使用解构
for (const { id, name } of users) {
console.log(`ID: ${id}, Name: ${name}`);
}
//基础数据
const person = { name: '小余', age: 28, job: '前端开发' };
// 6. 提取 name,其余属性放入 rest 对象
const { age, ...rest } = person;
console.log(age); // 输出:28
console.log(rest); // 输出:{ name: '小余', job: '前端开发' }
//基础数据
const settings = { darkMode: true, fontSize: 16 };
// 7. 条件语句中使用解构
if ({ darkMode: true } = settings) {
console.log('Hello 欢迎学习JS高级');
}
-
而数组解构虽然没有对象解构那么丰富,但在基础使用之外也有部分对应使用场景,例如交换值
-
这个案例在初学JS的时候非常常见,不过那种方式是在a、b两值交换时,通过中介变量c进行交替。在复杂度上其实会比解构赋值的交换变量来得繁琐,但易于理解
-
这种使用方式并不是黑魔法,遵循的是JS规范而非钻取漏洞去形成复杂效果
-
let a = 1;
let b = 2;
// 使用解构赋值进行变量交换
[a, b] = [b, a];
console.log(a, b); // 输出:2 1
-
这些灵活度会带来一定的学习难度,其中难度并非是使用难度,而是适用的判断,在什么场景下使用最合适的方式
-
错误的使用方式,会带来更多的问题
-
像这种问题是非常常见的,因此经常会有人讨论
过渡封装
的问题,在什么情况下才去封装?难度不在封装,而在于封装的时机,这些方式也是如此,因此在本章节中,我们在进行讲解数据解构和对象解构的时候,我们才会详细说明数组和对象之间的区别
-