类的概念
宏观
- 宏观上,我们可以定义:类是一种面向对象设计模式,比如迭代器模式、观察者模式、工厂模式、单例模式...
- 很多语言提供了面向类的原生语法,ES6也提供了类似语法,但是这并不意味着JavaScript实际上有类JavaScript
- JavaScript只有对象,并不存在可以被实例化的“类"。对象会通过原型链被委托关联起来,模拟类的行为。
JavaScript微观
- 类:一组对象从一个原型对象继承属性
- 类的标识是原型对象,而不是构造函数,两个构造函数的prototype属性可能指向同一个对象
创建类的方法
- 定义一个原型对象,然后反复用Object.create()创建继承它的对象
- 使用构造函数,定义其prototype属性,new调用构造器-构造函数的prototype属性将被用作新对象的原型【构造器模式】
- ES6的类语法,实际是构造函数prototype的语法糖
prototype
ES6的类语法实际是构造函数prototype的语法糖,接下来围绕-最基础的类定义机制 展开,即上文提到的创建类的方法2,联系下文-构造器模式进行理解
【一】、几乎所有对象都有原型,但是只有少数对象有prototype(函数对象/构造函数),有prototype属性的对象为所有其他对象定义了原型。
- Object.prototype没有原型
- 对象字面量和new Object创建的对象的原型对象是Object.prototype
- Object.create(null)新创建的对象也没有原型,没有继承任何方法。
p.s注意:实际编程中,不要直接把对象传给库函数,防止对象被第三方库意外修改,而是传递一个继承对象,可以使用Object.create()生成
function objectCreate(o){//object.create的单参数
function F(){}
F.prototype=o;//
return new F();//传入一个原型,返回一个根据原型创建的对象实例
}
【二】、有原型(一部分浏览器实现为__proto__)的一定是对象,有prototype原型的一定是函数,普通函数既有__proto__又有prototype(箭头函数、异步函数、生成器函数、Function.prototype.bind()除外)。
- 每个普通函数,自动拥有一个prototype对象属性,支持手动赋值
- prototype对象有一个不可枚举的constructor属性指向与之关联的构造函数。
- 在创建函数时,也会创建其prototype对象,同时自动给prototype.constructor属性赋值。
- 只有prototype对象才实际拥有constructor属性,其他对象都是通过原型链间接获得。
注意:如果使用对象字面量重写原型对象,意味着constructor属性会被改写指向Object,这时就不能通过constructor来识别类型。
const F = function () {};
const proto = F.prototype;
const constr = proto.constructor;
constr === F; // true
const obj = new F();
obj.constructor === obj.__proto__.constructor; // true
【三】、_实例的原型指向,作为对象的内部属性是不能直接访问的,但是为了查看对象原型:
- Firefox和Chrome内核的JavaScript引擎中提供了这个__proto__非标准访问器
- ECMA新标准引入了标准对象原型访问器Object.getPrototype(object)、Object.setPrototype(object)、
JavaScript的一些早期浏览器实现了通过__proto__暴露对象的prototype对象的读写,虽然被废弃但是存有代码,所以ES标准要求所有浏览器的JavaScript实现必须支持它。
让我们实验一下
function A(){};
let a=new A();
console.log(A.prototype.constructor);//ƒ A(){}
console.log(a.constructor);//ƒ A(){}
console.log(a instanceof A);//true
修改实例的原型对象,相当于切断与构造器的连接,重新定义a.constructor
a.__proto__ = {};//修改实例的隐式原型对象,相当于切断与构造器的连接
console.log( A.prototype.constructor);//ƒ A(){}
console.log(a.constructor);//ƒ Object() { [native code] }
console.log(a instanceof A);//false
又或者修改构造器的原型,注意这里是构造器方法,constructor会改变
A.prototype={
id:7
};//只影响后面的,不影响前面已经生成的
console.log(A.prototype.constructor);//ƒ Object() { [native code] }
console.log(a.constructor);//ƒ A(){} 前面已经生成的不受影响【脚本语言】
console.log(a instanceof A);//false
let aa=new A();
console.log(aa.constructor);//ƒ Object() { [native code] }之后实例化的生效
console.log(aa instanceof A);//true
r instanceof Range 并非检查r是否通过Range构造函数初始化,而是检查原型链上是否有Range.prototype 区别:isPrototypeOf,待思考
class-like 创建相似对象=>类
构造器模式
JavaScript中的new和面向类的语言完全不同,在JavaScript中构造函数只是一些使用new操作符时被调用的普通函数
构造函数不能直接访问prototype对象的属性
A.b1
new操作符调用构造函数:
- 在内存中创建一个新对象
- 新对象的原型被赋值为构造函数的prototype【仅有new调用构造器时会进行这一步】
- 构造函数中的this指向新对象
- 执行构造函数的代码,为新对象添加属性
- 如果构造函数没有返回对象,就返回新对象(参看盗用构造器模式)
//除了声明函数也可以写成表达式
function A () {
// return {b1:'in a'} // 覆盖了构造函数生成的对象,并没有获取到原型对象
this.b1 = 'in a'
}
A.prototype = {
b1:'in pro',
proKey:'only in pro',
}
const a = new A();
console.log(a.b1); // 'in a'
console.log(a.proKey); // 'only in pro'
盗用构造器模式
实例的constructor的指向是通过原型对象间接获得的
只有在new调用的时候才会自动继承prototype属性。而盗用构造器是使用apply方法借用构造器,不会继承prototype,c.constructor指向B。
function A() {
this.o = 'a';
}
A.prototype.say = function(){
console.log("hhh");
}
function B() {
A.call(this);
// return new A(); // constructor指向A
}
const c =new B();
console.log(c); // Bfunction A() {
this.o = 'a';
}
A.prototype.say = function(){
console.log("hhh");
}
function B() {
A.call(this);
// return new A(); // constructor指向A
}
const c =new B();
console.log(c); // B{o:'a'}
原型链继承
原型对象不采用构造函数默认的原型,而是另一个类的实例
function A() {
this.o = 'a';
}
A.prototype.say = function(){
console.log("hhh");
}
function B() {}
B.prototype = new A();
console.log(B.prototype.constructor); // A
const c =new B();
console.log(c);// B
console.log(c.constructor); // 指向A因为B.prototype.constructor并不指向B,而是指向A
c instanceof B; // true
c instanceof A; // true
A.prototype.isPrototypeOf(c); // true
B.prototype.isPrototypeOf(c); // true
寄生虫模式(最有效)
相比原型模式,将父类实例赋值给子类原型,寄生虫直接用父类原型创建一个原型对象,再手动绑定constructor关系
两个参数:子类构造器和父类构造器
function inheritPrototype(subT,superT){
let prototype=Object.create(superT.prototype);//创建父类原型的副本对象给子类的prototype
prototype.constructor=subT;//绑定明确的constructor
subT.prototype=prototype;//绑定
}
工厂模式
理解:类只是一种设计模式
function createPerson(name,age){
let o=new Object();//显式创建对象
o.name=name;
o.age=age;
o.sayName=function(){
console.log(this.name);
};
return o;
}
let p1=createPerson("Greg",27);
console.log(p1);
ES6 class语法
前文围绕JavaScript模拟class-like的行为,ES6中引入class(背后的实质仍然是原型和构造函数)
类的实质:使用立即执行函数【所以类声明定义都不能提升(函数声明可以提升)】在框定一个命名空间 TS官方提供的编译运行环境
- 采用对象字面量简写形式定义方法,即
a:function(){}a(){} - 如果类不需要任何初始化,可以省略constructor关键字及其方法体,解释器会隐式为你创建一个空构造函数。
class Range {
constructor (from,to) {
//添加到this上的全部都会成为实例成员
this.from = from;
this.to = to;
}
// 定义的字段不在原型上,可能位于实例或者类
static share =0;
each='ones';
//在类块中定义的所有方法都会定义在类的原型上
includes (x) {
return this.from <= x && x <= this.to;
}
// 可以使用字符串、符号或者计算的值作为键
*[Symbol.iterator]() { // 这个生成器方法让 类的对象支持迭代语法
for(let x = Math.ceil(this.from); x <= this.to; x++) {
yield x;
}
}
//静态,定义在类本身上
static locate(){
console.log("class",this);
}
}
//为已有类的原型添加一个 方法
Range.prototype.say=function (){
console.log('hh');
}
const r = new Range(0,4);
for (const p of r) {
console.log(p);
}
类内部定义的function在原型上,但是变量在实例上?
ES6标准的类,只允许创建方法(获取方法、设置方法、生成器)和静态方法,还没有定义字段的语法。如果想要在类实例上定义字段(实例属性),必须在方法中进行。静态字段,必须在类体之外,在定义类之后定义。
但是扩展类语法在发展,支持定义实例和静态字段的标准化在发展,浏览器逐渐支持定义公有实例字段,且定义公有实例字段的语法在使用React和Babel转义的JavaScript程序、typescript中非常常用。
子类
- 只能在派生类的构造函数和静态方法中使用super
- 在静态方法中使用super调用父类的静态方法。
- 不能单独引用关键字super,用么调用构造函数super();要么引用静态方法
- 构造函数,只有先调用super() ,才可以使用this
- 如果在派生类中显示定义了构造器,要么必须调用super(),要么返回一个对象
class SuperT{
identify(id){
console.log(id,this);//this的值反映调用相应方法的实例或者}
s类
tatic identifyClass(id){
console.log(id,this);//this的值反映调用相应方法的实例或者类
}
}
//继承
class SubT extends SuperT{
constructor(){
//调用父类构造器
super(); // 父类构造器内部可以通过new.target 获得 子类构造器
//必须在调用父类构造器之后才能引用this,否则抛出ReferenceError
console.log('subT constructor',this);
}
}
let son=new SubT();
let father=new SuperT();
son.identify('son');//son SubT{}
father.identify('father');//father SuperT{}
SuperT.identifyClass('父类');//父类 class SuperT{}
SubT.identifyClass("子类");//子类 class SubT{}
JavaScript提倡委托来关联对象而不是继承,推荐在自己的类中创建另一个类的实例,然后在需要的时候委托实例去完成。
类对生成器和迭代器的支持
class P{
//在原型上定义生成器方法
*createNameIterator(){
yield 'n1';
yield 'n2';
}
//也可以在类上定义 static *name
}//
let iter=new P().createNameIterator();
console.log(iter.next().value);//n1
console.log(iter.next().value);//n2
JavaScript 对象
描述对象的属性
属性分为:数据属性、访问器属性-访问器属性不包含数据值,通常包含getter、setter函数
采用内部特性来描述属性的特征, 属性描述符是ES5开始支持的语法
- Configurable是否可以通过delete删除、是否可以修改特性,是否更改为数据/访问器属性、默认true
- Enumerable是否可以通过for-in循环返回,默认true
- 【数据特有】Writable
- 【数据特有】Value
- 【访问器特有】Get,默认undefined
- 【访问器特有】Set,默认undefined
Object.getOwnPropertyDescriptor()方法可以获取指定属性的属性描述符(只可以获取自己的属性,即实例不能通过调用该方法知道自己的原型属性)
修改对象的属性
访问器属性不能直接定义,在支持Object.defineProperty()的浏览器中可以通过其修改特性,还可以多个描述符一次性定义多个属性:
let person={};
Object.defineProperty(person,"name",{
writable:false,
value:"hh"
});
//定义多个属性
let book={};
Object.defineProperty(book,{
year:{//数据属性
value:2017
},
edition:{//访问器属性
get(){
return this.year;
},
set(newyear){
if(newyear>2017){
this.year=newyear;
}}
}
})
访问对象的属性
- in操作符返回,是否可以通过对象访问属性(原型、实例)// 一个存在但是值为undefined的值 使用操作符in,返回true
- 实例名.hasOwnPrototype("属性名“);//检测属性是否属于实例
- 属性 !==undefined 注意: 属性 !==undefined 不能区别undefined和null,但是in可以
- Object.keys(对象名);//可枚举的实例属性名称
- Object.getOwnPrototypeNames(对象名);//包括不可枚举的实例属性名称
- Object.getOwnPrototypeNames-Symbol(对象名);//符号为键的属性
- Object.values()、Object.entries()两个静态方法接收一个对象,返回他们实例对象上的内容的数组
修改对象的原型
Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作)
function A() {
this.o = 'a';
}
A.prototype.say = function(){
console.log("hhh");
}
Object.setPrototypeOf(A.prototype, {addF(){
console.log("new");
}});
function B() {
return new A();
}
const c =new B();
console.log(c);
注意区别重写和修改、新增
Sub.prototype=new Super();//原型,继承方法 Sub.prototype.sayAge=function(){ console.log(this.age); };//原型链上的方法共享,新增
原型链的属性设置和屏蔽
给一个对象设置属性,原型链上有同名的属性时候,服从原型对象的属性的描述符 只有当writable:true才会在当前对象上添加一个新的屏蔽属性(hasOwnProperty返回true)
我们尽量避免在原型链的不同级别中使用相同的命名