共计 4081 个字符,预计需要花费 11 分钟才能阅读完成。
有句话说“JavaScript 里一切都是对象”,这句话当然是错的,因为它还有六种基本类型:String
、Number
、Boolean
、Null
、undefined
、Symbol
。对象才有属性和方法,当我们想要操作基本类型,比如获取 length、截取长度等方法,JS 就会自动把基本类型转换为对应的对象实例,我们就在对象实例上操作。
我们知道,JS 里没有类似于 C ++、Java 一样的 class
类,于是也就没有私有、公有之说。
但这并不代表我们不能模拟一个类,因为类的思想在面向对象编程里是很重要的一点,对于对象的继承与方法的引用和代码的重构等等都有极大好处。
所以很多人就在 JS 里模拟类的用法,ES6 甚至官方定义了一个 class
语法,但这也不是真正的类,只是 class
的语法糖。
说了这么多,JS 里到底怎么实现类呢?
利用函数的特性:所有的函数默认都会拥有一个名为 prototype 的公有且不可枚举的属性,它会指向另一个对象,也就是原型。
构造函数
什么是构造函数?我们知道 function
也是对象的一种,而当我们想要声明一个构造函数,可以使用下列形式:
function myConstructor() {
this.name = 'Simon';
var car = 'BMW';
function getCar() {console.log(car);
}
getCar();}
var myObj = new myConstructor();
使用上述方法我们就声明了一个构造函数myConstructor
,但其实构造函数也是普通的对象,它和 java 里类的构造函数完全不一样。
当我们使用 new
创建一个对象,实际上是把 myObj 的 [[Prototype]]
链接关联到 myConstructor.prototype
指向的对象。
我们知道,真正的面向类的语言中,类可以被复制或者实例化很多次,每创建一个实例相当于把类的方法属性复制到新实例中。
但 javascript 没有类似的复制机制,要想让新对象与构造的“类”对象有关系,就必须把两个对象的 prototype
关联起来。
实际上并不存在所谓的“构造函数”,我们只是对函数的“构造调用”。
当使用 new
来进行构造函数调用时,会执行以下四个步骤:
- 创建一个全新的对象
- 新对象会执行 [[Prototype]] 链接到 constructor 对象的 prototype
- 新对象会绑定到函数调用的 this
- 如果构造函数没有返回其他对象,那么 new 的函数调用会返回这个新对象
这样一来,我们就把新对象与构造函数对象关联起来了。所以说函数不是构造函数,只是当且仅当使用 new 时,函数调用会变成“构造函数调用”。
function Foo () {//.....}
var a = new Foo();
a.constructor === Foo; // true
Foo.prototype.constructor === Foo; // true
当我们创建了 Foo 函数,Foo.prototype 默认有一个公有且不可枚举的属性 constructor,这个属性引用的是对象关联的函数(也就是 Foo)。而调用 a.constructor,实际上只是通过 [[Prototype]] 委托给了 Foo.prototype,所以指向了 Foo。
如果我们创建了新的原型对象,那么新对象并不会自动获得 constructor 属性,比如说:
function Foo() {//....}
Foo.prototype = {/*....*/}; // 创建了新的原型对象
var a = new Foo();
a.constructor === Foo; // false
a.constructor === Object; // true
但问题来了,运行以下代码:
myObj.name; // "Simon"
myObj.car; // undefined
myObj.getCar(); // Uncaught TypeError: myObj.getCar is not a function
为什么会这样呢?原因就是构造函数有作用域。this.name = 'Simon'
相当于给构造函数这个对象的 name
属性赋值 "Simon"
,这里的this
指代构造函数对象原型。
而在构造函数里使用 var
、function
创建的变量和函数都相当于 局部变量,也就是“私有方法”,因为作用域的问题,这些变量和属性在构造函数之外是不能访问的,“私有方法”中的函数可以访问其他的私有变量,构造函数的实例却不能访问这些方法。
继承
那么,如果我们想要实例能继承构造函数的属性或者方法怎么办呢?JS 使用的机制为 原型继承,请看以下代码:
myConstructor.prototype.car = "Farrari";
myConstructor.prototype.getCar = function () {console.log(this.car);
};
myObj.car; // "Farrari"
myObj.getCar(); // "Farrari"
一旦我们在构造函数的原型上添加了属性 car
,当新对象 myObj 调用car
属性时,它本身没有这个属性,于是就通过委托,在原型链上找,最后在 myConsrtuctor.prototype
上找到。
但使用原型继承来实现子类继承父类更好的方法是这样:
function Foo(name) {this.name = name;}
Foo,prototype.getName = function() {console.log(this.name);
};
function Bar(name, type) {Foo.call(this, name); // 相当于 ES6 的 super(name);
this.type = type;
}
Bar.prototype = Object.create(Foo.prototype); // 相当于 ES6 的 extends
Bar.prototype.constructor = Bar; // 这里需要修复 consructor
Bar.prototype.getType = function () {console.log(this.type);
};
var a = new Bar('a', 'obj a');
a.getName(); // 'a'
a.getType(); // 'obj a'
这里用到的核心语句就是
Bar.prototype = Object.create(Foo.prototype);
调用 Object.create(obj)
会凭空创建一个新对象并把新对象的 [[Prototype]]
关联到 obj 上。
我们来看看 Object.create()
的 polyfill:
if (!Object.create) {Object.create = function (proto, propsObj) {if (!(proto === null || typeof proto === 'object' || typeof proto === 'function')) {throw TypeError('Arguments must be object, or Null');
}
var temp = new Object();
temp.__proto__ = proto;
if (propsObj) {Object.defineProperty(temp, propsObj);
}
return temp;
};
}
有的人可能会说,为什么不能直接 Bar.prototype = Foo.prototype;
呢?这样并不会创建一个关联到 Foo.prototype 的新对象,只是让 Bar.prototype 引用 Foo.prototype 对象,我们执行 Bar.prototype.getType = ....
也会在 Foo.prototype 上修改,跟预料的结果不一样。那这样根本不需要 Bar,还不如直接在 Foo 上修改就好了。
而之前介绍的 Bar.prototype = new Foo()
虽然也能实现原型继承,但它会有一些副作用。比如 Foo 如果有(写日志、修改状态、注册到其他对象、给 this 添加属性等等)的行为,就会影响到 Bar 的子类,造成无法预料的结果。
所以,要创建一个关联的“子类”对象,我们必须使用 Object.create(),但这样有一个缺点是要创建一个新对象而把旧对象抛弃。
好在 ES6 添加了 Object.setPrototypeOf(...)
方法,可以修改关联。
Bar.prototype = Object.create(Foo.prototype); // ES6 之前需要抛弃默认对象
Object.setPrototypeOf(Bar.prototype, Foo.prototype); // ES6 可以直接修改 Bar.prototype 对象
检查“类”的关系
考虑以下代码:
function Foo() {//...}
Foo.prototype.blah = ...;
var a = new Foo();
我们如果想要判断 a 的原型是否与 Foo 关联起来了,我们可以有以下几种方法:
instanceof
a instanceof Foo; // true
但这样有个缺点是只能处理对象和函数之间的关系。如果我们想判断两个对象之间是否通过 [[Prototype]]
关联。可以用下列方法:
isPrototypeOf()
Foo.prototype.isPrototypeOf(a); // true
要判断两个对象之间的关系,更直接:
b.isPrototypeOf(c); // 判断 b 是否出现在 c 的原型链中
Object.getPrototypeOf()
Object.getPrototypeOf(a) === Foo.prototype; // true
proto
我们可以直接用非标准的 __proto__
属性,__proto__
实际上存在于内置的 Object.prototype
中,且不可枚举。
a.__proto__ === Foo.prototype; // true
总结
要想创建类似于“类”的对象,可以使用构造函数调用,给构造函数添加公有方法,在构造函数的 prototype
上添加属性。新对象想要继承构造函数,使用 Javascript 的原型继承机制。