JavaScript的原型继承

135 阅读4分钟

对象是编程中一个最为重要的数据的结构,其产生在不同的语言中有不同的方式。

  • 在以类为中心的面向对象的编程语言中,由类创建对象。
  • 在以原型为编程思想的语言中,创建对象只需要克隆一个对象即可获得。 在原型继承方面,js需要遵循的原型编程的基本规则
  • 所有的数据都是对象。
  • 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它。
  • 对象会记住它的原型。
  • 如果对象无法响应某个请求,它会把这个请求委托给它自己的原型。

1.所有的数据都是对象

我们知道undifined也是一种数据结构,如果,不考虑未定义的变量undifined,那么,其余的数据类型都可以通过"包装类"的方式变成对象类型数据来处理。

var num = new Number(123);
var flag = new Boolean(true);
var str = new String('aaa');
// (1) num,flag,str ==> Number、Boolean、String
console.log(num instanceof Number); // true
console.log(flag instanceof Boolean); // true
console.log(str instanceof String); // true
// (2) Number、Boolean、String ==> Object
console.log(Number instanceof Object); // true
console.log(Boolean instanceof Object); // true
console.log(String instanceof Object); // true
// (3) num,flag,str ==> Object
console.log(num instanceof Object); // true
console.log(flag instanceof Object); // true
console.log(str instanceof Object); // true

从以上可以看出,num、flag、str和Number、Boolean、String有关联,Number、Boolean、String与Object有关联,然后,num、flag、str与Object也有关联,那么是不是可以推测其中有一些内在的关联正在沿着一条链传递。 事实上,js中的根对象是Object.propotype对象,该对象是一个空对象。我们再js中遇到的每一个对象,可以认为都是从Object.propotype对象克隆而来,Object.propotype就是他们的原型。

2.要得到一个对象,不是实例化类,而是找到一个对象作为原型并克隆它

我们知道创建一个对象一般有两种方式

  • var obj = {}
  • var obj = new Object() 此时,引擎会从Object.propotype上面克隆一个对象出来。 其中,new运算符从构造函数函数中得到一个对象的demo如下:
function Person (name) {
    this.name = name
}
Person.prototype.getName = function () {
    return this.name
}
var p = new Person('阿猫')
console.log(p.name) // 阿猫
console.log(p.getName()) // 阿猫
console.log(Object.getPrototypeOf(p) === Person.prototype) // true

以上例子中的Person在例子是作为构造函数存在,当然,也可以直接作为普通函数被调用。在new运算符来创建对象的过程,实际上也是先克隆Object.prototype对象,再进行一些其他的操作,new运算符创建对象的主要流程是:

  • 从Oject.propotype上克隆一个空的新对象
  • 新对象的__proto__指向构造函数的prototype
  • 让this指向新对象,并且将外部传入的属性赋值给新对象
  • 返回新对象 根据new运算符的流程(其流程也就是其功能)模拟new运算符的实现
function Person(name) {
    this.name = name
}
Person.prototype.getName = function () {
    return this.name
}
var newClone = function () {
    // (1)从Oject.propotype上克隆一个空的新对象
    var obj = new Object();
    // (2)获取外部传入的构造函数,并且让空对象指向正确的原型
    var Constructor = [].shift.call(arguments)
    obj.__proto__ = Constructor.prototype
    // (3)让this指向新对象,并且将外部传入的属性赋值给新对象
    var result = Constructor.apply(obj, arguments)
    // (4) 返回新对象
    return typeof ret === 'Object' ? result : obj
}
var p = newClone(Person, '阿狗')
console.log(p.name) // 阿狗
console.log(p.getName()) // 阿狗
console.log(Object.getPrototypeOf(p) === Person.prototype) // true

3.对象会记住它的原型

就js的真实实现来说,其实并不能说对象有原型,而只能说对香的构造函数有原型。也不能说对象把请求委托给它自己的原型,准确的说是,对象把请求委托给它的构造器的原型。那么,对象通过什么方式将它的请求交给它的构造器的原型呢? js给对象提供了一个名为__proto__的隐藏属性,对象的的__proto__属性默认会指向它的构造器的原型对象,即{Constructor}.propotype。 例如:

var obj = new Object()
console.log(obj.__proto__ === Object.prototype) // true
// 附加
console.log(Object.getPrototypeOf(obj) === Object.prototype) // true
console.log(obj.constructor.prototype === Object.prototype) // true

js中__proto__就是"对象"和"对象构造器的原型"联系起来的纽带

4.如果对象无法响应某个请求,它会把这个请求委托给它自己的原型

var A = function () {}
A.prototype = {
    age: 30
}

var B = function () {}
B.prototype = new A()

var b = new B()
console.log(b.age) // 30

console.log(b.__proto__ === B.prototype) // true
console.log(b.__proto__.__proto__ === A.prototype) // true
console.log(b.__proto__.__proto__.__proto__ === Object.prototype) // true

从以上示例中可以看出,b中虽未定义age属性,但是,依然可以成功打印b.age,其流程是:

  • 遍历b中所有属性,未找到age属性
  • 查找age的请求被委托给对象b的构造器的原型,它在b.__proto__记录并且指向B.prototype,而B.prototype是由new A()创建而来
  • 在new A()创建的对象中依然未找到age属性,于是请求被继续委托给这个对象构造器的原型A.prototype
  • 在A.prototype中找到age,并返回 假设要在b中寻找job属性,那么查询顺序依旧如上,但是,最终在A._proto_,即Object.prototype中也未找到,寻找结束。
    其实,以上沿着构造函数原型一直查找某个属性,直到在Object.prototype中终止而形成的链条,就是原型链

参考书籍:《JavaScript设计模式与开发实践》(曾探)