javascript的原型和继承

250 阅读5分钟

1.对象的 [[Prototype]]属性

每个对象都有[[Prototype]]属性,用于指向对象的原型对象。

当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。

遵循ECMAScript标准,someObject.[[Prototype]] 符号是用于指向 someObject的原型。从 ECMAScript 6 开始,[[Prototype]] 可以通过Object.getPrototypeOf()和Object.setPrototypeOf()访问器来访问。这个等同于 JavaScript 的非标准但许多浏览器实现的属性__proto__ 。

下面有个例子,手动修改函数的[[Prototype]]属性来构造一个原型链。

    // http://jsbin.com/wovunob/4/edit?js,console
    
    const a = {x: 1};
    const b = {y: 2};
    const c = {z: 3};
    a.__proto__ = b
    b.__proto__ = c
    console.log(a.x) //1
    console.log(a.y) //2
    console.log(a.z) //3

因此 [[Prototype]]属性是javascript原型链的基础。

这引出一个问题,可是到底对象的原型又是什么呢?下面会讲到

关于代码中直接操作__proto__属性: 其实官方是建议使用Object.setPrototypeOf 和 Object.getPrototypeOf ,我这里偷个懒,工作中还是要认真做。

2. 函数的prototype属性

只有函数才有prototype属性。

当你创建函数时,JS会为这个函数自动添加prototype属性,指向一个对象。这个对象含有constructor属性,你也可以在里面添加一些方法来实现对象之间的方法函数的共享。

而一旦你把这个函数当作构造函数(constructor)调用(即通过new关键字调用),那么JS就会帮你创建该构造函数的对象实例。我们前面知道对象都是有[[Prototype]]属性的,该对象的属性指向自己的原型对象,我们发现原型对象正是构造函数的prototype指向的对象。正因为如此,新生成的对象才可以继承构造函数的所有属性和方法。

从上面的例子中,我们对对象的原型有个直观的了解。

3.原型对象中的constructor属性

constructor属性指向的是对象的构造函数。

4.不同生成对象的方法和原型链

4.1 语法结构创建的对象

var o = {a: 1};

// o 这个对象继承了Object.prototype上面的所有属性
// o 自身没有名为 hasOwnProperty 的属性
// hasOwnProperty 是 Object.prototype 的属性
// 因此 o 继承了 Object.prototype 的 hasOwnProperty
// Object.prototype 的原型为 null
// 原型链如下:
// o ---> Object.prototype ---> null

var a = ["yo", "whadup", "?"];

// 数组都继承于 Array.prototype 
// (Array.prototype 中包含 indexOf, forEach等方法)
// 原型链如下:
// a ---> Array.prototype ---> Object.prototype ---> null

function f(){
  return 2;
}

// 函数都继承于Function.prototype
// (Function.prototype 中包含 call, bind等方法)
// 原型链如下:
// f ---> Function.prototype ---> Object.prototype ---> null

4.2 构造器创建的对象

在 JavaScript 中,构造器其实就是一个普通的函数。当使用 new 操作符 来作用这个函数时,它就可以被称为构造方法(构造函数)。

function Graph() {
  this.vertices = [];
  this.edges = [];
}

Graph.prototype = {
  addVertex: function(v){
    this.vertices.push(v);
  }
};

var g = new Graph();
// g是生成的对象,他的自身属性有'vertices''edges'.
// 在g被实例化时,g.[[Prototype]]指向了Graph.prototype.

4.3 Object.create 创建的对象

ECMAScript 5 中引入了一个新方法:Object.create()。可以调用这个方法来创建一个新对象。新对象的原型就是调用 create 方法时传入的第一个参数

这实际就是进行继承了对象方法

var a = {a: 1}; 
// a ---> Object.prototype ---> null

var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (继承而来)

var c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null

var d = Object.create(null);
// d ---> null
console.log(d.hasOwnProperty); // undefined, 因为d没有继承Object.prototype

Object.create的内部实现:

if (typeof Object.create !== "function") {
    Object.create = function (proto, propertiesObject) {
        if (typeof proto !== 'object' && typeof proto !== 'function') {
            throw new TypeError('Object prototype may only be an Object: ' + proto);
        } else if (proto === null) {
            throw new Error("This browser's implementation of Object.create is a shim and doesn't support 'null' as the first argument.");
        }

        if (typeof propertiesObject != 'undefined') throw new Error("This browser's implementation of Object.create is a shim and doesn't support a second argument.");

        function F() {}
        F.prototype = proto;

        return new F();
    };
}

运行代码 jsbin.com/wovunob/7/e…

5.类的继承

继承是一种在新对象上复用现有对象的属性的形式,这有助于避免重复代码和重复数据。 js中的继承其实就是将构造函数的prototype指向一个要继承的类的对象。

运行代码 jsbin.com/wovunob/8/e…

function Person(){}
Person.prototype.dance = function(){}

function Ninja(){}
Ninja.prototype = new Person()

const n = new Ninja();
console.log(n instanceof Ninja) //true
console.log(n instanceof Person) //true
console.log('dance' in n) //true
console.log(n.constructor === Person) //true

此时Ninja其实已经继承了Person,但是要还要修改原型对象的constructor属性 这里使用Object.defineProperty来改变属性。 之所以不直接对Ninja.prototype.constructor进行赋值,是因为这么做会导致constructor属性被遍历到。

运行代码 jsbin.com/wovunob/10/…

function Person(){}
Person.prototype.dance = function(){}

function Ninja(){}
Ninja.prototype = new Person()
Object.defineProperty(Ninja.prototype , 'constructor' ,{
  enumerable:false,
  writable:true,
  value:Ninja
})

const n = new Ninja();
console.log(n instanceof Ninja) //true
console.log(n instanceof Person) //true
console.log('dance' in n) //true
console.log(n.constructor === Ninja) //true
console.log(Object.keys(Ninja.prototype)) // []

6.ES6中的class

ES6中引入了新的关键字class,它提供了一种更为优雅的创建对象和实现继承的方式,底层仍然是原型链。

1.使用class创建类

使用方法很简单

class Person{
  constructor(name){
    this.name = name
  }
  dance(){
    return true
  }
}

const p = new Person('lilei');
console.log(p.name) //"lilei"
console.log(p.dance()) //true

其实class创建的类的类型还是一个函数,仍然具有prototype属性,对象的[[prototype]]仍然指向这个原型对象,有区别的是constructor变了class,这个暂时无法理解。

2.使用class进行继承

class Ninja extends Person 就可以了

不用再考虑原型或者覆盖属性的副作用了。

http://jsbin.com/wovunob/11/edit?js,console

class Person{
  constructor(name){
    this.name = name
  }
  dance(){
    return true
  }
}

class Ninja extends Person{
    constructor(name,weapon){
    super(name)
    this.weapon = weapon
  }
  wieldWeapon(){
    return true
  }
  
}
const p = new Person('lilei');
console.log(p.name) //"lilei"
console.log(p.dance()) //true

const n = new Ninja('Yoshi','sword')
console.log(n instanceof Person) //true
console.log(n instanceof Ninja) //true
console.log(n.name) //"Yoshi"
console.log(n.dance()) //true
console.log(n.wieldWeapon()) //true
console.log(n.constructor === Ninja) //true
console.log(n.__proto__ === Ninja.prototype) //true
console.log(n.__proto__.__proto__ === Person.prototype) //true

7.其他

7.1 instanceof

7.2 hasOwnProperty

7.3 改变原型对象的副作用

参考文章

developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain

github.com/creeperyang/blog/issues/9

javascript忍者秘籍