JS的构造函数,原型继承,Class

1.构造函数 constructor

使用构造函数就可以实现代码的复用,创建具有相似的特征(属性)和行为(方法)的对象。
构造函数必须通过关键字new调用,会创建一个新的内存空间,函数体内部的 this 指向该内存。构造函数的最后一步默认(隐式)返回this,如果手动添加返回值,若为基本数据类型则依然为this,如为引用类型(对象/数组)则返回该引用

function Person(age) {
    this.age = age;
    return age
}
var p = new Person(18);//{age:18}
console.log(p.constructor);//function Person

2.原型链

每个对象具有内部原型__proto__,每个函数具有构造器原型prototype

读取对象属性时,如属性不存在,则会在其__proto__上寻找,如还不存在则继续往上一级寻找。

函数默认的prototype是一个对象,它仅有constructor属性,并指向该函数自身。因此其实例对象也会继承该constructor属性(详见下文 2.4 构造函数)。

obj.__proto__ === obj.constructor.prototype === Fn.prototype

既然Fn.prototype是一个对象,显然对象的构造函数为Object,因此有:

Fn.prototype.__proto__ === Object.prototype

除了直接读取或写入__proto__prototype外,也可以使用以下方法:Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作)。

//以下均为===(全等)关系
new Person().__proto__ 
=== Person.prototype 
=== Object.getPropertyOf(new Person())

console.log(Person.prototype); // {constructor:function Person(){}}
console.log(p.prototype); // undefined

函数也是对象的一种。对于函数作为对象来说,上面的规则同样适用,函数对象都是由Function函数生成的:

function fn(){}
fn.__proto__ === Function.prototype;//true
Function.__proto__ === Function.prototype;//true
Object.__proto__ === Function.prototype;//true
2.1 原型可以共享属性和方法

原型链上的属性会被实例所继承,且为全等关系。这是原型链最大的作用。

function Person() {}
Person.prototype.arr = [0,1,2];
var p1 = new Person() , p2 = new Person();
p1.arr === p2.arr;//true

function Animal(){
  this.arr = [0,1,2];
}
var a1 = new Animal() , a2 = new Animal();
a1.arr === a2.arr;//false

实例中继承的引用类型修改会导致整个原型链的变动。

p1.arr.push(3);
p2.arr//[0,1,2,3]
2.2 判断属性类型
  • hasOwnProperty()
    用于判断属性是否是实例属性。true说明是实例属性,false说明不是实例属性。
  • in
    对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。
    • for ... in ...遍历同样不会区分实例属性和原型属性
2.3 instanceof 和 typeof
  • obj instanceof Object 检测Object.prototype是否存在于obj的原型链上。
    • null instanceof null会报错:Right-hand side of 'instanceof' is not an object
  • typeof XXX 返回XXX类型的字符串,可以为"number", "string", "object", "boolean", "function", "undefined", "symbol"
    • type of null会返回"object"
  • 直接使用Object.prototype.toString.call(XXX)可以得到比较满意的结果
Object.prototype.toString.call('hi') // "[object String]"
function Person() {
    this.age = 20;
};
var a = Person;
function Student() { };
Student.prototype = new Person();//继承原型
var s = new Student();

console.log(Person instanceof Function);//true
//s.__proto__===Student.prototype===new Person()
//new Person.__proto__=== Person.prototype
console.log(s.__proto__.__proto__ === Person.prototype);//true
console.log(s instanceof Person);//true
console.log(s.__proto__ === Student.prototype);//true
console.log(s instanceof Student);//true

2.4 constructor

constructor存在于每一个函数的 prototype 中,指向函数自身。

//以下均为===(全等)关系
    Person
    Person.prototype.constructor
    p.constructor
    p.__proto__.constructor

 //function Function(){} 每个函数都是通过new Function()构造的,包括构造函数
    console.log(Person.constructor);
    console.log(Function.constructor);
    console.log(Object.constructor);

// function Object(){} 每个对象都是通过new Object()构造的
    console.log({}.constructor); 

 //function Array(){} 每个数组都是通过new Array()构造的
    console.log([].constructor); 
2.5 特例 Function 与 Object
  • Object的prototype也是一个类型为"object"的对象,但比一般函数的默认prototype多了一大堆方法,这些方法都是JavaScript对象的系统默认方法。
    Object.prototype.__proto__ === null,这就是JavaScript原型链的终点。(否则若按照通用规则Object.prototype.__proto__ === Object.prototype会造成无限递归)
  • Function
    不同于type of 一般函数.prototype === 'object',为一个包含constructor的对象,type of Function.prototype === 'function'

规定Function.prototype.__proto__ === Object.prototype(这是因为如果按照通用规则Function.prototype.__proto__ === Function.prototype会造成无限递归,且可以让__proto__构成的原型链指向了唯一的终点:Object.prototype.__proto__ === null)

console.log(typeof Function.prototype);// "function"
console.log(Function.prototype);// ƒ () { [native code] } 系统编译好的二进制代码
  • Function instanceof Object //true
    Function.__proto__ == Function.prototype => Function.prototype.__proto__ == Object.prototype
  • Object instanceof Object //true
    Object.__proto__ == Function.prototype => Function.prototype.__proto__ == Object.prototype
  • Function instanceof Function //true
    Function.__proto__ == Function.prototype
  • Object instanceof Function //true
    Object.__proto__ == Function.prototype
2.6 复写原型 & 原型链继承

原型和实例是动态关联的,因此先生成实例再修改原型,实例依然可以继承修改结果。
当原型被复写(本质上只是prototype指向一个新的对象,原原型对象依然存在),原有实例依然继承原原型,新实例继承新原型。此时constructor也为新原型的constructor。

function Animal(){}
var dog = new Animal();
Animal.prototype.age = 18;
console.log(dog.age,dog.constructor);//18 Animal

//通过原型继承,peter可以拿到Human中的实例属性,以及Animal和Animal.prototype中的原型属性
function Human(){}
Human.prototype = new Animal();
var peter = new Human();
console.log(peter.age,peter.constructor);//18 Animal

Animal.prototype = {};
var cat = new Animal();
console.log(dog.age,dog.constructor);//18 Animal
console.log(peter.age,peter.constructor);//18 Animal
console.log(cat.age,cat.constructor);//undefined Object

Animal.prototype = {constructor:Animal};
var duck = new Animal();
console.log(duck.age,duck.constructor);//undefined Animal
  • 缺点 :
    2.1 中所说,原型对象上引用类型的值可以通过实例进行修改,致使所有实例的该引用类型值随之改变,这是原型链继承的弊端之一。另一弊端是无法通过子类向父类中传参。
2.7 借用构造函数继承

借用构造函数继承,是在子类的构造函数中通过 apply ()call () 调用父类构造函数,以实现继承。

function Animal(age) {
    this.age = age;
    this.friends = ["A"]
}
Animal.prototype.shout = "wowow";
function Human({name, age}) {
    this.name = name;
    Animal.call(this, age);
}
var peter = new Human({name:"peter", age:18});
peter.friends.push("B");
var tom = new Human({name:"tom", age:22});
console.log(peter.shout);//undefined
console.log(peter.name, peter.age, peter.friends, peter.constructor);//peter 18 [A,B] Human
console.log(tom.name, tom.age, tom.friends, tom.constructor);//tom 22 [A] Human
  • 缺点:
    这种形式的继承,每个子类实例都会拷贝一份父类构造函数中的方法,作为实例自己的方法,因此每个引用类型也是独立的而非指针,不会相互影响。但相对的,占用内存大,复用性差,且实例一旦生成就和父类无关,父类的修改只能影响到修改之后生成的实例。
2.8 组合继承

将两者结合到一起

  • 将引用类型 / 待传参的方法放到父类中,通过.call进行拷贝,获取独立内存,不会相互影响
  • 将基本类型 / 无参数方法放到父类的原型链上,将父类的实例作为子类的原型,并最终通过原型链继承给实例,占用内存小
function Animal(age) {
    if (age) this.age = age;
    this.friends = ["A"]
}
Animal.prototype.shout = "wowow";
function Human(name, age) {
    this.name = name;
    Animal.call(this, age);
}
Human.prototype = new Animal(5);//继承父类原型
Human.prototype.constructor  = Human;//让子类原型对象的`constructor`属性指向子类自身,因为在上一步中,`constructor`属性被覆盖为父类的构造函数
var peter = new Human("peter", 18);
peter.friends.push("B");
var tom = new Human("tom");
console.log(peter.shout);//wowow
console.log(peter.name, peter.age, peter.friends, peter.constructor);//peter 18 Human [A,B]
console.log(tom.name, tom.age, tom.friends, tom.constructor);//tom 5 Human [A]
console.log(peter instanceof Human);//true
console.log(peter instanceof Animal);//true

3.Class

ES6引入了Class(类)这个概念,通过class关键字可以定义类,使得语法上更类似面向对象语言。但本质上类就是一个构造函数,因此其各方面性质和构造函数相同。
class不存在变量提升,所以需要先定义再使用
类中默认为严格模式,其this不会指向window

3.0 类的定义及成员
class A {};
var B = class {};
var C = class D {
  log() {
    console.log(D.prototype == this.__proto__);
  }
}
new C().log();//true

需要注意的是,这个类的名字是C而不是D,D只在Class的内部代码可用,指代当前类。

  • constructor

    • 此构造方法非彼构造方法,不是Person.constructor
    • 如没有显式定义,会隐式生成一个constructor方法。
    • constructor方法默认(隐式)返回实例对象this,也可以手动添加对象/数组作为返回值。
  • 实例成员
    constructor 中通过 this 声明的成员都称为实例成员,只能通过实例访问
    ES7(ES2016)起类中声明的不加static的成员都视为实例成员

  • 原型成员(在ES7(ES2016)中废弃
    一种特殊的成员,仅在ES6中存在,在类中声明但不加static,既不是实例属性,也不是静态属性。
    原型成员定义在class的prototype上,可同时被类和实例访问(类似python)。

    • 属性和方法可直接被类调用,可以通过继承被实例调用
    • 方法不需要使用 function 关键字,且不使用逗号分隔
    • 由于引用类型的值不会出现在原型属性上,避免了原型链继承的实例引用值修改影响原型链问题
class Person{//定义了一个名字为Person的类
    value = 600;
    constructor(name,age){//构造方法,用来接收参数
        this.name = name;//this代表的是实例对象
        this.age = age;
        this.showAge= function(){
             console.log(this.age);
        }
    }
    say(){//这是一个类的方法,注意千万不要加上function
        return "我的名字叫" + this.name+"今年"+this.age+"岁了";
    }
}
var obj=new Person("laotie",18);
console.log(obj.say());//我的名字叫laotie今年18岁了
console.log(obj.value);//600
obj.showAge();//18
console.log(Person.constructor);//Function
console.log(Person.prototype.constructor);//Person

Person.prototype.say = function(){
  return "我被复写了"
};
console.log(obj.say());//我被复写了

类的本质是将非方法属性添加到实例,将方法添加到原型
以下类A和构造函数B等价:

class A {
    age = 18
    fn() {
        console.log(this.age)
    }

}

let a = new A()
console.log(a) //{age:18}
console.log(a.__proto__, a.__proto__ === A.prototype) //{constructor:  A, fn: ƒ} , true


function B() {
    this.age = 18
}
B.prototype.fn = function() {
    console.log(this.age)
}

let b = new B()
console.log(b) //{age:18}
console.log(b.__proto__, b.__proto__ === B.prototype) //{constructor: B, fn: ƒ} , true
3.1 类的继承

通过extends关键字继承。
子类没有自己的this对象,必须在constructor方法中调用super方法继承父类的this对象。

  • 子类实例化时,先执行子类构造函数,再执行父类构造函数
  • 子类中默认(隐式)存在如下构造函数,通过super将子类的参数传给父类
constructor(){
  super(...arguments)
}
  • 在子类中可以通过super访问父类的原型对象(__proto__)
  • 通过super调用父类的方法时,会绑定子类的this(即super.fn.call(this))
class Animal{
    constructor(age){
        this.age = age;
    }
    shout(){
        return "wowow"
    }
    say(){
        console.log(1,this.shout());
    }
    jump(){
        console.log('father jump');
    }

}

class Person extends Animal{
    //constructor(){
    //  super(...arguments)
    //}
    shout(){
        return "meomeo"
    }
    say(){
        console.log(2,this.shout());
        this.jump();
        super.jump();
    }
    jump(){
        console.log('child jump');
    }
}

var peter = new Person(5);
console.log(peter);//{age: 5}
peter.say();//2 meomeo  child jump  father jump
console.log(peter.constructor);//Person 
console.log(typeof peter);//object
console.log(peter instanceof Person);//true
console.log(peter instanceof Animal);//true
  • 类继承的原型链
    继承包含三种内容:实例成员、静态成员、函数
  class A{
    static bar = "bar"
    foo = "foo"
    show_foo(){
        console.log(this.foo)
    }
  }
  class B extends A{

  }

  let b = new B()
  console.log(b) //{foo: 'foo'}
  console.log(B.bar) //bar
  b.show_foo() //foo

等价于

  function A() {
    this.foo = "foo"
  }
  A.bar = "bar"

  A.prototype.show_foo = function() {
    console.log(this.foo)
  }

  function B() {
    A.call(this) // 继承实例成员
  }

  B.prototype.__proto__ = A.prototype // 继承函数
  B.__proto__ = A // 继承静态成员

  let b = new B()
  console.log(b) //{foo: 'foo'}
  console.log(B.bar) //bar
  b.show_foo() //foo

显然以下内容恒成立:

B.prototype.__proto__ === A.prototype
a.constructor === A.prototype.constructor === A
a.__proto__ === A.prototype
A.__proto__ // Native Code
B.__proto__ === A
//综上
B.prototype.__proto__ === B.__proto__.prototype
3.2 静态属性和静态方法

静态属性/方法指的是 Class 本身的属性/方法, 而不是定义在实例对象上的属性/方法。不需要实例化类,即可直接通过该类来调用。

ES6中,static只能修饰方法,不能修饰属性。class内直接定义的属性都是原型属性。
ES7中,static 可以修饰属性,class内直接定义的属性都是实例属性

  • 静态关键字static,或直接在class外自行添加。
  • 静态属性/方法不会被实例继承,仅会被子类继承,且依然为静态。
  • 子类的静态方法中,可以通过super调用父类的静态属性/方法。
class Box {
    static a() {
        return 100;
    }
    static value = 500
}
Box.b = 1;
class Desk extends Box {
    static a(){
        return super.a() + super.b;
    }
}
console.log(Desk.a()); //101
3.3 类的this指向
  • 类中默认为严格模式,即使匿名函数,其this也不会指向window
  • 静态成员中的this指向类,实例成员中的this指向实例,符合通用的this规则(指向调用者)。同理,如将其赋值给一个变量,再调用该变量时,this指向会改变:
class Logger {
  printName(name = 'there') {
    this.print(`Hello ${name}`);
  }
}
const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined

因此可以采用如下方式保证指向

  1. 在构造方法中绑定this
class Logger {
  constructor() {
    this.printName = this.printName.bind(this);
  }
  // ...
}
  1. 构造函数内使用箭头函数定义方法
class Obj {
    constructor(){
      this.printName=(name = 'there')=>{
        this.print(`Hello ${name}`);
      }
    }
}
  1. 构造函数外使用箭头函数定义方法(class fields中的实验性语法,可能有兼容问题)
class Logger {
    printName = (name = 'there') => {
      this.print(`Hello ${name}`);
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容

  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy阅读 9,506评论 1 51
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,599评论 18 139
  • 第5章 引用类型(返回首页) 本章内容 使用对象 创建并操作数组 理解基本的JavaScript类型 使用基本类型...
    大学一百阅读 3,212评论 0 4
  • 一直以来都比较喜欢像易中天这样的大家,《读城记》这本书是我在图书馆偶然间遇到,因为作者是易中天,我借来了它来读,因...
    城居笔记阅读 2,746评论 2 4
  • 1、演讲训练脸还是往常的烧,再继续锻炼,平时要积累素材。 2、昨天由于早起塞车头下午开始痛,平时要早睡,白天要学习...
    一只永不止步的龙阅读 175评论 0 0