JS19 - 面向对象、构造函数、原型、原型链、继承

183 阅读21分钟
  • For JavaScript, most things are objects.
  • JavaScript 一切皆对象

面向对象

  • 对象:是一个包含相关数据和方法的集合(通常由一些变量和函数组成,我们称之为对象里面的属性和方法)

  • 面向对象:不是固定的语法,是一种编程思想。在过去的编程思想是,每一个功能,都按照需求一步步地逐步完成,这种编程思想被称为面向过程,而面向对象是将功能数据封装在一个对象中,通过对象实现需求,而不必详细了解对象实现的所有过程。

  • 面向对象核心:高内聚低耦合(面向过程的高度封装)

Object-oriented programming(面向对象编程)Object-oriented programming (OOP) is a programming paradigm fundamental to many programming languages, including Java and C++. Object-oriented programming is about modeling a system as a collection of objects, where each object represents some particular aspect of the system. Objects contain both functions (or methods) and data. An object provides a public interface to other code that wants to use it but maintains its own private, internal state; other parts of the system don't have to care about what is going on inside the object.

创建对象

既然面向对象编程是通过对象实现需求,那么第一步就是要了解如何创建对象。

方式 1 - 字面量创建

特例Date() 对象只能通过 new 创建,不能通过字面量的方式

// 1. 字面量创建对象
let personJames = {
    sname:"James",
    sayHi:function(){
        console.log("hello world");
    }
    getName(){  //对象简写
        return this.sname;
    }
}
let personnelData = ["James","Joker","Frank"];

方式 2 - new 内置构造函数

// 2. 内置构造函数
let personJoker = new Object();
personJoker.sname = "Joker";
personJoker.getName = function() {return this.sname};

方式 3 - 工厂函数

// 3. 工厂函数
// 3.1 创建工厂函数
function createPerson(sname = "未取名"){
    //手动创建对象
    var person = {};
    //手动添加属性
    person.sname = sname;
    person.getName = function(){
        return this.sname;
    }
    //手动返回对象
    return person;
}
// 3.2 使用工厂函数去创建对象
console.log(createPerson());                // {sname: '未取名', getName: ƒ}
console.log(createPerson("Hellen").sname);  // Hellen
console.log(createPerson("Tim"));           // {sname: 'Tim', getName: ƒ}

方式 4 - new 自定义构造函数(最常用)

// 4. 自定义构造函数
// 4.1 制造一个自定义构造函数
// 注意:构造函数会自动创建对象,自动返回这个对象,我们只需要手动向里面添加内容就可以了
// 但是,需要和 new 关键字连用,如果不连通,就是一个普通函数
function ConstructorPerson(sname){
    //自动创建对象
    /* new 关键字作用... */
    
    //手动向对象里面添加成员:这里的 this 指向当前实例(即 new 创建的实例化对象)
    this.sname = sname;
    this.getName = function(){
        return this.sname;
    }
    
    //自动返回对象
    /* new 关键字作用... */
}
// 4.2 使用自定义构造函数创建对象
// new 关键字(实例化对象) -> [new 函数名()] 使用该关键字可以使函数成为构造函数,创建新对象
// 第一次调用 ConstructorPerson 的时候,和 new 关键字连用了(即 new ConstructorPerson("James")),
// 此时,new ConstructorPerson("James") 里面的 this 就指向了 personNewObj1 
// 此时函数内手动添加的 sname、getName 两个成员,就添加到了 personNewObj1 对象内
// 第二次调用的整个过程类似
let personNewObj1 = new ConstructorPerson("James");
let personNewObj2 = new ConstructorPerson("Joker");
console.log(personNewObj1); //ConstructorPerson{sname: 'James', getName: ƒ}
console.log(personNewObj2); //ConstructorPerson{sname: 'Joker', getName: ƒ}

方式 5 - Object.create() 方法

//Object.create(); 创建新对象,相当于创新一个对象的复制,但是是继承于原对象的
let foo1 = {name:"James"};
let foo2 = Object.create(foo1);
let foo3 = Object.create(foo2);
//继承:f001{name:"James"};
//原型链;foo3.__proto__ -> foo2.__proto__ -> foo1.__proto__ -> Object.prototype.__proto__ -> null
console.log(foo2.__proto__ == foo1);                //true
console.log(foo2.__proto__ == foo1.prototype);      //false -> 因为只有函数才有原型
console.log(foo3.__proto__ == foo2);                //true
//访问继承属性
console.log(foo3.name);                             //James
console.log(foo2.name);                             //James

方式 6 - class 语法糖

class 语法

  • 解决问题:(1)防止构造函数不与 new 连用的失误;(2)让属性和方法放在一起,增加可读性
  • class 声明:创建一个基于原型继承的具有给定名称的新类。类声明不可以提升(这与函数声明不同)。
  • class 表达式:是用来定义类的一种语法。和函数表达式相同的一点是,类表达式可以是命名也可以是匿名的。
class 类名 {
    //原先ES5 内的构造函数体
    constructor(){
        //....
    }
    static 属性名 = 属性值;  //静态属性
    static 函数名(){};      //静态方法
    
    //直接书写原型上的方法即可
}

静态属性和方法

  • 定义:class 本身的属性/方法,不是定义在实例对象(this)上的属性或构造函数原型上,一般通过 类.属性/函数 访问或调用,而类的实例不能访问或调用。
  • 说明:区别于实例属性/方法,实例属性一般通过 对象.属性this.属性 访问,是属于某个对象的属性,ES6也规定了实例属性必须在 constructor 中进行声明,另外,静态方法主要用于操作静态属性。
  • 语法:均需要 static 关键字,可以被继承,也能通过super调用
  • 典型Math 数学类型的的所有属性和方法都是静态的。
//class 声明 语法 -> 但是不同于 class 表达式,类声明不允许再次声明已经存在的类,否则将会抛出一个类型错误
class Polygon {
  constructor(height, width) {
    this.name = 'Polygon';
    this.height = height;
    this.width = width;
  }
}
class Square extends Polygon {
  constructor(length) {
    super(length, length);
    this.name = 'Square';
  }
}

//class 表达式
const MyClass = class [className] [extends] {
  // class body ...
};

//命名 class 表达式
let Foo = class FooName {
    constructor(){};
    takethis(){
        return FooName.name;
    }
}
console.log(Foo.name);              //FooName -> name 是内置属性返回类名
console.log(new Foo().takethis());  //FooName -> 调用类的方法需要实例化
console.log(FooName.name);          
//Uncaught ReferenceError: FooName is not defined -> 命名类表达式的这个类名只能在类体内部访问

//匿名 class 表达式
let Foo = class{
    constructor(name,age){
        this.name = name;
        this.age = age;
        this.getName();
    }
    getName(){
        return this.name;
    }
}
console.log(new Foo("James",28));           //foo {name: "James", age: 28}
console.log(new Foo("James",28).getName()); //James

构造函数(constructor)

  • 构造器/函数:在 JavaScript 中,构造器其实就是一个普通的函数。当使用 new 操作符来作用这个函数时,它就可以被称为构造方法(构造函数),因此,本质上也是一个函数,只是比较特殊,主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。
  • 重载:一个类可以有多个构造函数,根据参数不同,形成构造函数的重载。
  • 作用:为对象成员变量赋初始值
  • 语法:new运算符一起使用

构造模板/对象模板

构造函数的构造具有相似的特点 this.成员变量 = 形参(与成员变量同名) ,因为构造函数的主要功能是初始化对象,因此,就需要相同的结构(这种结构也是约定俗成的,不具有语法强制性,当看到这种结构的时候,要注意开发者可能是想用来创建对象的),才能保证初始化的对象是结构相同的,类似于用模具生产产品,故而构造函数也称为对象模板。 例如:

//构造函数(对象模板)
function Foo(name,age){
    this.name = name;
    this.age = age;
    this.getName = function(){
        this.name;
    }
    this.getAge = function(){
        this.age;
    }
}

注意事项

自定义构造函数的 注意事项

  1. 首字母大写:约定俗称,非语法要求;

  2. 结尾不加return:new 的过程就是将对象实例化的过程,会自动返回一个由构造函数创建的实例化对象,因此不用在构造函数最后加 return 来返回对象;

  3. 不要当作普通函数:构造函数只有在和 new 关键字连用才和普通函数有所区别,但因为构造函数的目的就是为了批量生产对象,因此不建议当作普通函数,当成普通函数使用,可能凭空给window加上属性,并且构造函数内一般没有最终返回值,就算当成普通函数用,也是没有返回结果的

  4. this指向实例化对象:new 创建的实例化对象,就是 this 的指向,因此,我们在构造函数内部书写的 this.XXX = XXX 就是在向自动创建出来的对象里面添加内容

  5. 推荐写小括号:如果在调用构造函数的时候不传递参数,可以不用写小括号,返回结果不受影响,但是不推荐

  6. 不要写 return:如果在构造函数内部 return 了一个基本数据类型,将不会起到任何作用,这条语句相当于作废;如果在构造函数内部 return 了一个复杂数据类型,将会替换掉 new 关键字自动返回的对象,返回 return 的这个对象,即没有办法正确创建对象

//下面这个函数虽然名称首字母大写,但并不是构造函数
function Foo(name,age){
     return{
         name:name,
         age:age
     }
}
//问题 1:没有使用this,会导致 new 关键字在创建构造函数的时候,对象无法调用构造函数的成员变量,达不到初始化的目的
//问题 2:虽然能够返回一个对象,但在原型链上继承的是Object,并非构造函数,因此不是由构造函数初始化的对象

解决构造函数的不合理处 - 原型

function Person(name,age){
    this.name = name;
    this.age = age;
    this.sayHi = function () { console.log(`Hello I'm ${this.name}, ${this.age} years old`)};
}
let p1 = new Person("James",18);
let p2 = new Person("Joker",20);
console.log(p1.sayHi == p2.sayHi);  //false -> 说明创建了两个函数
/**构造函数的不合理:
 *  只要 new 创建一次对象,就会一个函数占用空间,但其实需要一个函数就足够了
 * 不合理问题出现的原因:
 *  第一次执行 Person 函数的时候,this 指向 p1,
 *  此时,会把构造函数内的代码全部执行一遍,向 p1 添加 name、age、sayHi 的属性和方法
 *  第二次执行的时候,则会向 p2 再执行一遍,以此类推
 * 解决这个问题的办法:原型
 *  p1 的 sayHi 方法和 p2 的 sayHi 方法都是使用的 Person 构造函数的原型上的方法,
 *  因此,只需要向 Person 的原型上添加一些方法,所有的 Person 实例对象都可以使用,
 *  并且使用的都是同一个函数,不会存在浪费空间的现象
 */     

object.constructor 属性

对象一般都会有一个 constructor 属性,该属性返回实例化该对象的那个构造函数,因此主要是用于记录该对象引用于哪个构造函数。

function Foo(sname){
    this.sname = sname;
    this.getName = function(){
        this.sname;
    }
}
console.log(Foo.constructor); 
//ƒ Function() -> 所有的函数都是继承 Function,因此所有的函数都 "来源" 于 Function
console.log(instanceOfFoo.Foo); 
//function Foo(sname) 

探究 new 原理

明确 new 关键字创建实例化对象的过程:

  1. 创建空对象
  2. 将空对象的原型链指向构造函数的原型(取得构造函数的属性和方法)
  3. 将构造函数的this指向创建的空对象,并传参(为对象赋初始值-初始化对象)
  4. 返回新创建的对象
function iNew(){
    //1. 创建空对象,并获取构造函数
    var iObject = {};
    var constructorFn = [].shift.call(arguments);
    //2. 原型链指向构造函数原型
    iObject.__proto__ = constructorFn.prototype;
    //3. this指向创建的对象 -> 为对象赋初始值-初始化对象
    // 不能使用call,因为传递的参数可能不止一个,
    // 如果不传参数那么将无法做到new关键字那样,
    // 给未传参的 key 赋值 undefined
    //   [...arguments].forEach(item=>{
    //       constructorFn.call(iObject,item);
    //   });
    constructorFn.apply(iObject,arguments);
    //4. 返回创建的对象
    return iObject;
}

function Foo(sname){
    this.sname = sname;
    this.getName = function(){
        this.sname;
    }
}

let a = iNew(Foo);
let b = iNew(Foo,"James");
let c = new Foo();
let d = new Foo("James");

console.log(a); //Foo {sname: undefined, getName: ƒ}
console.log(b); //Foo {sname: 'James', getName: ƒ}
console.log(c); //Foo {sname: undefined, getName: ƒ}
console.log(d); //Foo {sname: 'James', getName: ƒ}

原型及原型链

相较于基于类的语言(Java、C++等),JavaScript 是动态的,本身不提供一个 class 的实现。即便是在 ES2015/ES6 中引入了 class 关键字,但那也只是语法糖,JavaScript 仍然是基于原型的。

原型 prototype

  • 定义:每个函数中天生自带的一个属性,本质上是一个对象,所以也称为原型对象注意,任何函数都有原型。而构造函数也是一个函数,因此也会有这个天生自带的属性 prototype,既然 prototype 是一个对象,我们就可以使用对象操作的语法,向里面添加一些内容。
  • 功能:一般情况下,在构造函数体内直接向实例化对象添加方法的行为并不好,因为这样会导致多于函数占据空间,因此一般在原型上添加方法,由此,原型就是专门保存由构造方法所实例化对象共有属性或方法
  • 属性 constructorFoo.prototype.constructor 指向这个原型对象 prototype 所对应的构造函数

隐式原型(__proto__) - 原型链

  • 实例化:通过构造函数创建对象的过程,创建出来的对象就叫做构造函数的一个实例化对象
  • 隐式原型:每个对象都有一个私有的 __proto__ 属性,称为隐式原型。
  • 原型链:用 __proto__ 串联起来的对象链状结构(注意是使用 __proto__,其它的都不行),因此,每一个对象数据类型,都会有一个属于自己的原型链。
  • 原型链作用:访问对象成员
  • 原型链顶端Object.prototype,由于 null 没有原型(prototype),从而成为 原型链 的最后一个环。
  • 对象访问机制:任何一个对象的隐式原型 __proto__ 都指向创建该对象的构造函数的原型对象 prototype ,而该原型对象也有一个自己的隐式原型 __proto__,指向上一级构造函数的原型对象 prototype,以此规律,层层向上直到一个对象的 prototype 为 null,因此,如果我们访问某个对象的成员,该对象会首先查看自身有没有这个属性,如果没有,则会自动地去所属构造函数的原型(prototype)上查找,如果构造函数也没有,则会继续向原型对象的构造函数的原型上查找,直到 Object.prototype 都没有,返回 undefined。

原型及原型链特性

  1. 每个对象都有 __proto__ 私有属性和 constructor, 且 实例化对象.__proto__ === 构造函数.prototype
  2. 每个函数都有 __proto__prototype(JavaScript中函数为第一等公民)
  3. Function 是所有函数的父类(包括最大级别对象的构造函数 Object()),因此,Foo.__proto__ Object.__proto__ Function.__proto__ 都指向 Function.prototype,即 Function 自己是自己的构造函数,同时自己也是自己的实例对象
  4. Object 是所有对象的父类(包括原型对象 prototype),但原型对象有一个特例( Function.prototype 是一个特殊函数,检查是不是函数可以用call方法,不是函数不能调用)
  5. Function.prototype虽然是特殊函数,但只有 __proto__,没有 prototypeFunction.prototype.__proto__ 指向 Object.prototype
  6. 原型链的尽头Object.prototype,所有对象均能从 Object.prototype 继承属性,而Object.prototype.__proto__null

原型链图解

原型链图解.png

继承

继承(inherit):使用现有类型,创建出新的类型,新的类型中具有现有类型的属性和方法,也可以拓展出现有类型没有的属性和方法(青出于蓝)。 因此,如果某个对象使用了属性和方法,不一定具有这个属性和方法。

继承的方式

  • 继承的途径主要有:构造函数继承、原型继承、组合继承、ES6 继承(super/extends)
//组合继承:通过构造函数+原型继承
function Person(name,age){
    this.name = name;
    this.age = age;
}
Person.prototype.say = function(){
    return `Hello, I'm ${this.name}, ${this.age} years old.`
}
console.log(new Person("James",18).say());  //Hello, I'm James, 18 years old.

//构造函数继承 - 只能继承属性
function Teacher(){
    Person.apply(this,arguments);           //继承Person -> 在这里通过构造函数继承,相当于是把实例化之后的Teacher中的Person的this指向到Teacher
}
console.log(new Teacher("Joker",28));       //Teacher {name: 'Joker', age: 28} -> 成功继承了Person的属性
// console.log(new Teacher("Joker",28).say()); //为继承方法 -> Uncaught TypeError: (intermediate value).say is not a function

//原型继承 - 只能继承原型上的方法
// Teacher.prototype = Person.prototype; //虽然能够完成继承Person的方法,但是由于prototype是引用类型,修改其中一个都会相互影响
Teacher.prototype = new Person();   //通过实例化,可以将方法继承,而且不会相互影响,但是也把属性也一起继承了,而且继承的还是undefined的值(相当于虽然继承了方法,但继承了过多的废料)
console.log(new Teacher("Alice",16).say()); //Hello, I'm Alice, 16 years old.

/* ES6 继承 */
class Person {
    constructor(name,age){
        this.name = name;
        this.age = age;
    }
    sayHi(){
        return `Hello, I'm ${this.name}, ${this.age} years old.`
    }
}
// console.log(new Person("James",28).sayHi());    //Hello, I'm James, 28 years old.
//继承方法和属性
class Student extends Person { 
    constructor(name,age,job){
        super(name,age);    //super() -> 继承属性 -> 等同于 Person.prototype.call(this,name,job);
        this.job = job
        // this.sayHi(); //这样方式调用原型方法,可以在new实例化之后立即执行方法,不用再手动调用
    }
    //如果在此处再次书写sayHi(),将会覆盖父类的方法;
    //如果不书写,则会直接调用父类的方法,如果想新增内容,可以使用super
    sayHi(){
        return super.sayHi() + ` And I'm a ${this.job} student.`;     //super.方法 -> 继承方法
    }
}
console.log(new Student("Alice",18,"college").sayHi()); //Hello, I'm Alice, 18 years old. And I'm a college student.

实例 - 渲染列表

<body>
    <ul class="recommend"><li></li><li></li><li></li></ul>
    <ul class="favorite"><li></li><li></li><li></li></ul>
    <ul class="detail"><span></span><li></li><li></li><li></li></ul>
</body>
<script>
    let data_1 = {
        title:"recommend",
        content:["item 1","item 2","item 3"]
    }
    let data_2 = {
        title:"favorite",
        content:["item 4","item 5","item 6"]
    }
    //如果传入的数据有新的key,就需要将原型中的方法做改进,这就涉及到继承
    let data_3 = {
        title:"detail",
        pwd:123584561,
        content:["item 4","item 5","item 6"]
    }
    function Display(selector){
        this.ul = document.querySelector(selector);
        this.lis = this.ul.querySelectorAll("li");
    }
    Display.prototype.render = function(value){
        let newNode = document.createElement("div");
        newNode.innerText = value.title;        
        this.ul.insertBefore(newNode,this.ul.firstChild);
        Array.from(this.lis).forEach((item,index)=>{
            item.innerText = value.content[index];
        });
    }
    //继承属性和方法
    function DisplayPwd(selector){
        //继承
        Display.call(this,selector);
        //增加
        this.span = this.ul.querySelector("span");
    }
    DisplayPwd.prototype.render = function(value){
        //继承
        Display.prototype.render.call(this,value);
        //增加
        this.span.innerText = `pwd:${value.pwd}`;
    };

    new Display(".recommend").render(data_1);
    new Display(".favorite").render(data_2);
    new DisplayPwd(".detail").render(data_3);
    /* 页面输出
        recommend
        item 1
        item 2
        item 3
        favorite
        item 4
        item 5
        item 6
        detail
        pwd:123584561
        item 4
        item 5
        item 6
     */
</script>

继承属性

  • 动态的属性“包”:当试图访问一个对象的属性时,不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。

  • 根据 ECMAScript 标准,someObject.[[Prototype]] 用于指向 someObject 的原型(被构造函数创建的实例对象的 [[Prototype]] 指向 func 的 prototype 属性),但如果直接通过点标记someObject.[[Prototype]] 访问,会报语法错,因此,从 ECMAScript 6 开始,[[Prototype]] 可以通过 Object.getPrototypeOf()Object.setPrototypeOf() 访问。这个等同于 JavaScript 的非标准但许多浏览器实现的属性 __proto__,但它不应该与构造函数 func 的 prototype 属性相混淆。

function Foo(age){
    this.name = "James";
    this.age = age;
}
//1. 实例化构造函数 Foo,实例化的对象具有 name 和 age 的属性值
let oFoo1 = new Foo(18);
let oFoo2 = new Foo(28);
console.log(oFoo1);      //Foo {name: 'James', age: 18}
console.log(oFoo2);      //Foo {name: 'James', age: 28}

//2. Foo 构造函数的原型上还没有name和age属性(因为还没有共有属性)
console.log(Foo.prototype.name);    //undefined
console.log(Foo.prototype.age);     //undefined

//3. 在 Foo 构造函数的原型上新定义属性
// (添加共有属性 -> 概念:prototype 专门保存实例化对象的共有属性)
Foo.prototype.name = "Joker";
Foo.prototype.job = "developer";
console.log(Foo.prototype.name);    //Joker
console.log(Foo.prototype.age);     //undefined
console.log(Foo.prototype.job);     //developer
/**注意不要在Foo构造函数的原型上直接定义 Foo.prototype = {name:"Joker",job:"developer"}
 * 这样会直接打破原型链,因为如果原型链完好,oFoo.__proto__ === Foo.prototype 结果是 true,
 * 但由于 Foo.prototype = {name:"Joker",job:"developer"} 打破原型链之后,其结果就变成了false 
 * 如果需要重新定义Foo.prototype,需要重新实例化对象ofoo,重新建立原型链
 * */ 

//访问自有和共有属性 - 补充概念:property shadowing 属性遮蔽
//oFoo1和oFoo2两个实例化对象都有name的私有属性吗?有,就直接输出
//但此时Foo原型也有一个name,但不会访问了,这种优先访问私有属性,在访问共有属性的情况,
//称为:属性遮蔽(property shadowing):共有和自有属性互不干扰,且会优先访问对象的私有属性
console.log(oFoo1.name);            //James
console.log(oFoo2.name);            //James
//oFoo1和oFoo2两个实例化对象都有age的私有属性吗?有,就直接输出
console.log(oFoo1.age);             //18
console.log(oFoo2.age);             //28
//oFoo1和oFoo2两个实例化对象都有job的私有属性吗?没有,就看原型有没有,有就直接输入
console.log(oFoo1.job);             //developer
console.log(oFoo2.job);             //developer
//oFoo1和oFoo2两个实例化对象都有salary的私有属性吗?没有,就看原型有没有,
//如果原型也没有,就看原型的原型有没有,
//直到 oFoo1.__proto__.__proto__.__proto__ === null,停止搜索,返回undefined
//补充:一般情况下:
//oFoo1.__proto__ === Foo.prototype;
//oFoo1.__proto__.__proto__ === Object.prototype;
//oFoo1.__proto__.__proto__.__proto__ === null;
console.log(oFoo1.salary);          //undefined
console.log(oFoo2.salary);          //undefined

//指定访问共有属性
console.log(oFoo1.__proto__.name);   //Joker -> 指向存储共享属性的原型,绕过了自有属性

//访问原型(共有属性集合;两种方式)
/**注意浏览器中的 [[prototype]]:
 * 实例化对象的 [[prototype]] 同 __proto__ 类似,指向构造函数的prototype属性,
 * 但如果直接通过点标记 someObject.[[Prototype]] 访问,会报语法错,
 * 因此,从 ES6 开始,[[Prototype]],
 * 可通过 Object.getPrototypeOf() 和 Object.setPrototypeOf() 访问。
 *     -> oFoo.[[Prototype]].name;      //语法报错
 *     -> Object.getPrototypeOf(oFoo);  //等同于 oFoo.__proto__
 * 
 */ 
console.log(oFoo1.__proto__);                //{name: 'Joker', job: 'developer', constructor: ƒ}
console.log(Object.getPrototypeOf(oFoo1));   //{name: 'Joker', job: 'developer', constructor: ƒ}

//给对象设置属性会创建自有属性,
//获取和设置行为规则的唯一例外是当继承的属性带有 getter 或 setter 时。
//创建自有属性
oFoo1.car = "volvo"; 
console.log(oFoo1.car);             //volvo
console.log(oFoo2.car);             //undefined
//自有属性 - 原型访问不到
console.log(oFoo1.__proto__.car);   //undefined
console.log(oFoo2.__proto__.car);   //undefined

prototype 和 Object.getPrototypeOf

对于一个函数 function A,如果执行 var a1 = new A(); var a2 = new A(); 那么 a1.doSomething 事实上会指向 Object.getPrototypeOf(a1).doSomething,它就是你在 A.prototype.doSomething 中定义的内容。

也就是说:Object.getPrototypeOf(a1).doSomething == Object.getPrototypeOf(a2).doSomething == A.prototype.doSomething

补充:实际上,执行 a1.doSomething() 相当于执行 Object.getPrototypeOf(a1).doSomething.call(a1)==A.prototype.doSomething.call(a1)

简而言之, prototype 是用于类的,而 Object.getPrototypeOf() 是用于实例的(instances),两者功能一致。

[[Prototype]] 看起来就像递归引用,如 a1.doSomethingObject.getPrototypeOf(a1).doSomethingObject.getPrototypeOf(Object.getPrototypeOf(a1)).doSomething 等等等,直到它被找到或 Object.getPrototypeOf 返回 null

因此,当你执行:

var o = new Foo();

JavaScript 实际上执行的是:

var o = new Object();
o.__proto__ = Foo.prototype;
Foo.call(o);

接着如果执行:

o.someProp;

就会检查 o 是否具有 someProp 属性。如果没有,它会查找 Object.getPrototypeOf(o).someProp,如果仍旧没有,它会继续查找 Object.getPrototypeOf(Object.getPrototypeOf(o)).someProp

继承方法

  • 方法即属性:在 JavaScript 里,任何函数都可以添加到对象上作为对象的属性,这就不同于其他基于类的语言所定义的"方法"。函数的继承与其他的属性继承没有差别,包括上面的“属性遮蔽”,即所谓的"方法遮蔽"这种情况相当于其他语言的方法重写。
  • 一般情况下,构造函数内设置属性,原型上设置函数
function Foo(name,age){
    this.name = name;
    this.age = age;
}
let foo = new Foo("James",18);
//创建子孙对象
let inheritFoo = Object.create(foo);
console.log(foo);                   //Foo {name: 'James', age: 18}
console.log(foo.__proto__);         //{constructor: ƒ}constructor: ƒ Foo(name,age)
console.log(inheritFoo);            //Foo {}
console.log(inheritFoo.__proto__);  //Foo {name: 'James', age: 18}

字面量创建对象 - 继承和原型链

普通对象

//1. 一般对象
let obj = {name:"James"};
//继承: Object.prototype
//原型链:obj.__proto__ -> Object.prototype -> null

数组对象

//2. 数组对象
let arr = ["James","Joker","Alice"];
//继承:Array.prototype
//原型链:arr.__proto__ -> Array.prototype.__proto__ -> Object.prototype -> null

函数对象

//3. 函数对象
function foo(){
    let name = "Grace";
    return name;
}
//继承:Function.prototype
//原型链:foo.__proto__ -> Function.prototype.__proto__ -> Object.prototype -> null

构造函数创建对象 - 继承和原型链

function Foo(name,age){
    this.name = name;
    this.age = age;
}
let foo1 = new Foo("James",28);
console.log(foo1.__proto__ === Foo.prototype); //true
//继承:Foo.prototype(修改之前的原型)
//修改前的原型,还没有自定义方法 console.log(foo1.changeName())//Uncaught TypeError: foo1.changeName is not a function
//原型链:foo1.__proto__ -> Foo.prototype.__proto__ -> Object.prototype.__proto__ -> null

//修改原型,设置自定义方法
Foo.prototype = {
    changeName:function(name){
        this.name = name
    }
}
//那么此时需要重新实例化对象,才能继承新的原型
let foo2 = new Foo("James",18);
console.log(foo1.__proto__ === Foo.prototype);  //false 原来的原型链被打破
console.log(foo2.__proto__ === Foo.prototype);  //true 重新建立的原型链
//使用继承的changeName()方法
foo2.changeName("Alice");
console.log(foo2.name);     //Alice
//原型链:foo1.__proto__ -> Foo.prototype.__proto__ -> Object.__proto__ -> null

Object.create() 方法创建对象 - 继承和原型链

//Object.create(); 创建新对象,相当于创新一个对象的复制,但是是继承于元对象的
let foo1 = {name:"James"};
let foo2 = Object.create(foo1);
let foo3 = Object.create(foo2);
//继承:f001{name:"James"};
//原型链;foo3.__proto__ -> foo2.__proto__ -> foo1.__proto__ -> Object.prototype.__proto__ -> null
console.log(foo2.__proto__ == foo1);                //true
console.log(foo2.__proto__ == foo1.prototype);      //false -> 因为只有函数才有原型
console.log(foo3.__proto__ == foo2);                //true
//访问继承属性
console.log(foo3.name);                             //James
console.log(foo2.name);                             //James

属性查找性能调优

在原型链上查找属性是一个非常耗时的过程,特别是当试图访问不存在的属性时会遍历整个原型链,这对性能带来较大压力,而且通过点标记查找属性返回undefined也不能说明就不包含这个属性,有可能是这个属性存在,但是值为undefined,也就是假删除的情况,因此需要借助 Object.prototype 给下面继承的 hasOwnProperty 方法以及 Object.keys() 方法,这两种方法,都只会检测本对象是否存在某个属性,而不会遍历整个原型链。

hasOwnProperty(“属性名”) 方法

let foo = {work:"developer"};
let foo1 = Object.create(foo);
let foo2 = Object.create(foo);
foo1.name = "James";
foo2.name = "Alice";
console.log(foo.hasOwnProperty("name"));    //false
console.log(foo.hasOwnProperty("age"));     //false
console.log(foo.hasOwnProperty("work"));    //true

console.log(foo1.hasOwnProperty("name"));   //true
console.log(foo1.hasOwnProperty("age"));    //false
console.log(foo1.hasOwnProperty("work"));   //false

console.log(foo2.hasOwnProperty("name"));   //ture
console.log(foo2.hasOwnProperty("age"));    //false
console.log(foo2.hasOwnProperty("work"));   //false

Object.keys(对象名) 方法

  • 功能Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的字符串数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致。
  • 语法Object.keys(obj)
  • 说明:返回的数组,是由属性所组成的,其排列顺序和有序遍历的顺序是一样的
let foo = {work:"developer",level:"manager"};
let foo1 = Object.create(foo);
let foo2 = Object.create(foo);
foo1.name = "James";
foo2.name = "Alice";
console.log(Object.keys(foo));  //["work","level"]
console.log(Object.keys(foo1)); //["name"]
console.log(Object.keys(foo2)); //["name"]

案例

优化选项卡

<!-- html部分 -->
<style>
    * {
        margin: 0;padding: 0;
    }
    html,body {height: 100%;}
    section {
        height: 45%;
        display: block;
        border: 1px solid black;
    }
    .nav {
        height: 20%;
        display: flex;
        list-style-type: none;
    }
    .nav li {
        width: 100%;
        height: 100%;
        display: flex;
        justify-content: center;
        align-items: center;
        background-color: black;
        color: white;
    }
    .nav li:nth-of-type(1) {
        background-color: white;
        color: black;
    }
    .content {
        width: 100%;
        height: 80%;
        list-style-type: none;
        position: relative;
    }
    .content li {
        width: 100%;
        height: 100%;
        position: absolute;
        text-align: center;
        display: flex;
        justify-content: center;
        align-items: center;
        font-size: 50px;
        visibility: hidden;
    }
    .content li:nth-of-type(1) {
        visibility: visible;
    }
</style>
<script src="./js28 面向对象-选项卡.js" type="module"></script>
<body>
    <section>
        <ul class="nav">
            <li>1</li><li>2</li><li>3</li><li>4</li>
        </ul>
        <ul class="content">
            <li>1</li><li>2</li><li>3</li><li>4</li>
        </ul>
    </section>
    <section>
        <ul class="nav">
            <li>1</li><li>2</li><li>3</li><li>4</li>
        </ul>
        <ul class="content">
            <li>1</li><li>2</li><li>3</li><li>4</li>
        </ul>
    </section>
</body>
<script type="module">
    //需求功能:通过面向对象的思维实现,封装函数,实现选项卡功能,例如:new Tabs(".nav","click"); 一般情况下,构造函数内设置属性,原型上设置函数
    //直接通过实例化对象实现功能 + 模块化语法 -> 一次封装多次调用
    import CustomTabs from "./js28 面向对象-选项卡.js";
    new CustomTabs("section:nth-of-type(1)", "click").change();
    new CustomTabs("section:nth-of-type(2)", "mouseover").change();
</script>
//js部分
//1. 创建构造函数 Tabs,并设置属性
function Tabs(selector, type) {
    //这里使用let是因为section仅在构造函数内使用一次
    let section = document.querySelector(selector);
    //这里使用this,是因为需要在原型定义方法的时候使用
    this.type = type;
    this.navBars = section.querySelectorAll(".nav>li");
    this.contents = section.querySelectorAll(".content>li");
}
//2. 在将方法放在原型上,如果要调用里面的方法,就需要实例化对象,this指向的就是这个实例化对象
//因此,为了防止this指向错误,例如:
//Array.from(arr).forEach(function(){ this }) 中的this指向的是window
//Element.addEventListener(this.type,function(){ this }) 中的this指向ELement元素
//最好使用箭头函数,让this指向父级,在这里就是实例化的 Tabs
Tabs.prototype.change = function () {
    Array.from(this.navBars).forEach((item,index) => {
        item.addEventListener(this.type, () => {
            Array.from(this.navBars).forEach(function(ele){
                ele.style.backgroundColor = "black";
                ele.style.color = "white";
            });
            item.style.backgroundColor = "white";
            item.style.color = "black";
            Array.from(this.contents).forEach(ele=>{
                ele.style.visibility = "hidden";
            })
            this.contents[index].style.visibility = "visible";
        })
    });
}
export default Tabs;    //模块化语法

通过原型思想实现选项卡功能.gif