摘要:因为操作符创建的对象都继承自构造函数的属性。继承的实现中常用的继承方式是组合继承,也就是通过构造函数和原型链继承同时来模拟继承的实现。
原文发布在我的博客
我们都知道 JavaScript 是一门基于原型的语言。当我们调用一个对象本身没有的属性时,JavaScript 就会从对象的原型对象上去找该属性,如果原型上也没有该属性,那就去找原型的原型,一直找原型链的末端也就是 Object.prototype 的原型 null。这种属性查找的方式我们称之为原型链。
类的实现由于 JavaScript 本身是没有的类的感念的。所以我们如果要实现一个类,一般是通过构造函数来模拟类的实现:
function Person(name,age){ //实现一个类 this.name = name; this.age = age; } var you = new Person("you",23); //通过 new 来新建实例
首先新建一个 Person 的构造函数,为了和一般的函数区别,我们会使用 CamelCase 方式来命名构造函数。
然后通过 new 操作符来创建实例,new 操作符其实干了这么几件事:
创建一个继承自 Person.prototype 的新对象
构造函数 Person 执行时,相应的参数传入,同时上下文被指定为这个新建的对象。
如果构造函数返回了一个对象,那么这个对象会取代 new 的结果。如果构造函数返回的不是对象,则会忽略这个返回值。
返回值不是对象 function Person(name){ this.name = name; return "person" } var you = new Person("you"); // you 的值: Person {name: "you"} 返回值是对象 function Person(name){ this.name = name; return [1,2,3] } var you = new Person("you"); // you的值: [1,2,3]
如果类的实例需要共享类的方法,那么就需要给构造函数的 prototype 属性添加方法了。因为 new 操作符创建的对象都继承自构造函数的 prototype 属性。他们可以共享定义在类 prototype 上的方法和属性。
function Person(name,age){ this.name = name; this.age = age; } Person.prototype = { sayName: function(){ console.log("My name is",this.name); } } var you = new Person("you",23); var me = new Person("me",23); you.sayName() // My name is you. me.sayName() // My name is me.继承的实现
JavaScript 中常用的继承方式是组合继承,也就是通过构造函数和原型链继承同时来模拟继承的实现。
//Person 构造函数如上 function Student(name,age,clas){ Person.call(this,name,age) this.clas = clas; } Student.prototype = Object.create(Person.prototype); // Mark 1 Student.constructor = Student; //如果不指明,则 Student 会找不到 constructor Student.prototype.study = function(){ console.log("I study in class",this.clas) }; var liming = new Student("liming",23,7); liming instanceof Person //true liming instanceof Student //true liming.sayName(); // My name is liming liming.study(); // I study in class 7
代码中 Mark 1 用到了 Object.create 方法。这个是 ES5 中新增的方法,用来创建一个拥有指定原型的对象。如果环境不兼容,可以用下面这个 Polyfill 来实现(仅实现第一个参数)。
if(!Object.create){ Object.create = function(obj){ function F(){}; F.prototype = obj; return new F(); } }
其实就是把 obj 赋值给临时函数 F ,然后返回一个 F 的实例。这样通过代码 Mark 1 Student 就得到了 Person.prototype 上的所有属性。有人会问了,那么为什么不干脆把 Person.prototype 直接赋值给 Student.prototype 呢?
是的,直接赋值是可以达到子类共享父类 prototype 的目的,但是它破坏了原型链。即:子类和父类共用了同一个 prototype,这样当某一个子类修改 prototype 的时候,其实同时也修改了父类的 prototype,那么就会影响到所有基于这个父类创建的子类,这并不是我们想要的结果。看例子:
//Person 同上 //Student 同上 Student.prototype = Person.prototype; Student.prototype.sayName = function(){ console.log("My name is",this.name,"my class is",this.clas) } var liming = new Student("liming",23,7) liming.sayName() //My name is liming,my class is 7; //另一个子类 function Employee(name,age,salary){ Person.call(name,age); this.salary = salary; } Employee.prototype = Person.prototype; var emp = new Employee("emp",23,10000); emp.sayName() //Mark 2
你们猜 Mark 2 会输出什么?
我们期望的 Mark 2 应该会输出 "My name is emp". 但实际上报错,为什么呢?因为我们改写 Student.prototype 的时候,也同时修改了 Person.prototype,最终导致 emp 继承的 prototype 是我们所不期望的,它的 sayName 方法是 My name is",this.name,"my class is",this.clas,这样自然是会报错的。
ES6 的继承随着 ECMAScript 6 的发布,我们有了新的方法来实现继承。也就是通过 class 关键字。
类的实现class Person { constructor(name,age){ this.name = name; this.age = age; } sayHello(){ console.log(`My name is ${this.name},i"m ${this.age} years old`) } } var you = new Person("you",23); you.sayHello() //My name is you,i"m 23 years old.继承
ES6 里面的继承也很方便,通过 extends 关键字来实现。
class Student extends Person{ constructor(name,age,cla){ super(name,age); this.class = cla; } study(){ console.log(`I"m study in class ${this.class}`) } } var liming = new Student("liming",23,7) liming.study() // I"m study in class 7.
这个继承相比上面的 ES5 里面实现的继承要方便了很多,但其实原理是一样的,提供的这些关键字方法只是语法糖而已,并没有改变 Js 是基于原型这么一个事实。不过 extends 这样实现的继承有一个限制,就是不能定义属性,只能定义方法。要新添属性,还是得通过修改 prototype 来达到目的。
Student.prototype.teacher = "Mr.Li" var liming = new Student("liming",23,7) var hanmeimei = new Student("hanmeimei",23,7) liming.teacher //Mr.Li hanmeimei.teacher //Mr.Li静态方法
ES6 还提供了 static 关键字,来实现静态方法。静态方法可以继承,但只能由类本身调用,不能被实例调用。
class Person{ constructor(name,age){ this.name = name; this.age = age; } static say(){ console.log("Static") } } class Student extends Person{} Person.say() // Static Student.say() // Static var you = new Person("you",23); you.say() // TypeError: liming.say is not a function
可以看到,在实例上调用的时候会直接报错。
Super关键字在子类中可以通过 super 来调用父类,根据调用位置的不同,行为也不同。在 constructor 中调用,相当于调用父类的 constructor 方法,而在普通方法里面调用则相当与调用父类本身。
class Person { constructor(name,age){ this.name = name; this.age = age; } sayHello(){ console.log(`My name is ${this.name},i"m ${this.age} years old`) } } class Student extends Person{ constructor(name,age,cla){ super(name,age); // 必须在子类调用 this 前执行,调用了父类的 constructor this.class = cla; } sayHello(){ super.sayHello; // 调用父类方法 console.log("Student say") } } var liming = new Student("liming",23,7); liming.say() // My name is liming,i"m 23 years old. Student say.总结
至此,我们可以看到:在 ES6 发布以后,JavaScript 中实现继承有了一个标准的方法。虽然它们只是语法糖,背后的本质还是通过原型链以及构造函数实现的,不过在写法上更易于我们理解而且也更加清晰。
参考:
JavaScript继承方式详解
JavaScript 原型系统的变迁,以及 ES6 class
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/79034.html
摘要:属性每个函数默认有属性方法返回的函数除外,其值为构造函数创建对象继承的对象。其思路使用原型链实现原型属性和方法的继承通过借用构造函数实现实例属性继承。 1 类和模块 每个独立的JavaScript对象都是一个属性的集合,独立对象间没有任何关系 ES5中的类是基于原型继承实现的:如果两个对象从同一个原型对象继承属性,称两个对象为同一个类的实例。r instanceof Range.pr...
摘要:使用新的易用的类定义,归根结底也是要创建构造函数和修改原型。首先,它把构造函数当成单独的函数且包含类属性集。该节点还储存了指向父类的指针引用,该父类也并储存了构造函数,属性集和及父类引用,依次类推。 原文请查阅这里,略有删减,本文采用知识共享署名 4.0 国际许可协议共享,BY Troland。 本系列持续更新中,Github 地址请查阅这里。 这是 JavaScript 工作原理的第...
摘要:使用新的易用的类定义,归根结底也是要创建构造函数和修改原型。首先,它把构造函数当成单独的函数且包含类属性集。该节点还储存了指向父类的指针引用,该父类也并储存了构造函数,属性集和及父类引用,依次类推。 原文请查阅这里,略有删减,本文采用知识共享署名 4.0 国际许可协议共享,BY Troland。 本系列持续更新中,Github 地址请查阅这里。 这是 JavaScript 工作原理的第...
摘要:下面是用实现转成抽象语法树如下还支持继承以下是转换结果最终的结果还是代码,其中包含库中的一些函数。可以使用新的易于使用的类定义,但是它仍然会创建构造函数和分配原型。 这是专门探索 JavaScript 及其所构建的组件的系列文章的第 15 篇。 想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你! 如果你错过了前面的章节,可以在这里找到它们: JavaScript 是...
阅读 2107·2023-04-25 17:23
阅读 2922·2021-11-17 09:33
阅读 2516·2021-08-21 14:09
阅读 3592·2019-08-30 15:56
阅读 2607·2019-08-30 15:54
阅读 1626·2019-08-30 15:53
阅读 2130·2019-08-29 13:53
阅读 1146·2019-08-29 12:31