精华内容
下载资源
问答
  • 下面属于javascript对象的有( )。 A、Window B、Document C、Table D、Java
  • 下面属于javascript对象的有:( ) A. Window B. Document C. Form D. String E. Navigator 解答:ACE 转载于:https://www.cnblogs.com/borter/p/9592765.html

    下面属于javascript对象的有:( )

     

    A. Window

     

    B. Document

     

    C. Form

     

    D. String

     

    E. Navigator

     

    解答:ACE

    转载于:https://www.cnblogs.com/borter/p/9592765.html

    展开全文
  • 原型(prototype)是 JavaScript 中每个对象都包含一个属性(除非使用 Object.create(null) 来创建对象)。但是它内部工作机制鲜为人知。原型属性分配机制是一个重要概念,利用它可以在 JavaScript 中应用继承...

    3f63cf6f8880fe79c437f37487177ccb.png

    作者 | Aparna Joshi 译者 | 王强 策划 | 李俊辰 原型(prototype)是 JavaScript 中每个对象都包含的一个属性(除非使用 Object.create(null) 来创建对象)。但是它的内部工作机制鲜为人知。原型属性的分配机制是一个重要的概念,利用它可以在 JavaScript 中应用继承。在理解原型之前,你必须了解 对象 的一些基本原理和用法。在本文中,我们将深入探讨在对象创建过程中是如何分配原型的,以及为什么这部分知识非常重要。

    本文最初发布于 Aparna Joshi 网站,经原作者授权由 InfoQ 中文站翻译并分享。

    什么是原型属性,它包含什么?

    使用任何可用方法创建的所有 JavaScript构造函数(constructor function) 都包含一个属性。这就是 原型 属性。这里很重要的是,原型属性本身就是一个对象。

    1. 构造函数的原型属性可用于访问 / 修改方法,以及访问 / 修改在创建对象时分配的原型对象中存在的其他属性。

    2. 每个原型对象都有一个称为 构造器(constructor) 的属性。这个属性指向构造函数 (Constructor Function) 本身。

    我们来看一个例子以更好地理解这一点:
    function Name(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.fullName = firstName + " " + lastName
    }
    var jensen = new Name("Jensen", "Ackles");
    console.log(jensen);

    如果尝试访问包含对象 jensen 的所有属性的 console.log,我们将得到以下结果。

    9d1de29cae53595c81b3e663d79ccbcf.png

    到这里发生过的事情原理是这样的:

    1. Name 是一个构造函数。它包含 原型 属性。

    2. 这个原型属性有一个称为 constructor 的属性,该属性指向 Name 构造函数本身。可以将其他任何属性(attribute)添加到这个属性上。

    3. 使用 Name 构造器创建新对象 jensen 后,这个对象可以访问属于 Name 函数的所有属性,包括其原型。

    4. 可以使用 proto 对象从新对象 jensen 中访问 Name 构造函数的 原型

    5. 由于 原型 本身是一个对象,因此它还包含一个 原型 属性。这样就创建了 原型链:

      1. https://aparnajoshi.netlify.app/javascript-prototype-inner-workings-of-objects#prototype-chain

    一些浏览器可以支持通过 proto 对象访问构造函数的原型。我们不建议在 JavaScript 编程中使用这个特性(该特性是非标准特性,可能无法在所有浏览器中都正常使用),但也可以用它在开发过程中快速检查原型链的运行状况。

    proto 的替代方法包括 Object.getPrototypeOf() 或 objectInstance.constructor.prototype。对于上述示例来说,可以通过以下方式使用它来访问相同的原型属性:
    Object.getPrototypeOf(jensen);
    jensen.constructor.prototype;

    8f09b16fab7c2079b52b3005eb2675ac.png

     原型链 

    我们通常是通过某些构造函数来创建对象的。如果未使用任何用户定义的构造器,则表示该对象是使用 JavaScript 的 Object Constructor(对象构造器) 创建的。这意味着我们创建的任何对象最终都将从 JavaScript 的对象构造器继承。

    看一下下面创建的对象,看看它们的 proto 对象包含哪些内容。
    function Fruit() {
        this.value = 10;
        this.quantity = 35;
    }
    function Apple(name, color) {
        this.name = name;
        this.color = color
    }
    Apple.prototype = new Fruit();
    var apple1 = new Apple("Apple", "Red");
    console.log(apple1);

    如果我们检查对象 apple1 的内部属性,则可以观察到以下内容:

    1. 对象 apple1 包含两个主要属性:name 和 color。这些属性的值与在创建过程中分配给它们的值是一样的。

    2. 对象 apple1 的 proto 属性指向 Fruit 对象的实例。反过来,这又包含另外两个属性:value 和 quantity。

      37b2cefd49e86c7b53ca9005f4880cb4.png

    3. 如果检查 Fruit 实例的 proto 属性,我们会发现它最终指向 JavaScript 的 Object 的原型。

    4. 当一个属性不直接存在于一个对象上时,JavaScript 会沿着 原型链 向上寻找,以在其最近的原型中找到该属性。就像 JavaScript 的作用域链一样,原型链也会不断上升,直到到达 Object.prototype 为止。

    原型的使用:继承和内置方法

    原型在 JavaScript 中广泛用于实现继承。传统上,JavaScript 仅用于脚本编写,并且不需要像其他语言一样提供面向对象的编程特性。但是,原型的概念可用来将方法和属性从一个构造函数传递到另一个构造函数。

    考虑以下示例:
    function Fruit() {
        this.value = 10;
        this.quantity = 35;
    }
    Fruit.prototype.setValue = function(value) {
        this.value = value;
    }
    function Apple(name, color) {
        this.name = name;
        this.color = color
    }
    Apple.prototype = new Fruit();
    var apple1 = new Apple("Apple", "Red");
    apple1.setValue(20);
    console.log(apple1.name); // Apple
    console.log(apple1.value); // 20
    console.log(apple1.quantity); // 35

    在上面的示例中,即使新对象 apple1 不具有 value 和 quantity 属性,我们仍然可以访问它们。需要注意的是,添加到 Fruit 构造函数 的原型属性上的 setValue 方法也可以通过对象 apple1 访问。这就是在 JavaScript 中实现继承的方式。

    使用任何构造器创建对象时,它都会附带某些可应用于对象的内置方法。其中一些内置方法包括 hasOwnProperty()、isPrototypeOf()、propertyIsEnumerable()、toLocaleString()、toString() 和 valueOf()。这些内置方法可用于所有对象。这是因为 JavaScript 中的所有对象都从 Object.prototype 继承属性和方法。

    所有内置的构造器,例如 Array()、Number() 和 String() 等,都是从 JavaScript 的 Object 构造器创建的,并且它们的原型也分配给 Object.prototype

    原型存在的一些问题

    JavaScript 中的原型有很多用途,它可用于继承父函数的方法,还可以用于抽象数据层,并仅公开 getter 和 setter 方法来操作属于各种对象的值。但原型也有其缺点。原型对象上添加的所有属性,对于使用其 构造函数 创建的对象的每个实例都是通用的。对于其中任一属性的任何更改都将反映在所有对象中。

    考虑以下示例:
    function Apple(name, color) {
        this.name = name;
        this.color = color
    }
    Apple.prototype.value = 20;
    var apple1 = new Apple("Apple", "Red");
    var apple2 = new Apple("Apple2", "Wheatish Red");
    console.log(apple1.name); // Apple
    console.log(apple1.value); // 20
    console.log(apple2.value); // 20
    Apple.prototype.value = 40;
    console.log(apple1.value); // 40
    console.log(apple2.value); // 40
    apple1.value = 30;
    console.log(apple1.value); // 30
    console.log(apple2.value); // 40

    在上面的示例中,直接在构造器原型上进行的更改会反映在其所有对象中;但是,当更改对象 apple1 内部的属性 value 时,这一更改就不会反映在其他对象中。这是因为 apple1 现在已经创建了自己的属性 value,并且从这个实例开始,apple1.value 将始终引用为其自身的属性 value,而不是继承的属性。

    为了解决这个问题,可以实现一个 构造器 - 原型 模式的组合。可以使用 构造函数 让属于该对象的数据值保持私有和唯一。那些可在所有对象之间共享的操作数据通用方法,可以添加到 原型对象 上。

    我希望这篇文章能够帮助读者详细了解原型属性及其用法。如果你对本文中描述的概念有任何疑问,请随时与我联系:

    https://twitter.com/aparna_joshi_

    延伸阅读

    https://aparnajoshi.netlify.app/javascript-prototype-inner-workings-of-objects

    c1e402b9a7a6a0f59baf706d55e8c8fd.gif

    展开全文
  • JavaScript 函数作用域在 JavaScript两种作用域类型:局部作用域全局作用域JavaScript 拥有函数作用域:每个函数创建一个新作用域。作用域决定了这些变量可访问性(可见性)。函数内部定义变量从函数外部是...

    JavaScript 作用域

    • JS 异常
    • JS Hoisting
    28e6297392238943ac80a27c5031e136.png

    作用域指的是您有权访问的变量集合。

    JavaScript 函数作用域

    在 JavaScript 中有两种作用域类型:

    • 局部作用域
    • 全局作用域

    JavaScript 拥有函数作用域:每个函数创建一个新的作用域。

    作用域决定了这些变量的可访问性(可见性)。

    函数内部定义的变量从函数外部是不可访问的(不可见的)。

    局部 JavaScript 变量

    在 JavaScript 函数中声明的变量,会成为函数的局部变量。

    局部变量的作用域是局部的:只能在函数内部访问它们。

    实例

    // 此处的代码不能使用 carName 变量function myFunction() {    var carName = "porsche";    // 此处的代码能使用 carName 变量}

    由于只能在函数内部识别局部变量,因此能够在不同函数中使用同名变量。

    在函数开始时会创建局部变量,在函数完成时会删除它们。

    全局 JavaScript 变量

    函数之外声明的变量,会成为全局变量。

    全局变量的作用域是全局的:网页的所有脚本和函数都能够访问它。

    实例

    var carName = " porsche";// 此处的代码能够使用 carName 变量function myFunction() {    // 此处的代码也能够使用 carName 变量}

    JavaScript 变量

    在 JavaScript 中,对象和函数也是变量。

    作用域决定了从代码不同部分对变量、对象和函数的可访问性。

    自动全局

    如果您为尚未声明的变量赋值,此变量会自动成为全局变量。

    这段代码将声明一个全局变量 carName,即使在函数内进行了赋值。

    实例

    myFunction();// 此处的代码能够使用 carName 变量function myFunction() {    carName = "porsche";}

    严格模式

    所有现代浏览器都支持以“严格模式”运行 JavaScript。

    您将在本教程稍后的章节学习更多如何使用严格模式的知识。

    在“严格模式”中不会自动创建全局变量。

    HTML 中的全局变量

    通过 JavaScript,全局作用域形成了完整的 JavaScript 环境。

    在 HTML 中,全局作用域是 window。所有全局变量均属于 window 对象。

    实例

    var carName = "porsche";// 此处的代码能够使用 window.carName

    警告

    除非有意为之,否则请勿创建全局变量。

    您的全局变量(或函数)能够覆盖 window 变量(或函数)。

    任何函数,包括 window 对象,能够覆盖您的全局变量和函数。

    JavaScript 变量的有效期

    JavaScript 变量的有效期始于其被创建时。

    局部变量会在函数完成时被删除。

    全局变量会在您关闭页面是被删除。

    展开全文
  • 本文作者:家园工作室研发组成员 @维尔希宁 本文出处:面向对象的 JavaScript:封装、继承与多态​blog.lenconda.top本文遵循署名-非商业性使用-禁止演绎3.0 未本地化版本(CC BY-NC-ND 3.0)协议发布,使用本文时请...

    eb507375d565897f6c29a0694f4358e5.png

    本文作者:家园工作室研发组成员 @维尔希宁

    本文出处:

    面向对象的 JavaScript:封装、继承与多态blog.lenconda.top
    114e2916e950adf3faf3c24271326657.png
    本文遵循署名-非商业性使用-禁止演绎3.0 未本地化版本(CC BY-NC-ND 3.0)协议发布,使用本文时请遵守该协议。

    在现代编程语言中,我们经常提及面向对象编程(Object-Oriented Programming,OOP)。所谓的面向对象,其实是一种具有对象概念的程序编程典范,同时也是一种程序开发的抽象方针。与函数式编程(Functional Programming,FP)不同,面向对象编程希望把所有的事物都认为是一个对象,而对象可以通过实例化一个类或继承一个对象而获得(函数式编程认为一切皆函数,一个确定的输入对应一个确定的输出,并且不会产生副作用)。

    面向对象通常可以采用两种方式实现:prototypeclass。JavaScript 中对象的实现显然属于前者,而后者的代表性语言有 Java、C++、TypeScript(TypeScript 同时支持这两种方式) 等。

    本篇文章将会简单地梳理 JavaScript 中的相关问题。读完本文,您应该能了解到包括但不限于以下内容:类与对象、对象创建的几种模式、对象继承的几种模式、对象方法的重写和重载在 JavaScript 中的实现。

    前置知识

    如何理解类和对象

    类是一类事物的抽象概括,它可能包含数据的类型和对数据的处理方法。对象是某个类的实例化。一个生动的例子也许可以帮助我们加深对它们的理解:生物学家会将地球上的生物划分为界、门、纲、目、科、属、种等层面,那么一个类可以是这些层面的一种,用于描述某个层面的生物所具有的独特属性。例如芹菜和胡萝卜同属伞形科植物,仔细观察您也许会发现胡萝卜的茎叶与芹菜的茎叶形态十分相似。在这个例子中,伞形科是一个类,而芹菜和胡萝卜是这个类的实例化,也就是对象。

    对象是程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性,对象里的程序可以访问及经常修改对象相关连的数据。在面向对象程序编程里,计算机程序会被设计成彼此相关的对象。

    函数和方法

    当我们在向身边的人闲聊这些问题时,往往会在不经意间将函数和方法混为一谈。虽然在大部分情况下无伤大雅,但是您必须明白函数和方法有根本上的区别。在 JavaScript 中,函数可以以如下几种方式表示:

    // 1
    function foo() {};
    
    // 2
    var foo = function() {};
    

    一个函数可以被认为是一个代码块(Code Block),从 JavaScript 解释器的角度上说是一个块级作用域,这个作用域在执行时会执行函数内部的代码,有的时候会根据传入的形式参数处理出一些结果。处理出的结果可能会在函数体内部返回出来。而方法通常指类或对象中的函数,它往往会依赖类或对象中的一些属性或其他方法。例如在 JavaScript 中:

    Date.parse();
    

    即使它是一个静态方法,但它仍然是一个方法:它不能脱离 Date 而独立存在。

    特性

    我们将会在下文中探讨面向对象的三大特性——封装、继承和多态,以及如何在 JavaScript 中使用它们。

    封装(Encapsulation)

    所谓封装,是指面向对象的编程语言中隐藏了某一方法的内部逻辑或某一属性的值。对象只对外提供与其它对象交互所必需的接口,我们只需要关注如何使用,而不需要关心这些方法和属性究竟是什么。上一个例子中我们提到了芹菜,我们再向下细分,仍然可以将芹菜的器官作为划分对象的最小单位。它在发育果实时,身体中的某个器官会生成某种激素,用于加快果实的发育。果实作为一个对象,只需要使用激素,而不需要关心激素是如何产生的,更不会参与激素的产生。

    于是,封装的优势就十分明显了:

    • 对内部数据起到一定的保护作用
    • 提高内聚,降低耦合(可以被多种对象复用)

    在 JavaScript 中,封装可以通过下文中几种方式实现

    函数模式:非严格意义的封装

    最简单的封装策略是通过一个立即执行函数(IIFE)和闭包(一个记住了自己所在的块级作用域的函数)的特性返回一个对象字面量:

    const foo = (function() {
      let bar = 0;
      return function(baz) {
        return {
          bar: bar + baz,
        };
      };
    })();
    
    // foo(1).bar = 1;
    

    我们给 foo 赋予了一个 IIFE,它将立刻执行并返回一个函数,这个函数将会返回一个对象,对象中 bar 的值由块级作用域中的 barfoo 的形式参数 baz 共同确定。

    字面量模式:最简单的封装模式

    顾名思义,字面量模式指的是 JavaScript 允许我们直接用 {} 定义一个对象。在 JavaScript 中,对象是使用一个散列表存储数据的,根据对象的键名生成对应的哈希,从不连续的内存空间(堆)中寻找到对应的值。通过字面量模式,我们可以直接创建一个对象。通过这种方式创建的对象并没有显式地实例化一个类,但它确实是 Object 的实例——这也是它的独特之处。

    const foo = {
      bar: '',
    };
    

    通过上面的代码我们可以创建一个名为 foo 的对象,其中包含了一个属性 bar。现在,我们希望使用 foo 实例化 2 个对象,于是,我们应该可以写出下面的代码:

    const foo1 = {};
    foo1.bar = 'baz1';
    
    const foo2 = {};
    foo2.bar = 'baz2';
    

    尝试运行上面的代码,我们可以确认这段代码确实可以帮助我们得到两个 foo 的实例化对象。但事实并非如此。虽然 foo1foo2 的字面量都相同(它们都有名为 bar 的属性,并且都是 string 类型),但是它们都不是 foo 的实例:foo1foo2 都没有和 foo 建立任何关联。字面量模式创建对象的过程非常简单,但是它暴露出的问题也十分明显并致命:

    • 对象无法和类产生关联
    • 若需要批量创建相同规格的对象,将会非常麻烦

    工厂模式:改进的字面量模式

    工厂模式将定义一个函数,这个函数将创建字面量的过程抽象出来了:

    function createFoo(bar, baz, fuz) {
      const foo = {};
      foo.bar = bar;
      foo.baz = baz;
      foo.fuz = fuz;
      foo.saySomething = function() {
        console.log(foo.bar, foo.baz, foo.fuz);
      };
      return foo;
    };
    

    我们可以发现,尽管 createFoo 函数将 foo 的创建过程抽象了,但函数内部仍然按照字面量模式创建了一个临时对象,将这个对象的属性值设置为相应的形式参数的值,最后再返回这个对象。此方法虽然解决了批量实例化对象时给编程人员造成的困扰,但最关键的问题仍未解决。

    构造函数模式:将对象与类联系起来

    一个典型的构造函数模式往往像下面这样:

    function Foo(bar, baz, fuz) {
      this.bar = bar;
      this.baz = baz;
      this.fuz = fuz;
      this.saySomething = function() {
        console.log(this.bar, this.baz, this.fuz);
      };
    };
    

    也许您会认为构造函数模式与之前的工厂模式很相似,但实际上并非如此。构造函数模式并不会显式地创建一个对象,而是将需要设置的属性和值赋予 this。当我们希望创建 Foo 的实例时,我们通常可以这样做:

    const foo = new Foo('fiz', 'buzz', 'lorem');
    foo.saySomething(); // fiz buzz lorem
    

    这样做的精妙之处在于使用 new 调用 Foo 时,Foo 将作为一个构造函数被调用。那么在这个过程中究竟发生了什么呢:

    • 创建一个新的对象
    • 将这个新的对象的 [[Prototype]] 指向构造函数的 [[Prototype]]
    • this 绑定到这个对象上
    • 返回这个对象

    尽管通过 new 调用构造函数的过程十分清晰,但是我们还是有必要对其中的某些概念进行探讨。

    理解 this

    在其它语言中,this 通常用于在类中指代这个类本身,我们往往可以通过这个关键字调用类自身的属性和方法。但是,在 JavaScript 中,这个关键字在函数里也能被访问(这也证明了函数在运行时确实是一个对象)。

    通常,this 永远指向调用这个函数的对象(或这个函数所在的运行时作用域)。this 在词法阶段是无法确定的,也就是说我们不能通过词法作用域判断某个函数中 this 的指向。我们可以分几种情况讨论:

    直接调用

    function foo() {
      console.log(this);
    };
    
    foo(); // window
    

    直接调用时,函数中的 this 将指向直接调用它的对象(如 window)。是的,在严格模式下 this 的指向可能是 undefined,但这显然不是本文要讨论的问题。如果您仍想了解这一有趣的现象,请翻阅:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode

    作为对象方法调用

    function bar() {
      console.log(this);
    };
    
    const foo = {};
    foo.bar = bar;
    foo.bar(); // foo
    bar(); // window
    

    作为对象方法调用时,this 指向调用它的对象。

    作为构造函数调用

    function Foo() {
      this.log = function() {
        console.log(this);
      };
    };
    
    const foo = new Foo();
    foo.log(); // foo
    

    作为构造函数调用时,this 指向通过 new 返回的新对象。

    通过 callapplybind 改变

    这三个方法本质上并没有区别。callapplybind 的区别在于调用前者会立即执行,而调用后者则会返回一个改变了作用域的函数。

    const foo = {};
    const buz = {};
    foo.bar = function() {
      console.log(this);
    };
    foo.bar(); // window
    foo.bar.apply(buz); // buz
    

    理解原型和原型链

    在上文中,我们了解到 JavaScript 通过原型实现面向对象。对此,JavaScript 设计者提出了一个规范的名称:[[Prototype]] 用于描述实例的原型对象。现代化浏览器几乎都实现了这个规范,并将其命名为 __proto__

    每个函数都有作为构造函数的潜质,因此每个函数都有一个隐式的 prototype 属性,指向这个函数的原型对象。所谓的原型对象就是一个隐藏的实例,这个实例被用于建立构造函数和实例的联系。实例中的 __proto__ 也指向这个原型对象。于是,实例和构造函数的关系就建立起来了。简而言之,__proto__ 永远指向构造函数的原型对象,也就是构造函数的 prototype

    function Foo() {};
    Foo.__proto__ === Function.prototype; // true
    

    如果我们把 Foo 也当作一个 Function 的实例,那么 Foo.__proto__Function.prototype 指向的是同一个对象。

    基于上述的关系,一个对象往往可以通过不断使用 __proto__ 属性追溯到最根源的原型对象上。这个原型对象就是 null,这种关系被称为原型链:

    function Foo() {};
    const foo = new Foo;
    
    foo.__proto__.__proto__.__proto__ === null; // true
    // foo.__proto__ = Foo.prototype;
    // foo.__proto__.__proto__.__proto__ = Foo.prototype.__proto__.__ptoto__ = Object.prototype.__proto__ = null;
    

    下面这张图也许能帮助我们更深入地理解原型链:

    8d6f01b7f71083896c2c1b5e327b10b8.png

    原型模式:构造函数模式的优化

    在上文中我们已经明白了原型对象和原型链的基本概念,因此,我们可以进一步通过原型优化类的封装。我们已经知道,prototype 实际上是一个指针,这个指针永远指向这个函数的原型对象。通过对上一节例子的观察我们可以发现:我们每实例化一次,就会为新的实例创建一个新的 saySomething 方法,但是我们并不希望每个 saySomething 方法都不一样(或最好一样)。于是,我们可以通过直接在 prototype 上定义一个方法或属性的方式完成优化:

    function Foo(bar, baz, fuz) {
      this.bar = bar;
      this.baz = baz;
      this.fuz = fuz;
    };
    
    Foo.prototype.saySomething = function() {
      console.log(this.bar, this.baz, this.fuz);
    };
    

    我们可以将一些不会更改的属性和方法使用原型模式设置,这样,每个实例都有一个独一无二的原型对象的副本,也能一定程度地共享方法的引用,避免对堆内存资源的不必要的占用。

    值得注意的是,若一个属性或方法存在于构造函数原型对象上,当我们以后修改这些属性值或方法时,通过该函数构造出的所有对象的相应的属性值或方法均会被改变:

    Foo.prototype.a = 1;
    
    const foo1 = new Foo();
    const foo2 = new Foo();
    
    foo1.a; // 1
    foo2.a; // 1
    
    Foo.prototype.a = 2;
    
    foo1.a; // 2
    foo2.a; // 2
    

    继承(Inheritance)

    继承更像是一种家族关系。我们一般会把 B 从 A 获取了一些属性或方法的现象叫做“B 继承自 A”,那么 A 就相当于 B 的父级类,简称“父类”。同样地,在开篇的例子中,我们通过生物学中的概念理解类和对象:芹菜和胡萝卜同属于伞形科,但芹菜属于芹属,胡萝卜属于胡萝卜属,这两个属的共同父类是伞形科,也就是说它们都继承了伞形科的特征(叶片形状、花序、种子形态等)。同样地,比科更广的是目,所以目是科的父类,属于同一目的生物也都有一定的共同特征,它们的共同特征都来自于这个目所赋予的属性和方法。随着不断向父类追溯,父类会变得越来越抽象(共同特征越来越少):生物学中父类的起点是界,动物界和植物界是占比最大的两个界,动物和植物的共同特征非常少(细胞层面以上的共同特征更是屈指可数)。

    在编程语言中,继承也是如此。最抽象的类一定是继承的起点。在 JavaScript 中,继承的起点是 nullObject 直接继承自它。我们所熟知的继承,一般是从 Object 开始的。如果我们使用字面量模式创建一个对象,那么这个对象就继承自 Object

    基于上述的认知,我们能够开始讨论 JavaScript 从 Object 的子类继承的方式了。

    原型链继承

    原型链继承将子类的原型对象指针指向父类的实例:

    function Person(name, age) {
      this.name = name;
      this.age = age;
    };
    
    function Man() {};
    
    let person = new Person('mike', 20);
    Man.prototype = person;
    
    const mike = new Man();
    const bob = new Man();
    
    console.log(mike); // Man ({ name: 'mike', age: 20 })
    console.log(bob); // Man ({ name: 'mike', age: 20 })
    

    不难发现,我们将 Man 的原型对象指向了父类 Person 的实例,因此,Man 继承自 Person。但是这并不是最理想的继承方式:虽然 Man 继承了 Person,但它的原型对象其实是 Person.prototype子类在实例化过程中并不能向父类传递参数。所以这种方式并未实现真正的继承。

    我们也许可以从下图中理解原型链继承:

    6737dce92b228505a507d743f1786012.png

    经典继承(借用构造函数)

    经典继承在子类中调用父类构造函数的 call 方法(apply 同样也可以):

    function Person(name, age) {
      this.name = name;
      this.age = age;
    };
    
    function Man(name, age) {
      Person.call(this, name, age);
    };
    
    const mike = new Man('mike', 20);
    const bob = new Man('bob', 19);
    
    console.log(mike); // Man { name: 'mike', age: 20 }
    console.log(bob); // Man { name: 'bob', age: 19 }
    

    经典继承的核心思想就是通过 call 改变 this 的指向达到继承的目的。这种方法看似实现了完美的继承,实则不然:

    mike.__proto__ === Person.prototype; // false
    mike.__proto__ === Man.prototype; // true
    Man.__proto__ === Person.prototype; // false
    Man.__proto__ === Function.prototype; // true
    Man.prototype.__proto__ === Person.prototype; // false
    

    子类的原型对象并不是指向父类的:并没有显式地为 Man 的原型对象指向 Person 的实例。因此,通过经典继承得到的子类的原型还是指向自身,prototype.constructor 属性却指向父类构造函数 Person 本身:

    316b45aabb5f4757258c7353fbe9f3e7.png

    因此,我们需要结合经典继承和原型链继承的优点,进一步完善继承机制。

    组合式继承

    上文提到,我们需要结合前两种继承方式。因此,我们提出了“组合式继承”。

    function Person(name, age) {
      this.name = name;
      this.age = age;
    };
    
    function Man(name, age) {
      Person.call(this, name, age);
    };
    
    Man.prototype = new Person();
    Man.prototype.constructor = Man;
    
    const mike = new Man('mike', 20);
    const bob = new Man('bob', 19);
    
    console.log(mike); // Man { name: 'mike', age: 20 }
    console.log(bob); // Man { name: 'bob', age: 19 }
    
    Man.prototype.__proto__ === Person.prototype // true
    

    组合式继承解决了经典继承中原型链断裂的问题:

    6531c805a82f3ae7b1e24db9880c80e6.png

    原型式继承

    不同于原型链继承,原型式继承通过调用 Object.create 方法实现继承:

    function Person(name, age) {
      this.name = name;
      this.age = age;
    };
    
    let person = new Person('mike', 20);
    const mike = Object.create(person);
    
    person = new Person('bob', 19);
    const bob = Object.create(person);
    
    console.log(mike); // Person ({ name: 'mike', age: 20 })
    console.log(bob); // Person ({ name: 'bob', age: 19 })
    

    我们注意到,mikebob 中的属性来自于父类的原型对象中,并不是真正属于自己。这通常会导致原型指针混乱而造成 this 指向不明的问题。

    寄生组合式继承

    寄生组合式继承对组合式继承进一步优化:简化了在更改子类原型对象指向时调用的父类构造函数。我们不难发现,在组合式继承中,当我们希望修改子类的原型对象指向时,通常需要指向父类的实例,这就导致了父类的实例需要被调用两次(第一次在子类构造函数内)。我们结合组合式继承原型链清晰的优点和原型式继承的不需要调用父类构造函数的优点,再次组合:

    function Person(name, age) {
      this.name = name;
      this.age = age;
    };
    
    function Man(name, age) {
      Person.call(this, name, age);
    };
    
    Man.prototype = Object.create(Person.prototype);
    Man.prototype.constructor = Man;
    
    const mike = new Man('mike', 20);
    const bob = new Man('bob', 19);
    
    console.log(mike); // Man { name: 'mike', age: 20 }
    console.log(bob); // Man { name: 'bob', age: 19 }
    

    寄生组合式继承是目前使用最广泛的继承方式。

    混入式继承

    混入式继承遍历父类的所有属性,并赋予子类。

    ES 6 extends

    使用 extends 关键字可以实现从父类继承,但它只是语法糖,其本质仍然是以上组合方式中的一种。

    多态(Polymorphism)

    顾名思义,多态意味着某个方法在不同的条件下会选择不同的动作。多态性包括以下两种特性:

    • 重写(Overwrite):允许子类对方法的实现重写,但签名、形式参数列表和返回值类型都不能改变
    • 重载(Override):允许子类改变方法的形式参数列表、返回值类型,但不允许改变签名,在调用方法时,根据输入的形式参数选择对应的重载

    回到之前的例子中:芹菜和胡萝卜的属性有一部分继承自伞形科,但是芹菜和胡萝卜最大的不同是胡萝卜会形成块状的橘红色根茎,而芹菜不会形成,但这种现象不允许输入的形式参数(土壤、光合作用、矿物质等)和返回值(根茎)是不同的。所以这两种植物在根茎基因表达上的不一致属于重写;在温度、阳光等条件适宜的情况下,芹菜会分化出花蕾,而恶劣条件下的芹菜可能一直都无法分化。这种现象则是重载。

    重写

    在 JavaScript 中,重写现象非常常见。您也许听说过这种说法:

    在判断引用类型时,我们通常不希望直接用 toString 方法,应该用 Object.prototype.toString.call 方法判断

    正是因为有些类(比如 Array)会重写 toString 方法以方便编程人员获取希望的字符串,而不是枯燥的 [object Object]

    JavaScript 的重写机制十分方便。在单页面应用中,我们通常使用 window.history.pushState 管理前端路由。不幸的是,这个方法并不会触发任何的事件。但是我们仍然可以使用重写的方式实现路由变换时的监听:

    03b981f693bcd99ef3f9e3d06a8b0837.png

    重载

    很遗憾,JavaScript 并没有提供重载的实现。这是因为在 JavaScript 中,函数是指针,如果定义了多个同一签名但形式参数列表不同的函数,后面的同签名的函数会覆盖前面的函数(似乎仍然是重写)。

    即便如此,我们仍有办法解决重载的实现。笔者通过判断 arguments 对象的长度来区分同签名的重载函数。我们可以定义一个 overridable 函数,返回一个拥有如下数据结构的对象:

    function overridable() {
      return {
        // 保存所有形式参数数量的函数,数组的 index 对应着形式参数的数量
        functions: Array<Function>,
        // 添加一个函数
        add: Function<Function>,
        // 当所有重载函数都添加完毕后,得到最终的结果
        result: Function;
      };
    };

    在上面的数据结构中,functions 可能让人迷惑:为什么它是一个元素为函数的数组?因为我们在实现的重载功能中,保存函数是通过函数形式参数列表长度实现的,也就是说当我们调用 add 时,会将 functionsindex 为当前传入函数的形式参数列表长度的元素设置为当前函数:

    // add(fn);
    const functionLength = fn.length;
    this.functions[functionLength] = fn;
    

    为了能够链式地调用addadd 最终会返回上述的数据结构:

    // add(fn);
    return {
      functions: this.functions,
      add: this.add,
      result: this.result,
    }
    

    以下代码便是 add 的全部代码:

    add: function(fn) {
      if (typeof fn !== 'function') { return false; }
    
      const functionLength = fn.length;
      this.functions[functionLength] = fn;
    
      return {
        functions: this.functions,
        add: this.add,
        result: this.result,
      }
    },
    

    前面提到,当我们添加完所有可能的重载函数后,我们需要调用 result 方法返回一个函数。为了保存所有重载函数的列表,我们需要在 result 中再返回一个闭包函数:

    result: function() {
      const functions = this.functions;
    
      return function() {
        const functionLength = arguments.length;
    
        if (functions[functionLength]) {
          return functions[functionLength].call(this, ...arguments);
        } else {
          throw new Error('There is no function match ' + functionLength + ' arguments.');
        }
      };
    },
    

    因此,我们能够在这个闭包函数中通过传入的形式参数列表从 functions 中选取合适的函数执行。

    您可以从下面的代码中回顾我们是如何实现 overridable 的:

    function overridable() {
      return {
        functions: [],
    
        add: function(fn) {
          if (typeof fn !== 'function') { return false; }
    
          const functionLength = fn.length;
          this.functions[functionLength] = fn;
    
          return {
            functions: this.functions,
            add: this.add,
            result: this.result,
          }
        },
    
        result: function() {
          const functions = this.functions;
    
          return function() {
            const functionLength = arguments.length;
    
            if (functions[functionLength]) {
              return functions[functionLength].call(this, ...arguments);
            } else {
              throw new Error('There is no function match ' + functionLength + ' arguments.');
            }
          };
        },
      };
    };
    

    使用案例:我们希望实现一个 foo 函数,当传入三个参数时,返回它们三者的和;当传入一个参数时,返回参数的平方:

    const foo = overridable()
      .add(function(a, b, c) {
        return a + b + c;
      })
      .add(function(a) {
        return Math.pow(a, 2);
      })
      .result();
    
    console.log(foo(1, 2, 3)); // 6
    console.log(foo(2)); // 4
    

    虽然我们已经实现了 JavaScript 中函数的重载,但我们的 overridable 并没有经历过严格的测试。这意味着我们无法保证未来 overridable 会出现怎样的问题,或者说我们目前并没有通过测试证明它在所有边界情况下仍然能保持正确的逻辑。因此,请不要将它用于真实的生产开发环境中。

    目前,笔者已将上述代码打包成 NPM 依赖,可以通过 npm i overridable 安装依赖并测试,并在 lenconda/overridable 上开放其源代码。如果您有兴趣完善这份代码,请按照 README 的要求贡献代码。

    参考资料

    • JavaScript 实现函数重载
    • 继承与原型链- JavaScript | MDN
    • JavaScript常用八种继承方案- 掘金
    • 一篇文章理解JS继承——原型链/构造函数/组合/原型式/寄生式/寄生组合/Class extends
    • 彻底理解js中this的指向,不必硬背。
    • Javascript 的 this 用法 - 阮一峰的网络日志
    • Js对象模式 | flura的博
    展开全文
  • 一、JavaScript历史1995年 网景招募了Brendan Eich,艾克在1995年5月花了十天时间就把原型设计出来,网景为了让这门语言搭上Java这个编程语言“热词”,将其改名为JavaScriptBrendan Eich1996年8月,IE3发布,微软...
  • 大家不要被这个感觉高大上的名字给吓着,我没有打算把原型的历史给说一遍,本文只是想帮助大家...1、先从JavaScript创建对象说起大家都知道JavaScript是一门面向对象的语言,但是没有类的概念(除非现在的ES6标准)...
  • 特记:题图来自JavaScript Tutorial​www.tutorialrepublic.com前言这一篇鸽了两个周,实在不能再拖下去了。之所以会拖这么久,除了一方面这一块不像之前那样只是一个小知识点,另一方面是总想着能写出点什么新东西...
  • 0 前言全是干货技术殿堂文章收录在我 GitHub 仓库,欢迎Star/fork: Java-Interview-Tutorial https://github.com/Wasabi1234/Java-Interview-Tutorialthis 是函数运行时,在函数体内部自动生成一个对象,只能...
  • 译者序最近在研究 JavaScript 基础性东西,但是看到对于 执行上下文 解释我发现两种,一种是执行上下文包含:scope(作用域)、variable object(变量对象)、this value(this 值),另外一个种是包含:lexical ...
  • 之前一章节提到了构造函数和类,其中几个重点放在接下来的两部分提一下,主要是this关键字(ES 5就已经了)的使用,对象的继承以及类(ES 6开始引入)的继承,这里先来看看this和对象的继承。1 this 关键字1.1 ...
  • 接上文,JavaScript基础内容中函数。我们这次把函数剩下全部分享完。各位可要用心看喽。11、bind bind 是创建了一个新函数而不是修改一个函数。新函数行为和原来函数行为一样,但他接收者是我们给定的对象,...
  • 一、继承的概念​ 继承是所有的面向对象的语言最重要的特征之一。大部分的oop语言的都支持两种继承:接口继承和实现继承。比如基于类的编程语言Java,对这两种继承都支持。从接口继承抽象方法 (只有方法签名),从类...
  • 为初学者介绍一下这 10 个最常被问到 JavaScript 问题在本文中,我收集了关于Javascript 最常被问到 10 个问题及其答案。这10 个问题大多涉及 Javascript 基础知识,所以如果你刚刚开始学习 JS,最好理解并...
  • 作者 | 阮一峰1、涵义this关键字是一个非常重要语法点...但不管是什么场合,this都一个共同点:它总是返回一个对象。简单说,this就是属性或方法“当前”所在的对象。this.property上面代码中,this就代表prope...
  • 为初学者介绍一下这 10 个最常被问到 JavaScript 问题在本文中,我收集了关于Javascript 最常被问到 10 个问题及其答案。这10 个问题大多涉及 Javascript 基础知识,所以如果你刚刚开始学习 JS,最好理解并...
  • 作者: 阮一峰JavaScript this 原理​www.ruanyifeng.com一、问题由来学懂 JavaScript 语言,一个标志就是理解下面两种写法,可能不一样结果。var 上面代码中,虽然obj.foo和foo指向同一个函数,但是执行...
  • 译者序最近在研究 JavaScript 基础性东西,但是看到对于执行上下文解释我发现两种,一种是执行上下文包含:scope(作用域)、variable object(变量对象)、this value(this 值),另外一个种是包含:lexical ...
  • 作者:樱桃小丸子儿链接:...Object 是 JavaScript 中所有对象的父对象 数据封装类对象:Object、Array、Boolean、Number 和 String 其他对象:Function、Arguments、Math、Date、RegExp、Error说出一些写Ja...
  • JavaScript对象

    2021-01-20 18:19:12
    对象的属性和普通的 javascript 变量基本没什么区别,仅仅是属性属于某个对象。 属性定义了对象的特征 可以通过点符号或者方括号访问或者设置一个对象的属性 对象的名字(可以是普通的变量)和属性的名字都是大小写...
  • JavaScript引擎是执行 JavaScript 代码程序或解释器。JavaScript引擎可以理解为标准解释器,或者以某种形式将JavaScript编译为字节码即时编译器。以下为实现JavaScript引擎流行项目列表:V8 — 开源,由 ...
  • 原文|https://blog.sessionstack.com/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code-ac089e62b12e译文 |https://github.com/qq449245884/xiaozhi/issues/2概述JavaScript引擎是...
  • javascript的对象

    2018-11-02 16:55:01
    在js里对象或者直接称为object,实际上是一些映射对集合,像map,字典一样。 js里7种类型:数字、...句话叫“javascript里一切皆对象”,是因为很多时候原始类型也被自动转换为对象,而函数实际上也是对象...
  • JavaScript对象总结

    2020-04-21 19:51:57
    JavaScript对象属于一种复合数据类型,在对象中可以存储多个不同数据类型属性 对象属于一种复合数据类型,在对象中可以存储多个不同数据类型属性,它几种分类: 内建分类:由ES标准中定义对象,在...
  • javascript对象

    2016-12-19 08:55:00
    对象,在javascript中具有着不可替代作用。 一般而言,我们认为两种方式定义对象: 1、字面形式 var object = { name: 'zhuhuoxingguang', sex: '男', age: 28 }  这属于我们比较熟悉方式...
  • 理解javascript对象继承

    2020-11-24 00:49:51
    比如我们一个“动物”对象的构造函数。 function animal() { this.type = '动物'; } 还有一个“猫”对象的构造函数。 function cat(name,color) { this.name = name; this.color = color; } 我们知道猫也...
  • 一种叫做Host Object,由运行环境提供例如document对象, Dom Node等 Native objects是一种松散的结构并且可以动态的增加属性(property),所有的属性都一个名字和一个值,这个值可以是另一个对象的引用 或者是...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,108
精华内容 443
关键字:

属于javascript对象的有

java 订阅