JavaScript 核心原理解析
周爱民
《JavaScript 语言精髓与编程实践》作者,南潮科技(Ruff)首席架构师
32699 人已学习
新⼈⾸单¥59
登录后,你可以任选3讲全文学习
课程目录
已完结/共 28 讲
开篇词 (1讲)
JavaScript 核心原理解析
15
15
1.0x
00:00/00:00
登录|注册

15 | return Object.create(new.target.prototype):做框架设计的基本功:写一个根类

new.target为什么称为元属性,它与a.b有什么不同
父类并不关心子类实例的原型时,它返回任何的对象都是可以的
根类可以自己创建一个实例
非派生类可以通过return返回值
父类需要为实例this设置原型
用户定制的创建过程需要返回一个子类所需要的实例
返回非对象值会默认替换成已创建的this
在传统的构造函数和非派生类的构造方法中,一样是有new.target
非派生类不需要调用super()
类声明中如果有extends语法,会插入一个SuperCall()
JavaScript会为没有声明“constructor()”方法的类创建一个
父类需要new.target来创建this对象
ECMAScript 6中的类创建this对象需要X.prototype
构建过程在JavaScript引擎中事先定义好
JavaScript使用原型继承
构建过程中使用new.target元属性
必须是一个构造器
返回一个用Object.create()来创建的对象
思考题
其他
定制的构造方法
隐式的构造方法
定义自己的构建过程的必要性
基本功是写一个根类
JavaScript框架设计的基本功

该思维导图由 AI 生成,仅供参考

你好,我是周爱民。
今天这一讲的标题呢,比较长。它是我这个专栏中最长的标题了。不过说起来,这个标题的意义还是很简单的,就是返回一个用Object.create()来创建的对象。
因为用到了return这个子句,所以它显然应该是一个函数中的退出代码,是不能在函数外单独使用的。
这个函数呢,必须是一个构造器。更准确地说,标题中的代码必须工作在构造过程之中。因为除了return,它还用到了一个称为元属性(meta property)的东西,也就是new.target
迄今为止,new.target是 JavaScript 中唯一的一个元属性。

为什么需要定义自己的构建过程

通过之前的课程,你应该知道:JavaScript 使用原型继承来搭建自己的面向对象的继承体系,在这个过程中诞生了两种方法:
使用一般函数的构造器;
使用 ECMAScript 6 之后的类。
从根底上来说,这两种方法的构建过程都是在 JavaScript 引擎中事先定义好了的,例如在旧式风格的构造器中(以代码new X为例),对象this实际上是由 new 运算依据X.prototype来创建的。循此前例,ECMAScript 6 中的类,在创建this对象时也需要这个X.prototype来作为原型。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

JavaScript框架设计的基本功是编写一个根类。本文介绍了在JavaScript中定义自己的构建过程的必要性,以及在构造器中使用`new.target`元属性的重要性。作者指出,JavaScript使用原型继承来构建面向对象的继承体系,而在ECMAScript 6中的类创建过程中,`new.target`的传递是至关重要的。文章还讨论了在没有声明构造方法时,JavaScript引擎会自动插入一个默认的构造方法,并且非派生类不需要调用`super()`。总的来说,文章强调了在JavaScript框架设计中,编写根类是至关重要的基本功,同时也解释了相关技术特点。 在JavaScript类构建过程中,`new.target`元属性的使用至关重要。它与普通属性访问有所不同,能够在构造器中指示正在构造的实例的目标类。此外,文章还介绍了在特殊情况下,类的构造方法中可能没有`this`引用,需要通过返回`Object.create(new.target.prototype)`来创建实例。父类对子类实例的原型关系并不关心时,返回任何对象都是可以的。文章还提出了一个思考题,探讨了`new.target`作为元属性与普通属性访问的不同之处。 总的来说,本文深入探讨了JavaScript框架设计中根类的编写以及相关技术特点,对于理解JavaScript类构建过程具有重要的参考价值。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《JavaScript 核心原理解析》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(9)

  • 最新
  • 精选
  • 青史成灰
    老师,最后的这个例子: ``` class MyClass { constructor() { return new Date }; } class MyClassEx extends MyClass { constructor() { super() }; // or default foo() { console.log('check only'); } } var x = new MyClassEx; console.log('foo' in x); // false ``` 因为`foo`并不在`x`实例上,那假如我要访问`foo`,那得通过什么方式?或者说,那我这个类中定义的`foo`定义到哪里去了?

    作者回复: 这个示例中,x与MyClassEx/MyClass这两个类是没有继承关系的,原型继承或类继承的逻辑在这里没有用。例如: ``` > x instanceof MyClass false > x instanceof MyClassEx false ``` 你需要自已来维护原型链/继承关系,例如: ``` class MyClass { constructor() { return Object.setPrototypeOf(new Date, new.target.prototype); } } ... console.log('foo' in x); //true console.log(x instanceof MyClassEx); // true console.log(x instanceof MyClass); // false ``` 因为继承关系跟类声明分开了,所以你可以有很多方法来灵活处理了。

    2020-01-15
    2
    3
  • Astrogladiator-埃蒂纳度斯
    new.target为什么称为元属性,它与a.b(例如 super.xxx,或者’a’.toString)有什么不同? 个人理解是new.target是用来描述构造器本身的属性,指代是当前这个构造器函数this, 它不属于实例对象的一部分,它可以由super函数传递至根类,并最终由根类创建带有子类实例的对象。

    作者回复: 谢谢。确实是正确答案之一。

    2019-12-19
    3
  • 行问
    这里的代码在 Chrome 或 Node 是报错的 class MyClass extends Object { constructor() { return 1; } } function MyConstructor() { return 1; } console.log(new MyClass) console.log(new MyConstructor)

    作者回复: Oh... 这个问题在之前的课程中讲过,所以这里没有说很细。在第13讲中, > 因此到了 ECMAScript 6 之后,那些一般函数,以及非派生类,就延续了这一约定:使用已经创建的this对象来替代返回的无效值。 > ... ... > 对于那些派生的子类(即声明中使用了extends子句的类),ECMAScript 要求严格遵循“不得在构造器中返回非对象值(以及 null 值)”的设计约定,并在这种情况下直接抛出异常。 所以简单地说,就是如果class声明中不使用extends,那么它就跟你示例中的MyConstructor()一样,不会报错。而如果像你的MyClass那样使用了extends,就会报错了。

    2019-12-18
    2
    3
  • 小童
    老师,我在看规范的时候,有那么一句话不理解,状态和方法都会被对象承载,结构,行为和状态都能被继承。 这句怎么理解呢? 比如: function Car(){ } Car.prototype.name="car1"; Car.prototype.run=function(){ console.log("run") } var car1=new Car(); //对象 承载状态和方法 指name 状态 run方法 function Dog(){ } Dog.prototype=Car.prototype; //继承 Dog 继承了Car对象的结构 行为指继承了run方法吗? 状态是name属性吗?

    作者回复: 特意找了找你说的这段ECMScript规范的文本。应该是指如下: > In a class-based object-oriented language, in general, state is carried by instances, methods are carried by classes, and inheritance is only of structure and behaviour. > In ECMAScript, the state and methods are carried by objects, while structure, behaviour, and state are all inherited. 这里分成两段,分别是指的基于类继承的对象系统和ECMAScript中的对象系统。在这里,讲述ECMAScript中的(基于原型继承的)对象系统时也使用了原来在类继承中的概念,例如state、methods、structure和behaviour等。 首先,“状态(state)”这种说法来自结构化程序设计,我在《程序原本》中也用了“状态”这个概念。如果放在JS里面,状态就是一般属性,包括作为一般属性来理解的“函数类型的属性”。状态就是数据(因为数据是可变的,所以称为状态),所以JavaScript的对象是“属性包”,也就可以理解为“状态(数据)的集合”。 第二个需要说明的概念是“结构(structure)”。这个结构就是“结构化程序设计”中的结构。在这个概念体系里面,结构就是“数据被组织起来的样式”,所以“对象的结构”,本质上就是“对象包括属性的样式”。例如说,我们下面的代码: ``` obj = { a: 1, b: 2 } ``` 这是定义了对象obj的结构。而其中: ``` obj.a = 100 obj.b = 200 ``` 则是定义或修改了obj的两个状态(属性)。 那么为什么要这样来区分概念呢?这是因为到了ES6之后,解构赋值或解构表达式来声明的参数表,就是利用了对象的“结构性”而实现的功能。例如: ``` // 解构赋值,利用对象obj的结构性 let {a, b} = obj console.log(a, b) // 在参数上的声明 function foo({a}) { ... } foo(obj); ``` 所以,结构就是指类似上述赋值模板中的“结构声明”。它的继承性就是指:子类对象与原型具有相似的结构。 回到你的问题,还有一个就是“方法/行为”。“方法(method)”这个词在OOP中是特指的,就是指对象实例上可以调用的那些函数——当然,这包括函数类型的属性,以及在类声明中直接声明的方法以及get/setter。 状态和方法都会被对象承载(state and methods are carried by objects),这句话反过来说就是“对象(objects)包括方法和状态(属性)”。 而“行为”则是“在概念层面上讨论OOP”时的一个用词。它指的是一个对象的能力,比如被调用或者被别的对象关联等等。我在讲OOP——例如第14讲——的时候也会用到“行为/能力”这个词,表达的都是相同的意思。说得简单一点,“行为”就是“方法(的能力)”的抽象概念。 所以“结构,行为和状态都能被继承(structure, behaviour, and state are all inherited)”意思就是指一个对象的表现样式、行为方法和状态属性,都是可以继承的。 最后,最开始的两段英文是对比着来读的。它的意思是类继承通常只继承样式和行为,而不继承属性;而JS的原型继承是三者都继承的。简单的示例如下: ``` // 原型继承 parent = { a: 100, foo: function() {} } x = Object.create(parent) console.log(x.a); // 继承属性 console.log(x.foo); // 继承行为 console.log('a' in obj, 'foo' in obj); // 继承结构 // 类继承(某些经典系统中) x = new MyObject() x.foo(); // okay `foo` in x; // okay console.log(x.a); // maybe support ... ``` 最后这个例子是指:在某些类继承体系中,“继承来的属性”可能并没有初始化,而仅仅是一个未初始化的状态(例如在Delphi中就有类似的性质)。

    2020-04-24
    2
    2
  • James
    老师,在看react源码的context时候,遇到一个问题,简化如下,如果var a = {}, a.a =a,最终a.a.a...好像会无限下去,这样,会不会执行这个代码的时候,就内存泄漏了啊,如果造成内存泄漏的化,怎么会在react源码里面呢? export function createContext<T>( defaultValue: T, calculateChangedBits: ?(a: T, b: T) => number, ): ReactContext<T> { const context: ReactContext<T> = { $$typeof: REACT_CONTEXT_TYPE, _calculateChangedBits: calculateChangedBits, _currentValue: defaultValue, _currentValue2: defaultValue, Provider: (null: any), Consumer: (null: any), }; context.Provider = { $$typeof: REACT_PROVIDER_TYPE, _context: context, }; let hasWarnedAboutUsingNestedContextConsumers = false; let hasWarnedAboutUsingConsumerProvider = false; context.Consumer = context; return context; }

    作者回复: 如果在一个循环中,并且是在该循环中深度遍历a.a,那么就会死循环。 这个正好在编程概念中是有的,叫做“循环引用(deep-circular-references)”。循环引用不仅仅发生在你的例子中,在一般的对象声明,以及JSON格式的数据中都可以存在,在数据结构中,也是数据常见的性质。 如果一个数据内有循环引用,那么它的数据遍历就会死循环。因此需要在代码中尝试去检测“遍历历史中是否存在相同结点”,这个代价是极高的。因此绝大多数系统中都会要求数据在产生时不要搞成循环引用的。——也就是保证数据干净。但某些情况下,也需要处理未知的、无法确保干净的数据,这时要么不做遍历,要么就在遍历中处理。——在数据结构的图论中,有许多“有向有环图”的处理,就是这类算法与机制。 回到你的问题本身。“循环引用”本身的存在,并不会导致泄露或溢出。这很简单,任何一个结点,其.next属性指向自身,那么就简单地构成了这种属性引用(也就是a.a = a),那么它的内存占用就恒为“2个元素(自身和next属性)”,这怎么可能泄露或溢出呢?很简单的、有限个成员的数据结构而已嘛。 所以不深度遍历它就不会出问题。又如果需要深度遍历,那么就需要相应的算法或处理机制。

    2020-03-23
    2
  • Elmer
    定制的构造方法中,如果返回通过return传出的对象(也就是一个用户定制的创建过程),这个时候返回的对象原型并不是子类的原型,那不是不需要再设置this的原型了吗。。 function test (){ return {a: 1}; } const b = new test(); b instanceof test // false;此时如果test有父类也不需要设置this的原型?

    作者回复: 是的。不需要。JavaScirpt确实允许“类”返回“任意对象”。从JavaScript 1.2的设计开始,就一直如此。

    2020-01-07
    1
  • 小炭
    “迄今为止,new.target是 JavaScript 中唯一的一个元属性。” 对这句话有疑惑,就是下面这些不是元属性吗? { value: 123, writable: false, enumerable: true, configurable: false, get: undefined, set: undefined }

    作者回复: 至ecmascript 2019规范中只有一个明确称为元属性的东西,就是new.target。

    2020-11-10
  • HoSalt
    // 在JavaScript内置类Date()中可能的处理逻辑 function _Date() { this = Object.Create(Date.prototype, { _internal_slots }); Object.setPrototypeOf(this, new.target.prototype); ... } 1. Create应该是小写 2. 这段代码前面设置的__proto__会被后面的覆盖吧,下面这样实现没问题吧,和继承null那个例子类似 function _Date() { this = Object.create(new.target.prototype); ... }

    作者回复: 1. 谢谢。我请编辑改一下。 2. 不对的。_Date()表达的意思就是 1). 拿Date.prototype来创建实例,因为Date()内部根本不会用一个外部的原型来创建实例。 2). 将创建实例this的原型指向new.target.prototype 根本上来说,Date()只能保证用Date.prototype来创建实例是“正确的”,而不保证能用new.target.prototype创建出正确的实例。设计上来说,new.target.prototype是什么,是未知的,对于Date()来说它不可依赖。

    2020-05-25
  • qqq
    因为可以改变默认的原型继承行为
    2019-12-18
收起评论
显示
设置
留言
9
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部