Skip to main content

深入理解JavaScript原型

· 9 min read
fish
var str = 'Hello world!';    // var str = new String('Hello world!')
str.substr(2, 4);
str.indexOf('world');

我们经常可以用到的字符串函数 substrreplaceindexOf等,是因为 String 对象上的 prototype 预先定义了这些方法。strString 的一个实例。

JavaScript标准库中常用的内置对象上 prototype 的方法还有: Array​.prototype​.pushArray​.prototype​.push
Date​.prototype​.get​DateDate​.prototype​.get​Year Function​.prototype​.toStringFunction​.prototype​.call ...

我们使用构造函数创建一个对象:

function User() {
}
var user = new User();
user.name = '张三';
console.log(user.name) // 张三

在这个例子中,User 是一个构造函数,我们使用 new 创建了一个实例对象 user

prototype


JavaScript 不包含传统的类继承模型,而是使用 prototype 原型模型。 每个函数都有一个 prototype 属性,换句话说, prototype 是函数才会有的属性

function User() {
}
User.prototype.name = '张三';
var user1 = new User();
var user2 = new User();
console.log(user1.name) // 张三
console.log(user2.name) // 张三

函数的 prototype 属性指向了一个对象,这个对象正是调用该构造函数而创建的 实例 的原型。也就是说这个例子中 User 的属性 prototype 对象是 user1 和 user2 的原型。

既然函数的 prototype 属性指向了一个对象,我们可以重写原型对象

function User() {}
User.prototype = {
name: '张三',
greeting: function() {
console.log('hello!')
}
};
var user = new User();
console.log(user.name); // 张三
user.greeting(); // hello!

这样,我们就可以 new User 对象以后,就可以调用 greeting 方法了。

然而将原子类型赋给 prototype 的操作将会被忽略

function User() {}
User.prototype = 1 // 无效

我们还可以在赋值原型 prototype 的时候使用 function 立即执行的表达式来赋值:

function User() {}
User.prototype = function() {
name = '张三',
greeting = function() {
console.log('hello!', this.name)
}
return {
name: name,
greeting: greeting
}
}();
(new User()).greeting();

它的好处就是可以封装私有的 function,通过 return 的形式暴露出简单的使用名称,以达到public/private的效果。

上述使用原型的时候,都是直接赋值原型对象,这样会覆盖之前已定义好的原型,导致之前原型上的方法或属性丢失,所以通常分开设置/覆盖 一个已知函数的 prototype

User.prototype.update = function() {} 

__proto__


所有 JavaScript 对象(null除外)都有的一个 __proto__ 属性,这个属性指向该对象的原型

function User() {
}
var user1 = new User();
var user2 = new User();
console.log(user1.__proto__ === User.prototype); // true
console.log(user1.__proto__ === user2.__proto__); // true

不管你创建多少个 User 对象实例,他们的原型指向的都是同一个 User.prototype

注: __proto__ 并不是语言本身的特性,这是各大厂商具体实现时添加的私有属性,不建议在生产中使用该属性,我们可以使用ES5的方法 Object.getPrototypeOf 方法来获取实例对象的原型。

Object.getPrototypeOf(user) === User.prototype

当查找一个对象的属性时,JavaScript 会向上遍历原型链,直到找到给定名称的属性为止。

到查找到达原型链的顶部 - 也就是 Object.prototype - 但是仍然没有找到指定的属性,就会返回 undefined。

function User() {
this.name = '张三'
}

User.prototype.name = '李四';

Object.prototype.age = 20

var user = new User();
console.log(user.name); // 张三
console.log(user.age); // 20

delete user.name
console.log(user.name); // 李四

如果一个属性在原型链的上端,则对于查找时间将带来不利影响。特别的,试图获取一个不存在的属性将会遍历整个原型链。

并且,当使用 for in 循环遍历对象的属性时,原型链上的所有属性都将被访问。

所以在使用 for in loop 遍历对象时,推荐总是使用 hasOwnProperty 方法, 这将会避免原型对象扩展带来的干扰。

for(var i in obj) {
if (obj.hasOwnProperty(i)) {
console.log(i);
}
}

构造函数


通过 new 关键字方式调用的函数都被认为是构造函数

function User() { }
var user = new User();
// user.constructor === user.__proto__.constructor
console.log(user.constructor === User ); // true
console.log(User.prototype.constructor === User); // true

原型 constructor 属性指向构造函数,在构造函数内部,this 指向新创建的对象 Object

如果被调用的函数没有显式的 return 表达式,则隐式的会返回 this 对象 - 也就是新创建的对象。

显式的 return 表达式将会影响返回结果,但总是会返回的是一个对象。

function Bar() {
return 2;
}
var bar = new Bar(); // 返回新创建的对象,而不是数字的字面量 2
console.log(bar.constructor === Bar); // true
function Foo() {
this.a = 1;

return {
b: 2
};
}

Foo.prototype.c = 3;

var foo = new Foo(); // 返回的对象 {b: 2}
console.log(foo.constructor === Foo); // false
console.log(foo.a); // undefined
console.log(foo.b); // 2
console.log(foo.c); // undefined

这里得到的 foo 是函数返回的对象,而不是通过new关键字新创建的对象。new Foo() 并不会改变返回的对象 foo 的原型, 也就是返回的对象 foo 的原型不会指向 Foo.prototype 。 因为构造函数的原型会被指向到新创建的对象,而这里的 Foo 没有把这个新创建的对象返回,而是返回了一个包含 b 属性的自定义对象。

如果 new 被遗漏了,则函数不会返回新创建的对象。

function Foo() {
this.abc = 1; // 获取设置全局参数 this===window
}
Foo(); // undefined

为了不使用 new 关键字,经常会使用工厂模式创建一个对象。

function Foo() {
var obj = {};
obj.value = 'blub';

var private = 2;
obj.setValue = function(value) {
this.value = value;
}

obj.getPrivate = function() {
return private;
}
return obj;
}

上面的方式看起来出错,并且可以使用闭包来达到封装私有变量, 但是随之而来的是一些不好的地方。

  • 为了实现继承,工厂方法需要从另外一个对象拷贝所有属性
  • 新创建的实例不能共享原型对象上的方法/属性,会占用更多的内存

继承


下面通过 call 实现继承,并将父级 prototype 给子 prototype

function Parent() {
this.value = 1
}
Parent.prototype.method = function() {
console.log('value: ' + this.value)
}

function Child() {
// this -> new Child()
Parent.call(this); // 调用Parent构造函数
}

var c1 = new Child();
console.log(c1.value); // 1

c1.method(); // 报错:Uncaught TypeError: c1.method is not a function

Child.prototype = Parent.prototype; //继承父方法
var c2 = new Child();

c2.method(); // 'value: 1'

但是这样继承存在一个问题,接上面代码继续

Child.prototype.fn = function() {
console.log('abc')
}

var p = new Parent();
p.fn(); // 'abc'

因为 Parent.prototype === Child.prototype,原型是同一个引用,可以直接将子类prototype 与 父类分离

for(var i in Parent.prototype) {
Child.prototype[i] = Parent.prototype[i]
}