new关键字做了什么
- step1:首先创建一个空对象,这个对象将会作为执行 new 构造函数() 之后,返回的对象实例
- step2:将上面创建的空对象的原型(
__proto__
),指向构造函数的 prototype 属性 - step3:将这个空对象赋值给构造函数内部的 this,并执行构造函数逻辑
- step4:根据构造函数执行逻辑,返回第一步创建的对象或者构造函数的显式返回值
newFunc的模拟训练
function Person(name) { this.name = name } const person = new newFunc(Person, 'lucas') console.log(person) // {name: "lucas"}
对newFunc的实现:
function newFunc(...args) { // 取出 args 数组第一个参数,即目标构造函数 const constructor = args.shift() // 创建一个空对象,且这个空对象继承构造函数的 prototype 属性 // 即实现 obj.__proto__ === constructor.prototype const obj = Object.create(constructor.prototype) // 执行构造函数,得到构造函数返回结果 // 注意这里我们使用 apply,将构造函数内的 this 指向为 obj,注意:如果构造函数有返回值,则result为返回值,否则为undefined const result = constructor.apply(obj, args) // 如果造函数执行后,返回结果是对象类型,就直接返回,否则返回 obj 对象 return (typeof result === 'object' && result != null) ? result : obj }
上述三目运算的处理,其实和构造函数的显式返回有关:
function Person(name) { this.name = name return {1: 1} } const person = new Person(Person, 'lucas') console.log(person) // {1: 1}
如何优雅地实现继承
有关继承的知识点在之前的几篇博客中都讲得很清晰了,下面就是对知识的进一步梳理总结:
其中原型链实现继承最关键的要点是:
Child.prototype = new Parent()
构造函数实现继承的要点是:
function Child (args) { // ... Parent.call(this, args) }
这样的实现其实只是实现了实例属性继承,Parent 原型的方法在 Child 实例中并不可用。
组合继承的实现要点是:
function Child (args1, args2) { // ... this.args2 = args2 Parent.call(this, args1) } Child.prototype = new Parent() Child.prototype.constrcutor = Child
Child 实例会存在 Parent 的实例属性。因为我们在 Child 构造函数中执行了 Parent 构造函数。同时,Child.__proto__
也会存在同样的 Parent 的实例属性,且所有 Child 实例的 __proto__
指向同一内存地址。同时上述实现也都没有对静态属性的继承。
一个比较完整的实现:
function inherit(Child, Parent) { // 继承原型上的属性 Child.prototype = Object.create(Parent.prototype) // 修复 constructor Child.prototype.constructor = Child // 存储超类 Child.super = Parent // 静态属性继承 if (Object.setPrototypeOf) { // setPrototypeOf es6 Object.setPrototypeOf(Child, Parent) } else if (Child.__proto__) { // __proto__ es6 引入,但是部分浏览器早已支持 Child.__proto__ = Parent } else { // 兼容 IE10 等陈旧浏览器 // 将 Parent 上的静态属性和方法拷贝一份到 Child 上,不会覆盖 Child 上的方法 for (var k in Parent) { if (Parent.hasOwnProperty(k) && !(k in Child)) { Child[k] = Parent[k] } } } }
静态属性继承存在一个问题:在陈旧浏览器中,属性和方法的继承我们是静态拷贝的,继承完后续父类的改动不会自动同步到子类。这是不同于正常面向对象思想的。但是这种组合式继承,已经相对完美、优雅。
小细节:这种继承方式无法实现对 Date 对象的继承,因为: JavaScript 的日期对象只能通过 JavaScript Date 作为构造函数来实例化得到。
如何实现对 Date 的继承呢?
function DateConstructor() { var dateObj = new(Function.prototype.bind.apply(Date, [Date].concat(Array.prototype.slice.call(arguments))))() //实现DateConstructor.prototype.__proto__ === Date.prototype Object.setPrototypeOf(dateObj, DateConstructor.prototype) dateObj.foo = 'bar' return dateObj } Object.setPrototypeOf(DateConstructor.prototype, Date.prototype) DateConstructor.prototype.getMyTime = function getTime() { return this.getTime() } let date = new DateConstructor() console.log(date.getMyTime())
ES6实现对Date的继承:
class DateConstructor extends Date { constructor() { super() this.foo ='bar' } getMyTime() { return this.getTime() } } let date = new DateConstructor() date.getMyTime() // 1558921640586
缺点:Babel 并没有对继承 Date 进行特殊处理,无法做到兼容。
Babel 编译结果研究
class Person { constructor(){ this.type = 'person' } } class Student extends Person { constructor(){ super() } } var student1 = new Student() student1.type // "person" student1 instanceof Student // true student1 instanceof Person // true student1.hasOwnProperty('type') // true
Babel 编译后的代码
var Person = function Person() { _classCallCheck(this, Person); this.type = 'person'; }; // 实现定义 Student 构造函数,它是一个自执行函数,接受父类构造函数为参数 var Student = (function(_Person) { // 实现对父类原型链属性的继承 _inherits(Student, _Person); // 将会返回这个函数作为完整的 Student 构造函数 function Student() { // 使用检测 _classCallCheck(this, Student); // _get 的返回值可以先理解为父类构造函数 _get(Object.getPrototypeOf(Student.prototype), 'constructor', this).call(this); } return Student; })(Person); // _x为Student.prototype.__proto__ // _x2为'constructor' // _x3为this var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; _again = false; // Student.prototype.__proto__为null的处理 if (object === null) object = Function.prototype; // 以下是为了完整复制父类原型链上的属性,包括属性特性的描述符 var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; //让 Student 子类继承 Person 父类原型链上的方法 function _inherits(subClass, superClass) { // superClass 需要为函数类型,否则会报错 if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } // Object.create 第二个参数是为了修复子类的 constructor subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); // Object.setPrototypeOf 是否存在做了一个判断,否则使用 __proto__ if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
jQuery 中的对象思想
const pNodes = $('p') // 我们得到一个数组 const divNodes= $('div') // 我们得到一个数组 const pNodes = $('p') pNodes.addClass('className') //数组上可是没有 addClass 方法 $('p') //$是一个函数 $.ajax() //$是一个对象
查看$的源码:
var jQuery = (function(){ var $ // ... $ = function(selector, context) { return function (selector, context) { var dom = [] dom.__proto__ = $.fn // ... return dom } } $.fn = { addClass: function() { // ... }, // ... } $.ajax = function() { // ... } return $ })() window.jQuery = jQuery window.$ === undefined && (window.$ = jQuery)
当调用 $('p')
时,最终返回的是 dom,而 dom.__proto__
指向了 $.fn
,$.fn
是包含了多种方法的对象集合。因此返回的结果(dom)可以在其原型链上找到 addClass 这样的方法。
同时 ajax 方法直接挂载在构造函数 $
上,它是一个静态属性方法。
这个很微妙地表现出来了jQuery面向对象的精妙设计。
类继承和原型继承的区别
传统的面向对象语言的类继承,引发的一些问题:
- 紧耦合问题
- 脆弱基类问题
- 层级僵化问题
- 必然重复性问题
- 大猩猩—香蕉问题
对于类继承和原型继承的区别,我们可以参看这篇文章: