【四】原型

碎碎念

小米扫地机器人没有抢到。。。。这些人太快了。
昨天写完文章后,为了等时间抢购,玩了三把第五人格,运气不好匹配到了红蝶,被闪现技能吓得心脏都要跳出来。
强烈建议不要有这个技能,吓死人不偿命啊。现在过了一个晚上回想起来还觉得心有余悸,可怕!!!三个严重的叹号。

前言

回归正题。原型原型链又是前端开发人员另外一个圣神的话题。初级会使用这个封装一个组件,几乎是你进阶的标志。

对象

对象是js的基础,js中一共有6中基本类型:

  1. string
  2. number
  3. boolean
  4. null
  5. undefined
  6. object

其中有个特殊的,typeof null === 'object'。也不用纠结,算是一个小问题。
原理上是不同的对象在底层都表示为二进制,在JavaScript中二进制前三位都为0的话会被判断为object类型,null的二进制表示是全0,自然前三位也是0,所以执行typeof时会返回“object”。

js还有对象子类型,通常被称为内置对象

  1. String
  2. Number
  3. Boolean
  4. Object
  5. Function
  6. Array
  7. Date
  8. RegExp
  9. Error

这些内置函数可以当作构造函数来使用,从而可以构造一个对应子类型的新对象。
需要注意的是var str = 'i am working',我们定义了一个str,现在我们尝试着str.length,这个length实际上是js把str隐性得转成了String对象length也是String对象的方法。

抽象点讲,类是一个蓝图,基础实现。类实例化或者被继承的时候,会把自己的特性复制到实例或者子类中。js里是没有类的,只有对象。

原型

1
2
3
4
// 这个Foo的prototype就是个原型
function Foo() {
// ...
}

[[prototype]]/ __proto__

js在声明一个对象的时候,都会给这个对象的默认加一个[[prototype]]属性,它用对其他对象的引用,也可以认为你的父级吧。

1
2
3
4
var obj = {
a: 1
}
obj.a //1

当我去访问a变量的时候,会隐性触发一个Getter,这个Getter会先从当前作用域找a,找不到就会顺着这个[[prototype]]引用的上级对象找a,最后找不到再返回undefined。这种关联定义了原型链
[[prototype]]链的尽头是Object.prototype
这个时候我们在浏览器控制台声明一个变量obj,打印obj,会发现obj有一个__proto__,这个就是上面说的内置属性,表现形式为这样而已,方便展示出来给开发者看。

[[prototype]]/ prototype

往往拿来比较的还有一个内置属性叫prototype。这个比较特殊,它只有在函数对象才会有,它是拿来指向这个函数对象的原型对象。

1
2
3
function Foo(){
}
Foo.prototype

Foo.prototype就是Foo的原型,顺带扯一下,Foo.prototype里有内置一个constructor,指向FooFooFoo.prototype构造器函数

构造函数

上面介绍了啥是构造函数

1
2
3
4
5
6
7
8
9
10
function Foo(){
}
// 到这里为止,Foo还是一个普通函数
var a = new Foo();
// 等价于下面的
// var a = {};
// a.__proto__ = Foo.prototype;
// Foo.call(a);

// Foo是构造函数

这里Foo就是一个构造函数(函数对象),a是一个Foo的实例对象(普调对象),在new的过程中,给a内置了一个[[prototype]]这里是__proto__,指向了Foo.prototype
所以这里一定程度上实现了继承,但是类的继承是属于复制的行为,但是js对象默认是不会复制的,所以new的过程,给两个对象做了关联,一定程度上可以让a使用Foo的方法,这就一定程度上实现了继承。
所以这个a.__proto__就全等于Foo.prototype,因为它们引用的是同一个东西。
a.__proto__constructor属性也被委托到了Foo.prototype,所以a.__proto__.constructor === Foo.prototype.constructor

来张图感受一下:
原型

继承

1
2
3
4
5
6
7
8
9
10
function Foo(name) {
this.name = name;
}
Foo.prototype.myName = function() {
return this.name;
};
var a = new Foo( "a" );
var b = new Foo( "b" );
a.myName(); // "a"
b.myName(); // "b"

这就典型的继承了原型上的myName的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Foo(name) {
this.name = name;
}
Foo.prototype.myName = function() {
return this.name;
};
function Bar(name,label) {
Foo.call( this, name );
this.label = label;
}
// 我们创建了一个新的 Bar.prototype 对象并指向到 Foo.prototype
Bar.prototype = Object.create( Foo.prototype );
// 注意!现在没有 Bar.prototype.constructor 了 // 如果你需要这个属性的话可能需要手动修复一下它
Bar.prototype.myLabel = function() {
return this.label;
};
var a = new Bar( "a", "obj a" );
a.myName(); // "a"
a.myLabel(); // "obj a"
`

这段代码的核心部分就是语句 Bar.prototype = Object.create()
Object.create(..) 会凭空创建一个“新”对象并把新对象内部的[[Prototype]]指向到你指定的对象(本例中是 Foo.prototype)。

讲这么多就是说,如何实现继承,就是要改成这个内置属性的指向。
如果能有一个标准并且可靠的方法来修改对象的[[Prototype]]关联就好了。
在ES6之前, 我们只能通过设置__proto__属性来实现,但是这个方法并不是标准并且无法兼容所有浏览器。
ES6添加了辅助函数Object.setPrototypeOf(..),可以用标准并且可靠的方法来修改关联。
我们来对比一下两种把Bar.prototype关联到Foo.prototype的方法:

ES6之前需要抛弃默认的Bar.prototype

Bar.prototype = Object.create( Foo.prototype );

ES6开始可以直接修改现有的Bar.prototype

Object.setPrototypeOf( Bar.prototype, Foo.prototype );

1
2
3
4
5
6
7
var foo = {
something: function() {
console.log( "Tell me something good..." );
}
};
var bar = Object.create( foo );
bar.something(); // Tell me something good...
1
2
3
4
5
6
7
8
9
// es5 的写法
const obj = {
method: function() { ... }
};
obj.__proto__ = someOtherObj;

// es6 的写法
var obj = Object.create(someOtherObj);
obj.method = function() { ... };

Class

es6新增了Class语法,通过extends就可以简单的实现继承。

1
2
3
4
5
6
7
8
9
10
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}

toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}

感觉es6出现真的是方便多了,像箭头函数啊,多好用,都不用想this,bind它。
这里有个super,在这里表示父类的构造函数,用来新建父类的this对象。
子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象
因此super()在这里相当于Point.prototype.constructor.call(this)
ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。
ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。

super还有个功能就是作为对象使用,代表父级。你可以super.toString()

总结

怎么写的这么语无伦次