原型与类
简述原型
-
原型:每一个 对象(null 除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性,其实就是
prototype对象。 在对象中可以通过__proto__访问原型,在构造函数中可以通过prototype访问。二者指向相同。 -
原型链:由相互关联的原型组成的链状结构就是原型链。
- 当前实例使用了自己没有的变量或者属性,就会顺着原型链查找。
instanceOf的原理是通过查找原型链,如果目标对象在其原型链上,则返回true,反之为false- 原型链是 ES 中的类
class出现前,实现继承的方式
如果能理解以下这张图,原型就没问题了
function Foo() {}
let f1 = new Foo();
let f2 = new Foo();
最后要注意,
-
所有构造器都是由
new Function而来,包括Function本身,即构造器.__proto__ === Function.prototype。至于为什么是本身,就是鸡生蛋蛋生鸡的故事了,可以理解为引擎在实现时就定义了这个规则。
-
Object与Function的关系:基于上一点,既然Object是被new Function出来的,那么Function为什么又会在Object的原型链上呢。与上一点相同,可以理解为初始的定义。Function是最顶层的构造器,Object是最顶层的对象。从原型链讲,Function继承了Object;从构造器讲,Funtion构造了Object。
组合寄生式继承
一般来说,除了class之外有六种继承方式,在这里就不报菜名了。直接分析一下历史给出的较好的方案,组合寄生式继承。
之后再将这个继承方式与ES6之后使用的类中的 extend / super 通过 babel 编译后进行对比
先来看看"菜名",组合+寄生=组合寄生式。那么它组合和寄生分别表现在哪,为什么要这么命名?
组合
组合:使用原型链对原型的属性和方法继承,使用构造函数在新建子实例的时候传递参数。即这两种操作的组合
function Parent(name) {
this.name = name
}
// 使用构造函数在新建子实例的时候传递参数。这样可以继承Parent
function Child(name, age) {
Parent.call(this, name) // 操作一
this.age = age
}
// 直接将Child的原型换成Parent的一个实例。将Child加入Parent的原型链中
// 可以使用原型上的方法和属性
Child.prototype = new Parent() // 操作二
寄生
组合中的操作二 Child.prototype = new Parent() 是可以通过寄生进行优化的。
为什么说是可以进行优化的。当 Parent 中的属性和操作很多的时候,对它进行调用,会产生一定的性能损耗(应该也没多少)
function objectCopy(prototype) {
function Fun() { };
Fun.prototype = prototype;
// 这里用空函数Fun创建实例,实现了实例obj.__proto__ === Parent.prototype 减少了性能开销
return new Fun();
}
// 以下操作效果和 Child.prototype = new Parent() 几乎等价
function inheritPrototype(child, parent) {
const prototype = objectCopy(parent.prototype); // 创建原型(增强对象)
prototype.constructor = child; // 完善原型属性
Child.prototype = prototype;
}
// 在非上古浏览器中可以用一行这个替代上面全部的代码
// Child.prototype = Object.create(parent.prototype, {
// constructor: { value: subClass },
// })
组合寄生式继承
将上述两种拼在一起大概是这样的
function objectCopy(obj) {
function Fun() {}
Fun.prototype = obj;
return new Fun();
}
function inheritPrototype(child, parent) {
const prototype = objectCopy(parent.prototype);
prototype.constructor = child;
Child.prototype = prototype;
}
// ------------组合------------
function Parent(name) {
this.name = name;
this.friends = ["rose", "lily", "tom"];
}
Parent.prototype.sayName = function () {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
// ------------寄生------------
inheritPrototype(Child, Parent);
// -------------测试样例-------------
Child.prototype.sayAge = function () {
console.log(this.age);
};
const child1 = new Child("yhd", 23);
child1.sayAge(); // 23
child1.sayName(); // yhd
child1.friends.push("jack");
console.log(child1.friends); // ["rose", "lily", "tom", "jack"]
const child2 = new Child("yl", 22);
child2.sayAge(); // 22
child2.sayName(); // yl
console.log(child2.friends); // ["rose", "lily", "tom"]
类
语法糖,本质是个函数。简化很多类似于设置原型链的操作,使书写对象更加清晰,内部工作机制就是原型操作。
基本对比
实例属性与方法
// 类
class User2 {
constructor(name) {
this.name = name;
}
// 会自动把方法放到原型中
getName() {
return this.name;
}
}
// 直接原型操作
function User1(name) {
this.name = name;
}
User1.prototype.getName = function () {
return this.name;
};
// ---------------测试用例---------------
const stu1 = new User1("zhangsan");
console.log(stu1.name); // zhangsan
console.log(stu1.getName());// zhangsan
const stu2 = new User2("lisi");
console.log(stu2.name); // lisi
console.log(stu2.getName());// lisi
静态属性和方法
// 类
class Web1 {
static url = "baidu.com";
static show() {
console.log(`im ${Web1.url}`);
}
}
// 原型
function Web2() {}
Web2.url = "niming.com";
Web2.show = function () {
console.log(`im ${Web2.url}`);
};
// -----------测试用例-----------
console.log(Web1.url); // baidu.com
Web1.show(); // im baidu.com
console.log(Web2.url); // niming.com
Web2.show(); // im niming.com
继承
类中的代码
class Animal {
constructor(name) {
this.name = name;
}
run() {
console.log(`${this.type}: ${this.name} is running`);
}
}
class Dog extends Animal {
constructor(name, age) {
super(name);
this.age = age;
this.type = "dog";
}
getAge() {
console.log(`my age is ${this.age}`);
}
}
let dog1 = new Dog("旺财", 2);
dog1.run(); // 旺财 is running
dog1.getAge(); // my age is 2
babel compiler
将上面的代码用babel编译之后会是什么样的呢?
这里只看核心代码,为了简洁同时把一些 edgecase 删除了。有兴趣的可以自己到babel官网试试看
Babel · The compiler for next generation JavaScript (babeljs.io)
// 寄生的实现
function _inherits(subClass, superClass) {
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: { value: subClass, writable: true, configurable: true },
});
if (superClass) Object.setPrototypeOf(subClass, superClass);
}
这里做了两件事
- 使得
subClass.prototype.__proto__ -> superClass.prototype,同时配置了constructor属性。 因为subClass.prototype是由Object.create创建出来的,所以它是不包含任何属性的纯对象,只有一个__proto__指向父类的原型 这样,new subClass()出来的实例函数,就能够访问父原型上的属性和方法
- 使得
subClass.__proto__ -> superClass。这里跟后续的super实现有关。 同时实现了可以通过子类访问父类的方法和属性。
如果只看第一件事,那么是实现效果上和组合寄生式继承式一样的,不过 babel 实现的更加严谨
// 组合的实现
var Dog = /*#__PURE__*/ (function (_Animal) {
_inherits(Dog, _Animal); // 寄生
var _super = _createSuper(Dog); // 获取父类
function Dog(name, age) {
var _this;
_classCallCheck(this, Dog);
_this = _super.call(this, name);
// _this = _Animal.call(this, name); //上一行可以简单的等价为这个
_this.type = "dog";
_this.age = age;
return _this;
}
_createClass(Dog, [
{
key: "getAge",
value: function getAge() {
console.log("my age is ".concat(this.age));
},
},
]);
return Dog;
})(Animal);
这里的核心代码 _this = _super.call(this, name) 实现了组合
结论
到此我们简单的验证了一件事情,class 不过是一个语法糖,其实现完全可以通过对原型的操作进行还原。
参考资料
《JavaScript高级程序设计》(第四版)
《JavaScript忍者秘籍》(第二版)