prototype 与 proto
- 函数都有 prototype 属性,bind 函数、Function.prototype 、箭头函数 除外。
- Function.prototype 是唯一一个使用 typeof 返回 function 的构造器原型。Function.prototype也是唯一一个没有 prototype 属性的函数。箭头函数也没有 prototype 属性
var o = {// 下面的 a 也是没有 prototype 属性的a(){},b:function(){}}o.a.prototype // undefinedo.b.prototype // fn
每个对象都有 proto 属性,但只有函数对象才有 prototype 属性。prototype 属性指向的是函数的原型对象,其实也是一个普通对象,prototype有两个预定义的属性,constructor,proto。
proto:**指向了创建该对象的构造函数的原型对象(prototype)**
js对象分为普通对象和函数对象,Object 、Function 是 JS 内置的函数对象,都是通过new Function创建的。所有的函数对象的proto都指向Function.prototype。所有的函数(包括 Function.prototype._proto)的 prototype._ **proto**_ 属性都指向 Object.prototype
Object.__proto__ === Function.prototype // trueObject.constructor == Function // true
String,Array,RegExp,Date,Number,Boolean都是如此。类似自定义的function Person(){}。只不过前者是内置的。
Function.proto === Function.prototype // true
Function 也是对象函数,也是通过new Function()创建,所以Function.proto指向Function.prototype。
构造器都来自于 Function.prototype,甚至包括根构造器Object及Function自身。所有构造器都继承了·Function.prototype·的属性及方法。如length、call、apply、bind。
Function.prototype 也是唯一一个 typeof XXX.prototype 为 function 的 prototype 。其它的构造器的 prototype 都是一个对象。
console.log(typeof Function.prototype) // function
console.log(typeof Object.prototype) // object
所有构造器(含内置及自定义)的proto都是Function.prototype,那Function.prototype的proto是谁呢?
相信都听说过JavaScript中函数也是一等公民,那从哪能体现呢?如下
console.log(Function.prototype.proto === Object.prototype) // true
有两个特殊的Math,JSON都是以对象形式存在的,不需要New。他们的proto都指向Object.prototype
Math.proto === Object.prototype // true Math.construrctor == Object // true
每个对象都有 proto 属性,但只有函数对象才有 prototype 属性。
function Person(){}
var person1 = new Person()
1、person1.proto 是什么?
2、Person.proto 是什么?
3、Person.prototype.proto 是什么?
4、Object.proto 是什么?
5、Object.prototype.proto 是什么?
答案:
第一题:
因为 person1.proto === person1 的构造函数.prototype
因为 person1的构造函数 === Person
所以 person1.proto === Person.prototype
第二题:
因为 Person.proto === Person的构造函数.prototype
因为 Person的构造函数 === Function
所以 Person.proto === Function.prototype
第三题:
Person.prototype 是一个普通对象,我们无需关注它有哪些属性,只要记住它是一个普通对象。
因为一个普通对象的构造函数 === Object
所以 Person.prototype.proto === Object.prototype
第四题,参照第二题,因为 Person 和 Object 一样都是构造函数
第五题:
Object.prototype 对象也有proto属性,但它比较特殊,为 null 。因为 null 处于原型链的顶端,这个只能记住。
Object.prototype.proto === null
继承:
function A(){}function B(){A.call(this)}// Object.create通过一个中间构造函数实现,这样继承有个问题 B.prototype.constructor === AB.prototype = Object.create(A.prototype)//所以需要重新指定B.prototype的construtorB.prototype = Object.create(A.prototype,{constructor:{value:B})B.prototype.__proto__ = A.prototypeObject.setPrototypeOf(B.prototype,A.prototype)// 后面两种方法是一样的。
原型链继承:
SubType.prototype = new SuperType()// 修改子类构造函数的指向,否则子类实例的构造函数会指向SuperType。SubType.prototype.constructor = SubType;
优点:父类方法可以复用 缺点:
父类的引用属性会被所有子类实例共享
子类构建实例时不能向父类传递参数
构造函数继承:
SuperType.call(SubType);
优点:和原型链继承完全反过来。
父类的引用属性不会被共享
子类构建实例时可以向父类传递参数
缺点:
- 父类的方法不能复用,子类实例的方法每次都是单独创建的
组合继承
原型链继承和构造函数继承的组合:
function SuperType() {this.name = 'parent';this.arr = [1, 2, 3];}SuperType.prototype.say = function() {console.log('this is parent')}function SubType() {SuperType.call(this) // 第二次调用SuperType}SubType.prototype = new SuperType() // 第一次调用SuperType
优点:
父类的方法可以被复用
父类的引用属性不会被共享
子类构建实例时可以向父类传递参数
缺点: 调用了两次父类的构造函数,第一次给子类的原型添加了父类的name, arr属性,第二次又给子类的构造函数添加了父类的name, arr属性,从而覆盖了子类原型中的同名参数。这种被覆盖的情况造成了性能上的浪费。
原型式继承
本质:原型式继承是利用Object.create,本质上是对参数对象的一个浅复制。
优点:父类方法可以复用
缺点:
父类的引用属性会被所有子类实例共享
子类构建实例时不能向父类传递参数
function object(o){function F(){}F.prototype = o;return new F();}var person = {name: "Nicholas",friends: ["Shelby", "Court", "Van"]};var anotherPerson = object(person);anotherPerson.name = "Greg";anotherPerson.friends.push("Rob");var yetAnotherPerson = object(person);yetAnotherPerson.name = "Linda";yetAnotherPerson.friends.push("Barbie");
寄生式继承
核心:使用原型式继承获得一个目标对象的浅复制,然后增强这个浅复制的能力。 优缺点:仅提供一种思路,没什么优点
function createAnother(original){var clone=object(original); //通过调用函数创建一个新对象clone.sayHi = function(){ //以某种方式来增强这个对象alert("hi");};return clone; //返回这个对象}var person = {name: "Nicholas",friends: ["Shelby", "Court", "Van"]};var anotherPerson = createAnother(person);anotherPerson.sayHi(); //"hi"
寄生组合继承(目前最好的继承方式)
为解决组合继承父类构造函数调用两次的问题
function inherit(sub, super){let prototype = object(super.prototype)prototype.constructor = subsub.prototype = prototype}// 上面的代码等价于function inherit(sub, super){sub.prototype = Object.create(super.prototype,{constructor:{value:sub}})}function inherit(sub, super){Object.setPrototypeOf(sub.prototype,super.prototype)//跟下面的等价// sub.prototype.__proto__ = super.prototype}function Person(name){}function Student(){SuperType.call(this)}inherit(Student, Student)
模拟Object.create实现:
function create(parentPrototype,props={}){function Fn(){}Fn.prototype = parentPrototypelet fn = new Fn()for(let key in props){Object.defineProperty(fn,key,{...props[key],enumerable:true})}return fn}
ES6 继承
es6类的继承会继承父级的静态方法,不继承静态属性:
class B{constructor(){this.b = 1return {b:2} // 如果返回一个对象,则会将这个对象作为new实例后this的指向}static a(){return 1}}class A extends B{}console.log(A.a()) // 1 继承静态方法let a = new A()console.log(a.b) // 2console.log(A.prototype.__proto__ === B.prototype) // trueconsole.log(A.__proto__ === B) // true
这样的结果是因为,类的继承是按照下面的模式实现的:
class B {}class A {}// A 的实例继承 B 的实例Object.setPrototypeOf(A.prototype, B.prototype);// 等同于A.prototype.__proto__ = B.prototype// A 继承 B 的静态属性Object.setPrototypeOf(A, B);// 等同于A.__proto__ = Bconst a = new A();
模拟 es 类的实现:
es6写法:
class B{constructor(){this.b = 1}static b_fn(){return 1}}class A extends B{} // 会继承静态方法,不继承静态属性
es6编译:
var _createClass = function () {function defineProperties(target, props) {for (var i = 0; i < props.length; i++) {var descriptor = props[i];descriptor.enumerable = descriptor.enumerable || false;descriptor.configurable = true;if ("value" in descriptor) descriptor.writable = true;Object.defineProperty(target, descriptor.key, descriptor);}}return function (Constructor, protoProps, staticProps) {// 3、定义原型上的方法if (protoProps) defineProperties(Constructor.prototype, protoProps);// 4、定义静态方法if (staticProps) defineProperties(Constructor, staticProps);return Constructor;};}();function _classCallCheck(instance, Constructor) {if (!(instance instanceof Constructor)) {throw new TypeError("Cannot call a class as a function");}}var B = function () {function B() {_classCallCheck(this, B); // 1、调用检测this.b = 1;}_createClass(B, null, [{ // 2、创建类key: "b_fn",value: function b_fn() {return 1;}}]);return B;}();// 子类继承父类function _inherits(subClass,superClass) {// 继承公有属性subClass.prototype = Object.create(superClass.prototype,{constructor:{value:subClass}});// 继承静态方法Object.setPrototypeOf(subClass,superClass);}let A= (function (Parent){// 先实现继承父类的公有属性和静态方法_inherits(C,Parent);function C() {_classCallCheck(this,C);let obj = Parent.call(this);let that = this;if(typeof obj === 'object'){that = obj;}that.age = 9; // 解决了父类返回一个引用类型的问题return that;}return C;})(B);
es5 与 es6 继承的区别:
ES5 的继承,实质是先创造子类的实例对象
this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。
摘自 阮一峰 http://es6.ruanyifeng.com/#docs/class-extends
而且在 es6 中,如果在子类中有写 constructor ,则必须显示的调用 super() (也就是调用父类的 constructor )。如果子类没有定义constructor方法,这个方法会被默认添加。
class ColorPoint extends Point {}// 等同于class ColorPoint extends Point {constructor(...args) {super(...args);}}
在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有super方法才能调用父类实例。
super 关键字
- super 作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数。
- super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
class ColorPoint extends Point {constructor(x, y, color) {super(x, y); // 调用父类的constructor(x, y)this.color = color;}toString() {return this.color + ' ' + super.toString(); // 调用父类的toString()}}
更多参考:http://es6.ruanyifeng.com/#docs/class-extends#super-%E5%85%B3%E9%94%AE%E5%AD%97
深度解析原型中的各个难点:https://github.com/KieSun/Blog/issues/2
一篇文章理解JS继承——原型链/构造函数/组合/原型式/寄生式/寄生组合/Class extends:
https://segmentfault.com/a/1190000015727237#articleHeader6
JavaScript常见的六种继承方式:
https://segmentfault.com/a/1190000016708006
私有属性
关于私有属性,以前一直很模糊。直到一次腾讯面试才让我重新去了解了下,下面是参考网上的实现做个记录:
1、基于编码规范约定实现方式
很多编码规范把以下划线_开头的变量约定为私有成员,便于同团队开发人员的协同工作。实现方式如下:
function Person(name){this._name = name;}
var person = new Person('Joe');
这种方式只是一种规范约定,很容易被打破。而且也并没有实现私有属性,上述代码中的实例person可以直接访问到_name属性:
alert(person._name); //'Joe'
2. 基于闭包的实现方式
另外一种比较普遍的方式是利用JavaScript的闭包特性。构造函数内定义局部变量和特权函数,其实例只能通过特权函数访问此变量,如下:
function Person(name){var _name = name;this.getName = function(){return _name;}}var person = new Person('Joe');
这种方式的优点是实现了私有属性的隐藏,Person 的实例并不能直接访问_name属性,只能通过特权函数getName获取:
alert(person._name); // undefinedalert(person.getName()); //'Joe'
使用闭包和特权函数实现私有属性的定义和访问是很多开发者采用的方式,Douglas Crockford也曾在博客中提到过这种方式。但是这种方式存在一些缺陷:
私有变量和特权函数只能在构造函数中创建。通常来讲,构造函数的功能只负责创建新对象,方法应该共享于prototype上。特权函数本质上是存在于每个实例中的,而不是prototype上,增加了资源占用。
3. 基于强引用散列表的实现方式
JavaScript不支持Map数据结构,所谓强引用散列表方式其实是Map模式的一种变体。简单来讲,就是给每个实例新增一个唯一的标识符,以此标识符为key,对应的value便是这个实例的私有属性,这对key-value保存在一个Object内。实现方式如下:
var Person = (function() {var privateData = {},privateId = 0;function Person(name) {Object.defineProperty(this, "_id", { value: privateId++ });privateData[this._id] = {name: name};}Person.prototype.getName = function() {return privateData[this._id].name;};return Person;}());
上述代码的有以下几个特征:
使用自执行函数创建Person类,变量privateData和privateId被所有实例共享; privateData用来储存每个实例的私有属性name的key-value,privateId用来分配每个实例的唯一标识符_id; 方法getName存在于prototype上,被所有实例共享。 这种方式在目前ES5环境下,基本是最佳方案了。但是仍然有一个致命的缺陷:散列表privateData对每个实例都是强引用,导致实例不能被垃圾回收处理。如果存在大量实例必然会导致memory leak。
造成以上问题的本质是JavaScript的闭包引用,以及只能使用字符串类型作为散列表的key值。针对这两个问题,ES6新增的WeakMap可以良好的解决。
4、基于WeakMap的实现方式
WeakMap有以下特点:
支持使用对象类型作为key值;
弱引用。
根据WeakMap的特点,便不必为每个实例都创建一个唯一标识符,因为实例本身便可以作为WeakMap的key。改进后的代码如下:
var Person = (function() {var privateData = new WeakMap();function Person(name) {privateData.set(this, { name: name });}Person.prototype.getName = function() {return privateData.get(this).name;};return Person;}());
改进的代码不仅仅干净了很多,而且WeakMap是一种弱引用散列表, 这意味着,如果没有其他引用和该键引用同一个对象,这个对象将会被当作垃圾回收掉。解决了内存泄露的问题。不过兼容性不太好哦。
原型链污染、攻击防护:
- 使用Object.freeze来冻结对象,几乎所有的JavaScript对象都是Object的实例。所以冻结Object.prototype即可。具体大家可以去了解下es5就新增的Object.freeze方法,使用了这个方法,那么黑客就无法对prototype新增或者重写对应原型链上的方法,不过这样可能会导致一些隐性的bug产生,而且可能还不知道是哪里出错了。
- 用map数据结构来代替自带的对象结构。Es6就有map结构啦,这个map和Object的区别就是map的键可以是任意的对象类型,数组也好,对象也好。
- 使用Object.create(null)(强烈推荐), 使用这个方法就可以更好的防御原型链污染攻击了,因为Object.create(null)使得创建的新对象没有任何的原型链,是null的,不具备任何的继承关系,
