1.概念
JavaScript并不提供一个class的实现,在ES6中提供class关键字,但是这个只是一个语法糖,JavaScript仍然是基于原型的。JavaScript只有一种结构:对象。每个对象都有一个私有属性:_proto_,这个属性指向它构造函数的原型对象(property)。它的原型对象也有一个属于自己的原型对象,这样层层向上知道一个对象的属性为null。根据定义null没有原型,它是这个原型链中的最后一个环节。
几乎所有的JavaScript中的对象都是位于原型链顶端的Object的实例。
2.基于原型链的继承
JavaScript对象是动态的属性“包”(指其自己的属性)。JavaScript对象有一个指向原型对象的链。当视图访问一个对象的属性时,它不仅仅在对象上搜寻,还会搜寻对象的原型,以及该对象原型的原型,依次层层向上搜索,直至找到一个名字匹配的属性或者到达原型链的顶端。
在ECMA标准中,someObject.[[Prototype]]符号是用于指向someObject的原型。从ES6开始[[Prototype]]可以通过Object.getPrototypeOf()和Object.setPrototype()访问器来访问。这个是JavaScript的非标准,但是很多浏览器都实现了__proto__。注意浏览器没有实现对象的object.Property这样的属性,即没有实现对象实例的Prototype属性。
但是它和构造函数func的prototype属性不同。被构造函数创建的实例对象的[[prototype]]指向func的prototype属性。Object.prototype属性表示Object的原型对象。
这里我们举一个例子,假设我们有一个对象o,它有自己的属性a, b,o 的原型 o.__proto__有属性 b 和 c, 最后, o.__proto__.__proto__ 是 null,JavaScript代码如下:
var o = {a: 1, b: 2}; o.__proto__ = {b: 3, c: 4}; console.log(Object.getPrototypeOf(o)); console.log(o.__proto__); console.log(Object.getPrototypeOf(Object.getPrototypeOf(o))); console.log(o.__proto__.__proto__); console.log(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(o)))); console.log(o.__proto__.__proto__.__proto__);
输出结果如下:
第一句:定义一个对象o,对象有属性a,b
第二句:设置对象o的原型为一个新的对象{b: 3, c: 4}
第三句:使用ES6方法Object.getPrototypeOf获取对象o的原型,输出{b: 3, c: 4}
第四句:使用浏览器实现的原型属性__proto__获取对象o的原型,输出{b: 3, c: 4}
第五句:使用ES6的方法Object.getPrototypeOf获取对象o的原型的原型,是原型链顶端Object的实例
第六句:使用浏览器实现的原型属性__proto__获取对象o的原型的原型,是原型链顶端Object的事例
第七句:使用ES6的方法Object.getPrototypeOf获取对象o的原型的原型的原型,是null
第八句:使用浏览器实现的原型属性__proto__获取对象o的原型的原型的原型,null
3.继承方法
JavaScript没有其他基于类的语言所定义的“方法”。在JavaScript里,任何函数都可以添加到对象上作为对象的属性。函数的继承与其他的属性继承没有任何区别,包括“属性遮蔽”(这相当于其他语言的方法重写)。
当继承的函数被调用时,this指向的当前继承的对象,而不是继承的函数所在的原型对象。看下面的例子:
var o = { a: 2, m: function () { return this.a + 1; } }; // 当调用o.m()的时候,this指向了o console.log(o.m()); // 创建一个对象p,p.__proto__是o,p是一个集成自o的对象 var p = Object.create(o); // 下面两句和上面的效果一样 // var p = {}; // p.__proto__ = o; // 创建p自身的属性a p.a = 4; // 调用p.m()函数时this指向了p,p继承o的m函数此时this.a,即p.a指向p自身的属性a,最后得到5 console.log(p.m());
上面代码中,调用p对象的m()方法时,m()方法中this.a指向p对象的a属性,而不是它的父对象o的属性a,有点类似英语语法中的“就近原则”,即先从自身属性开始找,而不是它的原型对象。
4.__proto__和prototype的关系
上面提到“JavaScript中只有一种结构,就是对象”,在JavaScript任何对象归根结底都是对象类型,他们都有对象的共同特点,即都有私有属性__proto__,基本上所有的浏览器都实现了这个属性,但是不建议在代码中使用这个属性,所以使用了一个比较怪异的名字__proto__,表示智能在内部使用,也叫隐式属性,意思是一个隐藏起来的属性。__proto__属性指向构造当前对象的构造函数的原型,它保证了对象实例能够访问在构造函数原型中定义的所有属性和方法。
JavaScript中方法这个对象除了和其他对象一样有隐式属性__proto__之外,还有自己特有的属性prototype,这个属性是一个指针,prototype指向原型对象,这个对象包含所有实例共享的属性和方法,我们把prototype属性叫做原型属性。prototype指向的原型对象又有一个属性constructor,这个属性也是一个指针,指回原构造函数,即这个方法。
下面我们来看一张图:
1.构造函数Foo()的原型属性Foo.prototype指向了原型对象,在原型对象中有共有的方法,构造函数声明的实例f1,f2都共享这个方法。
2.原型对象Foo.prototype保存着实例的共享的方法,它又有一个指针constructor,指回到构造函数,即函数Foo()。
3.实例。f1,f2是Foo这个对象的两个实例,这两个对象也有属性__proto__,指向构造函数的原型对象,这样就可以访问原型对象的所有方法。
4.构造函数Foo()除了是方法,也是对象,它的__proto__属性指向它的构造函数的原型对象Function.prototype。
5.Foo()的原型对象也是对象,它的__proto__属性指向它的构造函数的原型对象,即Object.prototype。
6.最后Object.prototype的__proto__指向null。
7.对象有属性__proto__,指向该对象的构造函数的原型对象。
8.方法除了有属性__proto__,还有属性prototype,指向该方法的原型对象。
4. 使用不同的方法来创建对象和生成原型链
4.1 语法结构创建的对象
var o = { a: 1 };这是一个定义对象的语法,这个语句使对象o继承了Object.prototype上所有的属性,o本身没有名为hasOwenProperty的属性,hasOwnProperty是Object.property的属性,因此对象o继承了Object.prototype的hasOwnProperty属性方法。Object.property的原型为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,使用console.log方法输出f,console.log(f)只能把函数的内容输出,并不能看到函数的原型,函数的原型的原型,目前本人还没有搞清楚这个问题。截图如下:
4.2使用构造器创建的对象
在JavaScript中,构造器其实就是一个普通的函数。当使用new操作符来作用这个函数时,它就可以被成为构造方法或者构造函数。看下面的代码:
function Graph() { this.vertices = [] this.edges = [] } Graph.prototype = { addVertice: function (v) { this.vertices.push(v); } } var g = new Graph(); console.log(g);
输出如下:
g是生成的对象,它有自己的属性‘vertices’和‘edges’,在g被实例化时,g.[[Prototype]]指向了Graph.prototype
4.3 Object.create创建的对象
ECMAScript5中引入了一个新的方法:Object.create()。可以调用这个方法来创建一个新对象。新对象的原型就是调用create方法时传入的第一个参数。我们来看下面的例子:输出结果如下:
var a = {a: 1}; var b = Object.create(a); console.log(b.a); var c = Object.create(b); console.log(c); console.log(c.a); var d = Object.create(null); console.log(d.hasOwnProperty);
输出结果如下:
第一句:定义对象a,它有属性a
第二句:使用Object.Create(a)创建对象b,b的原型是a
第三句:输出b.a,现在对象b上查找属性a,没有,然后在b的原型上找,值是1,输出1
第四句:使用Object.Create(b)创建对象c,c的原型是b
第五句:输出对象c,它的原型的原型上有一个属性c,值为1
第六句:输出c.a,现在对象c的属性中查找a,没有,在c的原型b上查找属性a,没有,在b的原型a上查找属性a,有,值为1,输出1
第七句:使用Object.Create(null)创建对象d,注意null没有原型
第八句:输出d.hasOwnProperty方法,在d的方法中找,没有,在d的原型null中找,也没有,最后输出undefined
4.4 class关键字创建对象
es6引入一套新的关键字来实现class。使用基于类的语言对这些结构会很熟悉,但它们是不同的。JavaScript是基于原型的。这些新的关键字包括class,constructor,static,extends和super。来看下面的例子:
class Polygon { constructor(height, width) { this.width = width; this.height = height; } } class Square extends Polygon { constructor(sideLength) { super(sideLength, sideLength); } get area() { return this.height * this.width; } set sideLength(sideLength) { this.height = sideLength; this.width = sideLength; } } var square = new Square(2); writeStr(square.area);
输出结果如下:
5. JavaScript中的继承
5.1 先看看如何封装
上面我们讲到创建对象的方式,有了对象之后就会有封装,在JavaScript中封装一个类很容易。通过构造器创建对象时,在构造函数(类)的内部通过对this(函数内部自带的变量,用于指向当前这个对象)变脸添加属性或者方法来实现添加属性或方法。代码如下:
// 类的封装 function Book1 (id, bookname, price) { this.id = id; this.bookname = bookname this.price = price } var Book2 = function (id, bookname, price) { this.id = id; this.bookname = bookname this.price = price }
也可以通过在构造函数类(对象)的原型上添加属性和方法。有两种方式,一种是为原型对象赋值,另一种是讲一个对象赋值给类的原型对象。如下:
// 方式一 Book.prototype.display = function () { } // 方式二 Book.prototype = { display: function () { } }
需要访问类的属性和方法时不能直接使用Book类,例如Book.name,Book.display(),而要用new关键字来创建新的对象,然后通过点语法来访问。
通过this添加的属性,方法是在当前函数对象上添加的,JavaScript是一种基于原型prototype的语言,所以每次通过一个构造函数创建对象的时候,这个对象都有一个原型prototype指向其继承的属性,方法。所以通过prototype继承来的属性和方法不是对象自身的,但是在使用这些属性和方法的时候需要通过prototype一级一级向上查找。
通过this定义的属性或方法是该函数对象自身拥有的,每次通过这个函数创建新对象的时候this指向的属性和方法都会相应的创建,而通过prototype继承的属性或者方法是通过prototype访问到的,每次通过函数创建新对象时这些属性和方法不会再次创建,也就是说只有单独的一份。
面向对象概念中“私有属性”,“私有方法”,“公有属性”,“公有方法”,“保护方法”在JavaScript中又是怎么实现的呢?
私有属性,私有方法:由于JavaScript函数级作用域,声明在函数内部的变量和方法在外界是访问不到的,通过这个特性可以创建类的私有变量以及私有方法。
公有属性,公有方法:在函数内部通过this创建的属性和方法,在类创建对象时,没有对象自身都拥有一份并且可以在外部访问到,因此通过this创建的属性,方法可以看做对象公有属性和对象公有方法。类通过prototype创建的属性或方法在实例的对象中通过点语法访问到,所以可以将prototype对象中的属性和方法也称为类的公有属性,类的公有方法。
特权方法:通过this创建的方法,不但可以访问这些对象的共有属性,方法,而且可以访问到类或者对象自身的私有属性和私有方法,权利比较大,所以可以看做是特权方法。
类构造器:在对象创建时可以通过特权方法实例化对象的一些属性,因此这些在创建对象时调用的特权方法可以看做类的构造器。
静态共有属性,静态共有方法:通过new关键字和方法名来创建新对象时,由于外面通过点语法添加的属性和方法没有执行到,所以新创建的对象中无法使用它们,但是可以通过类名来使用。因此在类外面通过点语法来创建的属性,方法可以被称为类的静态共有属性和类的静态共有方法。
参考下面的代码:
var Book = function (id, name, price) { // 私有属性 var num = 1; // 私有方法 function checkId() { }; // 特权方法 this.getName = function () { }; this.getPrice = function () { }; this.setName = function () { }; this.setPrice = function () { }; // 对象公有属性 this.id = id; // 对象公有方法 this.copy = function () { }; // 构造器 this.setName(name); this.setPrice(price); } // 类静态公有属性(对象不能访问) Book.isChinese = true; // 类静态公有方法(对象不能访问) Book.resetTime = function () { console.log(‘new Time‘); }; Book.prototype = { // 公有属性 isJSBook: false, //公有方法 display: function () { } };
通过new关键字创建对象是指是对新对象的this不断的赋值,并将prototype指向类的prototype所指向的对象,而在类的构造函数外面通过点语法定义的属性,方法不会添加在新的对象上。因此要想在新创建的对象上访问isChinese就得通过Book类而不能通过this,如Book.isChinese,类的原型上定义的属性在新对象里可以直接使用,这是因为新对象的prototype和类的prototype指向同一个对象。
类的私有属性num以及静态公有属性isChiese在新创建的对象里是访问不到的,而类的公有属性isJSBook在对象中可以通过点语法访问到。看下面实例代码,注意这段代码是在上面的实例代码基础上写的:
var b = new Book(11, ‘Javascript‘, 50); console.log(b.num); // undefined console.log(b.isJSBook); // false console.log(b.id); // 11 console.log(b.isChinese); // undefined console.log(Book.isChinese); // true Book.resetTime(); // new Time
new关键字的作用可以看做对当前对象的this不停地赋值,如果没有指定new关键字则this默认指向当前全局变量,一般是window,来看下面的代码:
5.1 子类的原型对象—类式继承
// 类式继承 // 申明父类 function SuperClass() { this.superValue = true } //为父类添加共有方法 SuperClass.prototype.getSuperValue = function () { return this.superValue; } // 申明子类 function SubClass() { this.subValue = false; } // 继承父类 SubClass.prototype= new SuperClass() // 为子类添加共有方法 SubClass.prototype.getSubValue = function () { return this.subValue; } let sup = new SuperClass(); let sub = new SubClass(); console.log(sup.getSuperValue()); //true // console.log(sup.getSubValue()); //Uncaught TypeError: sup.getSubValue is not a function console.log(sub.getSubValue()); // false console.log(sub.getSuperValue()); // true console.log(sub instanceof SubClass); // true console.log(sub instanceof SuperClass); // true console.log(sup instanceof SubClass); // false console.log(sup instanceof SuperClass); // true console.log(SubClass.prototype instanceof SuperClass); // true console.log(SubClass.prototype instanceof SuperClass.prototype); // Uncaught TypeError: Right-hand side of ‘instanceof‘ is not callable console.log(sub.prototype instanceof SuperClass); // false
类的原型对象用来为类添加共有方法,但是不能直接添加,访问这些属性和方法,必须通过原型prototype来访问。新创建的对象复制了父类构造函数的属性和方法,并将原型__proto__指向父类的原型对象,这样就拥有了父类的原型对象上的属性和方法,这个新创建的对象可以直接访问到父类原型对象上的属性和方法。
这种继承方式有2个缺点,其一,子类通过其原型对父类实例化,继承了父类。如果父类中的共有属性是引用类型的话,会被所有子类的实例公用,任何一个子类实例修改了父类属性(引用类型),会直接影响到所有子类和这个父类。看下面代码:
function SuperClass() { this.books = [‘javascript‘, ‘html‘]; } function SubClass() {} SubClass.prototype = new SuperClass(); var instance1 = new SubClass(); var instance2 = new SubClass(); console.log(instance1.books); //["javascript", "html"] instance2.books.push(‘java‘); console.log(instance1.books); //["javascript", "html", "java"] console.log(SuperClass.books);//undefined var sup1 = new SuperClass(); var sup2 = new SuperClass(); sup2.books.push(‘css‘); console.log(sup1.books); // ["javascript", "html"] console.log(sup2.books); // ["javascript", "html", "css"]
上面例子中instance2修改了父类的books属性,添加了一个“java”,结果instance1的books属性也有了个新的元素“java”。注意SubClass.prototype = new SuperClass();new操作符会复制一份父类的属性和方法,var sup = new SuperClass();也会复制一份父类的属性和方法,但是他们是不同的,相互不会影响。并且只有前者才会出现这种引用类型被无意修改的情况。
5.2 创建即继承—构造函数继承
// 构造函数继承 // 申明父类 function SuperClass(id) { // 引用类型共有属性 this.books = [‘javascript‘, ‘html‘, ‘css‘]; // 值型共有属性 this.id = id; } // 父类申明原型方法 SuperClass.prototype.showBooks = function () { console.log(this.books); } // 申明子类 function subClass(id) { // 继承父类 SuperClass.call(this, id); } // 创建两个实例 var instance1 = new subClass(10); var instance2 = new subClass(11); instance1.books.push(‘java‘); console.log(instance1.books); // ["javascript", "html", "css", "java"] console.log(instance1.id); // 10 console.log(instance2.books); // ["javascript", "html", "css"] console.log(instance2.id); // 11 instance1.showBooks(); // Uncaught TypeError: instance1.showBooks is not a function instance2.showBooks(); // Uncaught TypeError: instance1.showBooks is not a function // 申明父类实例 var instance3 = new SuperClass(12); instance3.showBooks(); // ["javascript", "html", "css"]
注意SuperClass.call(this, id);这句是构造函数式继承的关键。call方法可以改变函数的作用环境,在子类中对SuperClass调用合格方法就是讲子类中的变量在父类中执行一遍,在父类中给this绑定属性,因此子类就继承了父类的共有属性。由于这种类型的继承没有涉及原型,所以父类的原型方法不会被子类继承,要想被子类继承就必须放在构造函数中,这样创建的实例会单独拥有一份父类的属性和方法,而不是共用,这样违背了代码复用的原则。
5.3 组合继承
组合继承又叫“伪经典继承”,是指将原型链和构造函数技术组合在一起的一种继承方式,下面看一个例子:
// 申明父类 function SuperClasss(name) { // 值类型共有属性 this.name = name; // 引用类型共有属性 this.books = [‘html‘, ‘css‘, ‘Javascript‘]; } // 父类原型共有方法 SuperClasss.prototype.getName = function () { console.log(this.name); } // 申明子类 function SubClass(name, time) { // 构造函数式继承父类name属性 SuperClasss.call(this, name); // 子类的共有属性 this.time = time; } // 类式继承,子类原型继承父类 SubClass.prototype = new SuperClasss(); // 子类原型方法 SubClass.prototype.getTime = function () { console.log(this.time); } var instance1 = new SubClass(‘js book‘, 2014); instance1.books.push(‘java‘); console.log(instance1.books); // [‘html‘, ‘css‘, ‘Javascript‘, ‘java‘] instance1.getName(); // ‘js book‘ instance1.getTime(); // 2014 var instance2 = new SubClass(‘css book‘, 2013); console.log(instance2.books); // [‘html‘, ‘css‘, ‘Javascript‘] instance2.getName(); // ‘css book‘ instance2.getTime(); // 2013
在子类构造函数中执行父类构造函数,在子类原型上实例化父类就是组合模式。子类的实例中更改父类继承下来的引用类型属性books不会影响到其他实例,并且子类实例化过程中又能将参数传递到父类的构造函数中。
这种方式也有缺点,在使用构造函数继承时执行了一次父类的构造函数,而在实现子类原型的类式继承时又调用了一遍父类的构造函数。
5.4 简洁的继承—原型式继承
原型式继承的思想是借助prototype根据已有的对象创建一个新的对象,同时不必创建新的自定义对象类型。代码如下:
// 原型式继承 function inheritObject(o) { // 申明一个过渡函数对象 function F() {} // 过渡对象的原型继承父对象 F.prototype = o; // 返回过渡对象的一个实例,该实例的原型继承了父对象 return new F(); } var book = { name: ‘js book‘, alikeBook: [‘css book‘, ‘html book‘] } var newBook = inheritObject(book); newBook.name = ‘ajax book‘; newBook.alikeBook.push(‘xml book‘); var otherBook = inheritObject(book) otherBook.name = ‘flash book‘; otherBook.alikeBook.push(‘as book‘); console.log(newBook.name); // ajax book console.log(newBook.alikeBook); // ["css book", "html book", "xml book", "as book"] console.log(otherBook.name); // flash book console.log(otherBook.alikeBook); // ["css book", "html book", "xml book", "as book"] console.log(book.name); // js book console.log(book.alikeBook); // ["css book", "html book", "xml book", "as book"]
和类式继承一样,父类对象book中的值类型被复制,引用类型属性被共用。
5.5 寄生式继承
// 寄生式继承 function inheritObject(o) { // 申明一个过渡函数对象 function F() {} // 过渡对象的原型继承父对象 F.prototype = o; // 返回过渡对象的一个实例,该实例的原型继承了父对象 return new F(); } var book = { name: ‘js book‘, alikeBook: [‘css book‘, ‘html book‘] } function createBook(obj) { // 通过原型继承方式创建对象 var o = new inheritObject(obj); // 拓展对象 o.getName = function () { console.log(obj.name); } // 返回拓展后的新对象 return o; } var newBook = createBook(book); newBook.name = ‘ajax book‘; newBook.alikeBook.push(‘xml book‘); var otherBook = createBook(book); otherBook.name = ‘flash book‘; otherBook.alikeBook.push(‘as book‘); console.log(newBook.name); // ajax book newBook.getName(); // js book console.log(newBook.alikeBook); // ["css book", "html book", "xml book", "as book"] console.log(otherBook.name); // flash book console.log(otherBook.alikeBook); // ["css book", "html book", "xml book", "as book"] otherBook.getName(); // js book console.log(book.name); // js book console.log(book.alikeBook); // ["css book", "html book", "xml book", "as book"]
寄生式继承是对原型继承的二次封装,并在二次封装过程中对继承的对象进行了拓展,这样新创建的对象不仅仅有父类中的属性和方法,而且还添加了新的属性和方法。
5.6 寄生组合式继承
上面介绍的组合继承是把类式继承和构造函数继承组合使用,这种方式有一个问题,就是子类不是父类的实例,而子类的原型是父类的实例,所以才有了这里要说的寄生组合继承。寄生继承依赖于原型继承,原型继承又与类式继承很像,寄生继承有些特殊,它处理的不是对象,而是对象的原型。
组合继承中通过构造函数继承的属性和方法是没有问题的,这里主要探讨通过寄生式继承重新继承父类的的原型。我们需要继承的仅仅是父类的原型,不再需要调用父类的构造函数,也就是在构造函数继承中我们已经调用了父类的构造函数。因此我们需要的就是父类的原型对象的一个副本,而这个副本我们通过原型继承可以得到,但是这么直接赋值给子类会有问题的,因为对父类原型对象复制得到的复制对象p中的constructor指向的不是subClass子类对象,因此在寄生式继承中要对复制对象p做一次增强处理,修复它的constructor属性指向不正确的问题,最后得到的复制对象p赋值给子类的原型,这样子类的原型就继承了父类的原型并且没有执行父类的构造函数。测试代码如下:
// 寄生式继承 function inheritObject(o) { // 申明一个过渡函数对象 function F() {} // 过渡对象的原型继承父对象 F.prototype = o; // 返回过渡对象的一个实例,该实例的原型继承了父对象 return new F(); } // 寄生组合继承 function inheritPrototype(subClass, superClass) { // 复制一份父类的原型副本保存在变量中 var p = inheritObject(superClass.prototype); // 修正因为重写子类原型而导致子类的constructor属性被修改 p.constructor = subClass; // 设置子类的原型 subClass.prototype = p; } // 定义父类 function SuperClass(name) { this.name = name; this.colors = [‘red‘, ‘blue‘, ‘green‘]; } // 定义父类原型方法 SuperClass.prototype.getName = function () { console.log(this.name); }; // 定义子类 function SubClass(name, time) { // 构造函数式继承 SuperClass.call(this, name); // 子类新增属性 this.time = time; } // 寄生式继承父类原型 inheritPrototype(SubClass, SuperClass); // 子类新增原型方法 SubClass.prototype.getTime = function () { console.log(this.time); } var instance1 = new SubClass(‘js book‘, 2014); var instance2 = new SubClass(‘css book‘, 2013); instance1.colors.push(‘black‘); console.log(instance1.colors); //["red", "blue", "green", "black"] console.log(instance2.colors); //["red", "blue", "green"] instance2.getName(); //css book instance2.getTime(); //2013
最大的改变就是对子类原型的处理,被赋予父类原型的一个引用,这是一个对象,因此这里有一点要注意就是子类再想添加原型方法必须通过prototype对象,通过点语法的方式一个一个添加方法了,否则直接赋予对象就会覆盖掉从父类原型继承的对象。
原文地址:https://www.cnblogs.com/tylerdonet/p/9595436.html