共计 4435 个字符,预计需要花费 12 分钟才能阅读完成。
对于搞前端的小伙伴来说,不论是老手还是老鸟,我想对于原型应该都被折腾过,总是云里雾里的感觉,要是原型都没搞明确,你还好意思说你是前端攻城狮?
对于对象
当一说到面向对象 (Object-Oriented OO) 时,你第一反馈必定想到类、对象、接口实现等概念,那咱们这里为啥已上来就说对象呢?因为 ECMAScript 里没有类,另外因为 ECMAScript 中的函数没有签名,所以也没有接口。
ECMAScript-262 中对象定义为:“无序属性的汇合,其属性能够是根本值、对象或者函数”。因而从数据结构的角度,能够把对象看成散列表(Hash Table)。
对象分类
从对象的创立形式上能够把对象分成:内置对象、宿主对象、自定义对象三大类。对于对象分类具体点这里。
特地须要强调的是,除了 number、string、boolean、null、undefined、symbol 这 6 中根本类型外,其它通通都是对象 (援用类型),包含函数, 所有的函数都是对象,反之则不成立。
对象和函数的关系
对象的创立
后面说过,ECMAScript 中没有类,那怎么创建对象呢?
对象字面量
// 形式一:对象字面量
var zhangsan = {
type: "人类",
name: "张三",
age: 18,
greeting: function() {console.log(`hello I'am ${this.name}`);
}
};
zhangsan.greeting(); // "hello I'am 张三 "
该形式次要有一下几个问题:
- 当要创立多个变量的时候,不得不写大量反复代码;
- 每个实例都会持有一个 greeting 函数,但实际上性能都一样,没有复用,浪费资源;
- 创立所有“人类 ”(type=” 人类 ”)的实例,type 的值都是一样的,然而每个实例还是持有一个独立的正本;
- 创立实例无奈辨认类型(也就是说创立的实例具体是啥类型不晓得,只晓得它是 Object 的实例)。
工厂模式
// 形式二:工厂模式
function createPerson (name, age) {var p = new Object();
p.type = "人类";
p.name = name;
p.age = age;
p.greeting = greeting;
return p;
}
var lisi = createPerson ("李四", 20);
lisi.greeting(); // "hello I'am 李四 "
function greeting () {console.log(`hello I'am ${this.name}`);
}
形式二尽管进行了封装,防止了创立时大量反复的代码,也通过把 greeting 抽离到全局作用域而解决了多个实例持有多个 greeting 正本的问题,但同时也给全局空间引入了一个只有该类型实例才会援用的函数,净化了全局空间;最初它也米有解决对象辨认问题。
// 形式三:构造函数
function Person (name, age) {
this.type = "人类";
this.name = name;
this.age = age;
this.greeting = greeting;
}
var wangwu = new Person("王五", 24); // wangwu instanceof Person === true
wangwu.greeting(); // "hello I'am 王五 "
function greeting () {console.log(`hello I'am ${this.name}`);
}
这个形式近乎完满了,解决了对象辨认问题,然而任然没有解决共享函数净化全局空间的问题;为了解决这个问题,上面请出咱们的配角 prototype(原型)。
原型 & 原型链
终于切入正题了,要解决下面形式三面临的问题,就要有一个属于构造函数专有(不必定义到全局净化全局空间),可能为构造函数创立的所有对象实例所共享的对象。这个对象就是原型(或称为原型对象)。
什么是原型(prototype)
默认状况下,任何函数都有一个属性 prototype
,它是一个指针,指向一个对象(原型对象),原型对象的用处是蕴含特定类型实例所共享的属性和办法,默认原型对象只有一个constructor
属性,咱们能够给它定义更多属性和办法。
// 形式四:原型法
function Person (name, age) {
this.name = name;
this.age = age;
}
Person.prototype.type = "人类";
Person.prototype.greeting = function () {console.log(`hello I'am ${this.name}`);
};
var wangwu = new Person("王五", 24); // wangwu instanceof Person === true
wangwu.greeting(); // "hello I'am 王五 "
那下面的实例 wangwu 是怎么找到原型对象里定义的 greeting 的呢?起因是所有的对象都有一个外部指针,指向实例构造函数的原型对象,ECMAScript-262 第 5 版中称为 [[Prototype]]
,尽管规范并没有定义怎么拜访这个外部指针,然而 Firefox、Safari、Chrome 在每个对象上都反对一个指向雷同、名为__proto__
指针属性。
在 chrome console 里查看 wangwu 的属性如下图:
[站外图片上传中 …(image-1d07-1644313611733)]
原型链查找
当对象实例拜访某个属性或调用某个办法时,首先在自有属性里找,找到则返回值或发动调用,没有则沿着 __proto__
的指向往上找,直到最初查到 Object.prototype, 任然没有查到,即终止并报错。
对象实例、构造函数、构造函数的原型对象这三者的关系如下图:
上图中红色的门路及为查找方向,这条有 __proto__
指针串起来的链即为 原型链 (prototype chain)
。 原型链的实质是一串程序指向原型对象的指针列表。
原型的动态性
因为对象实例的 __proto__
仅仅是一个指向原型对象的指针,因而对原型对象的批改立刻能够在实例上体现进去,哪怕这个实例在批改原型之前创立的:
Person.prototype.work = function () {console.log('work function');
}
// 这里的 wangwu 是下面创立的实例, 给原型减少 work 办法后,能够立刻调用
wangwu.work(); // "work function"
然而如果重写整个原型对象后,相当于为构造函数指定了新的原型对象,而已创立的实例的 __proto__
依然指向旧原型对象,因而拜访不到在新原型里定义的办法:
Person.prototype = {work: function () {console.log('work function');
}
};
// 报错
wangwu.work(); // "wangwu.work is not a function"
// 在批改原型对象后创立的实例,因为获取到的__proto__属性是指向新原型的,因而不会报错
var sanma = new Person('三毛', 30);
// 能够欢快的“工作”sanma.work(); // "work function"
[图片上传失败 …(image-b3adea-1644313611733)]
笼罩整个原型对象后,相当于下面图中原来的 prototype 指向被切断了,指向了新的原型。
小结一下
默认状况下(因为原型对象实际上是可写的,因而能够被扭转):
- 任何函数都有一个指向其原型对象的指针属性 prototype;
- 任何对象实例都有一个指向其构造函数原型对象的外部指针
[[Prototype]](__proto__)
;- 原型对象也是对象,因而也有
__proto__
(例如上图中指向 Object.prototype 那个);- 对象实例的
__proto__
指针指向构造函数的原型对象:wangwu.__proto__ === Person.prototype
;- 原型对象的
constructor
属性指向构造函数:Person.prototype.constructor === Person
;- 构造函数和对象实例没有间接分割,仅仅是都有一个指针属性指向同一个原型对象。
对象实例辨认(检测)
咱们晓得,对于 number、string、boolean、undefined、function 这几种类型值,能够通过 typeof 操作符简略辨别,然而对于除 function 外的援用类型实例和 null,typeof 都返回 ”object”, 然而再往细了辨别,某个对象实例是神类型的实例,typeof 就没方法了。
instanceof 操作符
要辨认具体的对象实例类型,就要用到 instanceof 操作符,格局为 instance instanceof Func
, instance 是待检测实例对象,Func 是一个构造函数,有了下面原型链的了解,那 instanceof 的检测机制就简略多了,只有在 instance 的原型链上某个__proto__
指向了 Func 的原型对象,就返回 true,否则返回 false。即:
instance.__proto__...__proto__ === Func.prototype
另外也能够用 Func.prototype.isPrototypeof(instance)、Object.getPrototypeof(instance) === Func.prototype 来判断。
console.log(wangwu instanceof Person); // true
console.log(wangwu instanceof Object); // true
console.log(Person.prototype.isPrototypeof(wangwu)); // true
console.log(Object.prototype.isPrototypeof(wangwu)); // true
console.log(Object.getPrototypeof(wangwu) === Person.prototype); // true
console.log(Object.getPrototypeof(wangwu) === Object.prototype); // false, 因为 getPrototypeof 函数只返回实例原型,而不会返回原型链上的其它原型
原型继承
了解了原型,那原型继承就很简略了,须要扩大的类指向父类的原型即可,上面是简略的原型继承实现:
function Men() {//}
Men.prototype = Object.create(Person.prototype);
Men.prototype.constructor = Men;
特地留神,给 prototype 属性赋值后,Men.prototype.constructor 指向了 Person,因而必须再把它指回 Men。