本文主要梳理JS 的原型,原型链,new运算及继承等,会持续补充更新哦!
构造函数
constructor 返回创建实例对象时构造函数的引用。此属性的值是对函数本身的引用,而不是一个包含函数名称的字符串。
Symbol 是构造函数吗?
Symbol是基本数据类型,但作为构造函数来说它并不完整,因为它不支持语法 new Symbol(),但其原型上拥有 constructor属性,即 Symbol.prototype.constructor。Chrome 认为其不是构造函数,如果要生成实例直接使用 Symbol()即可。
constructor 值只读吗?
对于引用类型来说 constructor **属性值是可以修改的,但是对于基本类型来说是只读的,创建他们的是只读的原生构造函数(native constructors),当然 *null *和 *undefined *是没有 **constructor 属性的。
原型
Parent 对象有一个原型对象 Parent.prototype,其上有两个属性,分别是 constructor 和 proto,其中 proto (访问器属性)已被弃用,推荐使用 Object.getPrototypeOf()。
proto 是每个实例上都有的属性,prototype 是构造函数的属性,这两个并不一样,但 p.proto 和 Parent.prototype 指向同一个对象。
原型链
每个对象拥有一个原型对象,通过 proto指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null。
每个对象都有proto 属性,但只有函数对象才有 prototype *属性(但 *Function.prototype 除外,它是函数对象,但它很特殊,他没有prototype属性)
原型对象(Person.prototype)是 构造函数(Person)的一个实例。
所有的构造器都来自于 Function.prototype,甚至包括根构造器Object及Function自身。所有构造器都继承了Function.prototype·的属性及方法。如length、call、apply、bind
所有的构造器也都是一个普通 JS 对象,可以给构造器添加/删除属性等。同时它也继承了Object.prototype上的所有方法:toString、valueOf、hasOwnProperty等。
所有函数对象proto都指向Function.prototype,它是一个空函数Empty function
Object.getOwnPropertyNames,获取所有(包括不可枚举的属性)的属性名不包括 *prototy *中的属性,返回一个数组
如果要创建一个新对象,同时继承另一个对象的 [[Prototype]] ,推荐使用 Object.create()。
原型和原型链是JS实现继承的一种模型。
原型链的形成是真正是靠proto 而非prototype
function Person(){} var person1 = new Person(); console.log(person1.__proto__ === Person.prototype); // true console.log(Person.prototype.__proto__ === Object.prototype) //true console.log(Object.prototype.__proto__) //null Person.__proto__ == Function.prototype; //true Object.__proto__ === Function.prototype // true Function.__proto__ === Function.prototype // true Function.prototype.__proto__ === Object.prototype //true console.log(Function.prototype)// function(){} (空函数) var num = new Array() console.log(num.__proto__ == Array.prototype) // true console.log( Array.prototype.__proto__ == Object.prototype) // true console.log(Array.prototype) // [] (空数组) console.log(Object.prototype.__proto__) //null console.log(Array.__proto__ == Function.prototype)// true
New运算符
new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。 ——(MDN)
new一个对象过程发生了什么?
- 创建了一个全新的对象。(可以访问到构造函数和原型里的属性)
- 这个对象会被执行[[Prototype]](也就是proto)链接。
- 生成的新对象会绑定到函数调用的this。
- 通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上。
- 如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用会自动返回这个新的对象。
模拟一个new
function objectFactory() {
Constructor = [].shift.call(arguments);//取得外部传入的构造器
//var obj = {} // 创建一个空对象
//var obj = new Object(),//从Object.prototype上克隆一个对象
//obj.__proto__ = Constructor.prototype;
var obj = Object.create(Constructor.prototype);
// var F = function () { };
// F.prototype = Constructor.prototype;
// obj = new F();//指向正确的原型
var ret = Constructor.apply(obj, arguments);//借用外部传入的构造器给obj设置属性
return typeof ret === 'object' ? ret||obj : obj;//确保构造器总是返回一个对象
};
继承
一个在构造函数上常用的规则是,用于复用的成员(译注:属性和方法)应该被添加到原型上。
代码复用才是目标,继承只是达成这个目标的一种手段。
1、原型链继承
重写原型对象,代之以一个新类型的实例。
缺点:
- 多个实例对引用类型的操作会被篡改。
- 在创建实例时,不能向SuperType传参
SubType.prototype = new SuperType();
2、借用构造函数继承(经典继承)
使用父类的构造函数来增强子类实例,等同于复制父类的实例给子类(不使用原型)
SuperType.call(this);
缺点:
- 只能继承父类的实例属性和方法,不能继承原型属性/方法
- 无法实现复用,每个子类都有父类实例函数的副本,影响性能
3、组合继承
原型链继承和经典继承双剑合璧。
缺点:在使用子类创建实例对象时,其原型中会存在两份相同的属性/方法。
4、原型式继承
利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型。
var SubType = Object.create(SuperType);
缺点:
- 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
- 无法传递参数
5、寄生式继承
在原型式继承的基础上,增强对象,返回构造函数(为构造函数新增属性和方法,以增强函数)
缺点:跟借用构造函数模式一样,每次创建对象都会创建一遍方法。
6、寄生组合式继承
function inheritPrototype(subType, superType){
var prototype = Object.create(superType.prototype); // 创建对象,创建父类原型的一个副本
prototype.constructor = subType; // 增强对象,弥补因重写原型而失去的默认的constructor 属性
subType.prototype = prototype; // 指定对象,将新创建的对象赋值给子类的原型
}
// 父类初始化实例属性和原型属性
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
// 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
function SubType(name, age){
SuperType.call(this, name);
this.age = age;
}
// 将父类原型指向子类
inheritPrototype(SubType, SuperType);
// 新增子类原型属性
SubType.prototype.sayAge = function(){
alert(this.age);
}
var instance1 = new SubType("xyc", 23);
var instance2 = new SubType("lxy", 23);
instance1.colors.push("2"); // ["red", "blue", "green", "2"]
instance1.colors.push("3"); // ["red", "blue", "green", "3"]
这是最成熟的方法,也是现在库实现的方法
7、混入继承多个对象
混元(Mix-ins),任意多数量的对象中复制属性,然后将它们混在一起组成一个新对象
// 混合其它
Object.assign(MyClass.prototype, OtherSuperClass.prototype);
Object.assign会把 OtherSuperClass原型上的函数拷贝到 MyClass原型上,使 MyClass 的所有实例都可用 OtherSuperClass 的方法。
8、ES6类继承extends
复制属性继承-浅拷贝,深拷贝-extend
问几个问题
Q1:函数声明和类声明的区别?
函数声明会提升,类声明不会。首先需要声明你的类,然后访问它,否则像下面的代码会抛出一个ReferenceError。
Q2:ES5继承和ES6继承的区别?
- ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.call(this)).
- ES6的继承有所不同,实质上是先创建父类的实例对象this,然后再用子类的构造函数修改this。因为子类没有自己的this对象,所以必须先调用父类的super()方法,否则新建实例报错。
巨人的肩膀
- 《JavaScript高级程序设计(第3版)》
- 冴羽JavaScript深入系列15篇
- 木易杨前端进阶-第 5 期:原型 Prototype
- JavaScript常用八种继承方案
- 最详尽的 JS 原型与原型链终极详解,没有「可能是」。(一)
- 最详尽的 JS 原型与原型链终极详解,没有「可能是」。(二)
- 最详尽的 JS 原型与原型链终极详解,没有「可能是」。(三)
- 代码复用模式
最后
欢迎纠错,看到会及时修改哒!❤
温故而知新,希望我们都可以保持本心,念念不忘,必有回响。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!