JS高级程序设计——阅读笔记六
本系列博客主要是面向自己创作,实属自己的读书笔记,注重记录一些重点,并摘录引用一些大佬对于部分知识点的解释,帮助自己翻阅和理解,一定要配合原著食用。
第八章 对象、类与面向对象编程
其实这部分内容算是比较常用的,熟悉的知识应该会很多,暂时选一些不太熟悉的内容来写。首先要明确一个问题,以前可能对这两个两个概念没有如此明确的区分,对象是一个无序集合,但是无序集合是可迭代的。
8.1 理解对象
8.1.1 属性类型
属性分为两种:数据属性和访问器属性(好像不太常用,但是对JS对象深入理解还是要搞明白这些概念)
数据属性
这四个被称为特性
- [[Configurable]] 能否通过delete删除属性重新定义,修改属性特性,把属性修改为访问器属性
- [[Enumerable]] 能否通过for-in循环,默认都是true
- [[Writable]] 能否修改属性值
- [[Value]] 属性实际的值
修改特性的方法:
var person={}
Object.defineProperty(person,"name",{
configurable:false,
writable:false,
value:"nn"
})
delete person.name;
由于configurable:false
的存在,所以非严格模式下delete
不会生效,严格模式会报错。
PS.一个属性被定义为不可配置以后就不能再变回可配置了。
访问器属性
访问器也有四个特性,与数据属性有些类似:
- [[Configurable]] 能否通过delete删除属性重新定义,修改属性特性,把属性修改为访问器属性
- [[Enumerable]] 能否通过for-in循环,默认都是true
- [[Get]] 获取函数,默认值为undefined
- [[Set]] 设置函数,默认值为undefined
但是访问器属性不包含数据值,他有getter 读取访问器属性和setter 写入访问器属性。
定义和修改访问器属性
var book={
_year:2004,
edition:1
};
Object.defineProperty(book,"year",{
get:function(){
return this._year;
},
set:function(value){
if(value>2004){
this._year=value;
this.edition +=value-2004;
}
}
})
PS.这里注意一个规则:属性名中的下划线常用来表示该属性并不希望在对象方法的外部被访问。
8.1.4 合并对象
Object.assign
方法接受一个目标对象和一个或多个源对象作为参数,返回一个合并后的对象。
let dest = {};
let src = {id:"src",name:'srcname'};
let bbb = {id:"bbb"}
let result = Object.assign(dest,src,bbb);
console.log(src);//{id: "src", name: "srcname"}
console.log(dest);//{id: "bbb", name: "srcname"}
console.log(bbb);//{id: "bbb"}
console.log(result);//{id: "bbb", name: "srcname"}
这里有个小疑惑,这样做得到的对象result和对象dest完全一样吗?是否也是一种深拷贝方式?
console.log(result === dest);//true
虽然对于这两个对象来说是全等的,但是后文说明了,这里需要着重注意:
Object.assign
实际上是对每个源对象执行了浅复制,如果多个源对象都有相同的属性,则使用最后一个复制的值。而且不能在两个对象间转移获取函数和设置函数。
PS.这里需要注意的是,如果属性是访问器属性,那么这个方法会使用源对象上的[[Get]]以获取属性值,然后再在目标对象上的[[Set]]设置属性值,这里做个小实验:考虑到如果多个源对象都有相同的属性,则使用最后一个复制的值,如果[[Set]]设置的属性也是重复,那么是否依然会覆盖?
let dest = {id:"aaa", set a(val){ this.id = val; }};
let src = {
id: "src",
get a() {
return "foo";
}
}
Object.assign(dest, src)
console.log(dest);
可以看出最后会使用源对象上的[[Get]]以获取属性值,然后再在目标对象上的[[Set]]设置属性值,就算前面也有id属性,也会进行覆盖。
8.1.6 增强的对象语法
属性值简写
属性和变量名相似得时候可以只用变量名简写
可计算属性
可以在对象字面量中完成动态属性赋值
简写方法名
其实我们平时开发都是使用这种简写的方式,不需要过多赘述
8.1.7 对象解构
解构赋值也是非常常见的操作,注意事项有两点:
- 如果想让变量直接使用属性的名称,就可以使用简写的语法
- 赋值的时候可以忽略某些属性也可以引用不存在的属性,这时不存在的属性就是undefined
解构在内部使用函数ToObject()把源数据结构转换为对象,这意味着在对象解构的上下文,原始值会被当做对象。(这段话很难直接弄明白,但是在5.3.3小节中我们区分过原始值和对象的区别,用下列代码来观察一下)
在控制台上我们可以清楚的看到原始值字符串和new的字符串是有明显差异的,通过对原始值解构,原始值变成对象,进而拥有了length属性,可以解构出来。
嵌套解构
解构也可以用来复制对象属性,但是复制是浅拷贝。
部分解构
如果一个解构表达式涉及多个赋值,开始的赋值成功而后面的赋值除左,整个解构赋值只会完成一部分。
8.2 创建对象
8.2.2 工厂模式
举例说明
function createPerson(name,job){
var o-new Object();
o.name=name;
o.job=job;
return o;
}
let person1 = new createPerson("aaa","bbb")
let person2 = new createPerson("ccc","ddd")
该方法可以解决创建多个类似对象的问题,但是不能解决新创建的对象类型的问题。
8.2.3 构造函数模式
要注意使用new操作符会执行何种操作:
- 在内存中创建一个新对象
- 新对象内部[[Prototype]]特性被赋值构造函数的prototype属性
- 构造函数内部this被赋值新对象
- 添加对象属性
- 如果构造函数返沪非空对戏,则返回该对象,否则返回刚创建的新对象
任何函数只要用new操作符就是构造函数,不适用new操作符调用的函数就是普通函数。
再调用一个函数而没有明确设置this值的情况下(没有作为对象的方法或call\apply调用),this始终指向Global对象。
两个问题:
- 构造函数内部的方法再创建多个对象的时候会定义多个功能相同的Function实例。
- 将方法定义在全局可以解决上述问题,但是又会导致自定义类型引用的代码不够内聚。
所以产生了原型模式:
8.2.4 原型模式
理解原型的本质
其实构造函数的prototype属性和constructor属性是循环引用的,我们可以打印看一下
function Person() {}
Person.prototype
就在控制台观察可以发现,是点不到头的
__proto__属性可以访问对象的原型,最后终止于Object,如图所示
console.log(person1.__proto__ === Person.prototype); //true
console.log(person1.__proto__.constructor === Person); //true
构造函数通过prototype属性链接到原型对象,同一个构造函数创建的两个实例共享同一个原型对象。
使用Object.getPrototypeOf()
可以方便的取得一个对象的原型。
注意!!!使用Object.setPrototypeOf()
会造成严重的性能下降
in 与 hasOwnProperty
只要通过对象可以访问,in就返回true,而hasOwnProperty只有属性存在于实例上
8.2.5 对象迭代
这里觉得在数据处理的时候Object.values()和Object.entries()是比较常用的两个方法,接受一个对象返回内容数组。
注意,这两个方法执行的都是浅复制,而且符号属性会被忽略。
实际开发中通常不单独使用原型模式
8.3 原型链
每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。
默认情况下,所有引用类型都继承自Object。
instanceof操作符可以检测实例的原型链中是否出现过相应构造函数。
isprototypeOf()方法可以检测原型链中是否包含这个原型。
原型链问题
- 原型中包含引用值的时候,该引用值会在所有实例间共享。
- 子类型在实例化时不能给父类型的构造函数传参。
8.3.2 盗用构造函数
基本思路:在子类型的构造函数中调用父类型的构造函数,以新创建的对象为上下文执行构造函数。
盗用构造函数基本上也不能单独使用,作为了解。
8.3.3 组合继承
这里就用一个例子简单说明:
function Aaa(name){
this.name = name;
this.colors = ["red","blue","green"];
}
Aaa.prototype.sayName = function(){
console.log(this.name);
};
function Bbb(name,age){
Aaa.call(this,name);
this.age = age;
}
Bbb.prototype = new Aaa();
Bbb.prototype.sayAge = function(){
console.log(this.age);
}
let instance1 = new Bbb("Nicholas",29);
instance1.colors.push("black");
console.log(instance1.colors);//["red", "blue", "green", "black"]
instance1.sayName();//Nicholas
instance1.sayAge();//29
let instance2 = new Bbb("Greg",27);
console.log(instance2.colors);//["red", "blue", "green"]
instance2.sayName();//Greg
instance2.sayAge();//27
PS.关于各类原型链实现继承的方式,例如原型式继承、寄生式继承、寄生式组合继承来说,寄生式组合继承式引用类型继承的最佳方式,但却不是最为有效的方式,引文各种策略都有自己的问题,也存在着各种妥协,所以还是主要将ES6中的类作为最需要掌握的继承方式。
8.4 类
8.4.1 类定义
其中需要注意的是,类表达式的名称是可选的。在赋值给变量后,可以通过name属性取得类表达式的名称字符串,但是不能再类表达式作用域外部访问这个标识符。
8.4.2 类构造函数
构造函数的调用时机就是使用new操作符进行实例化的时候。
注意操作内容:
1)在内存中创建新对象
2)实例的[[Prototype]]指向构造函数的prototype属性
3)this指向新对象
4)执行构造函数内部代码
5)如果构造函数返回非空对象则返回该对象,否则返回新创建对象。
ps.调用类构造函数必须使用new操作符
8.4.3 实例、原型和类成员(react中this绑定问题)
实例间共享方法需要在类构造函数中定义,添加到this的所有内容都会存在于不同的实例上
其实写到这里,我不难联想到react中this指向的问题,于是又重新梳理了一遍
react中this指向的问题来源于严格模式,展开来说:
- 我们在创建虚拟 DOM的时候都会用到JSX。
- JSX 语法是不被 webpack 识别的,webpack 默认只能处理 .js 后缀名的文件,所以需要借助 Babel 这个
JavaScript 编译器,而 babel 开启了严格模式。 - this 本质上就是指向它的调用者,this 是在函数运行时才绑定,JS 里边普通函数都是 window 调用的,所以指向
window,开启了严格模式之后是 undefined。 - 在 JSX 中传递的事件不是一个字符串(在原生 JS 的中监听事件,采用的是回调函数的形式,在Vue中给监听事件传递的是字符串变量),而是一个函数(如上面的:onClick={this.speak}),此时onClick即是中间变量,最终是由React调用该函数,而因为开启了严格模式的缘故,this 是undefined,所以处理函数中的this指向会丢失。
这才造成react中this指向的问题。
指向实例的this:
- 类式组件里面的构造器里面的this是指向实例对象的。
- render 函数里面的 this是指向实例的。
性能差异:
解决this指向问题的方式有两种,分别是bind 和箭头函数,二者解决问题的结果没有差异,但是性能上却有着区别,这里先说明结论,bins方式绑定this的性能要远高于箭头函数。
在组件实例化后,函数只是一个普通变量,实际的方法体是保存在原型对象上的,被所有实例共享。反观箭头函数声明的函数,实例化后,实例要单独分配一块内存去存储这个箭头函数。
详细来说,使用箭头函数来解决性能会比较低,因为箭头函数不是方法,它们是匿名函数表达式,所以将它们添加到类中的唯一方法是赋值给属性。前面介绍ES6的类的时候可以看出来,ES 类以完全不同的方式处理方法和属性。
方法被添加到类的原型中,而不是每个实例定义一次。
类属性语法是为相同的属性分配给每一个实例的语法糖,实际上会在 constructor里面这样实现:
constructor(){
super()
this.speak = () => {console.log(this)}
}
这意味着新实例被创建时,函数就会被重新定义,丢失了JS实例共享原型方法的优势。而bind方法,只是在生成实例时多了一步 bind 操作,在效率与内存占用上都有极大的优势。
获取和设置访问器
之前用的比较少,模板记一下:
class Person{
set name(newName){
this.name_ = newName;
}
get name(){
return this.name_;
}
}
let p = new Person();
p.name = 'Jake';
console.log(p.name);//Jake
可以联系第九章中的反射来理解get和set分别在什么时候会调用。
静态类方法
每个类上只能有一个静态成员。
class Person{
constructor(){
this.locate = () => console.log('instance',this);
}
locate(){
console.log('prototype',this);
}
static locate(){
console.log("class",this);
}
}
let p = new Person();
p.locate();//instance Person {locate: ƒ}
Person.prototype.locate();//prototype {constructor: ƒ, locate: ƒ}
Person.locate();//class class Person{……}
8.4.4 继承
使用extends关键字可以达到继承的效果,不仅可以继承一个类,还可以继承普通的构造函数。
super()
派生类通过super关键字引用原型,这个关键字只能再派生类中使用,而且仅限于类构造函数、实例方法和静态方法内部。
在类构造函数中使用super可以调用父类构造函数。
在类构造函数中,不能再调用super()之前引用this。