乐趣区

关于node.js:万字长文深度剖析面向对象的javascript

简介

本将会深刻解说面向对象在 javascript 中的利用,并具体介绍三种对象的生成形式:构造函数,原型链,类。

什么是对象

尽管说程序员不缺对象,随时随地都能够 new 一个进去,然而在程序的世界中,对象到底是什么呢?

对象是单个实物的形象。

对象是一个容器,封装了属性(property)和办法(method)。

而面向对象是绝对于面向过程来讲的,面向对象办法,把相干的数据和办法组织为一个整体来对待,从更高的档次来进行零碎建模,更贴近事物的天然运行模式。

面向对象的益处就是可形象,封装和可重用性,同时提供了继承和多态等十分有用的个性。

而随着 JS 的倒退,曾经超过了最开始的脚本语言,尤其是 nodejs 的呈现之后,更是极大的丰盛了 js 的工作能力。

所以 JS 也须要进行对象化。

一般来说,在 JS 中构建对象有三种形式:

  • 构造函数(constructor)
  • 原型链(prototype)
  • 类 (class) —ES6 提供

接下来,咱们一一来解说。

构造函数

构造函数是专门用来生成对象的函数。它提供模板,形容对象的根本构造。

一个构造函数,能够生成多个对象,这些对象都有雷同的构造。构造函数的写法就是一个一般的函数,然而有本人的特色和用法.

var Book  = function () {this.name = 'www.flydean.com';}

Book 就是构造函数,它提供模板,用来生成实例对象。为了与一般函数区别,构造函数名字的第一个字母通常大写。

构造函数的特点

构造函数首先是一个函数,也就是说是 function 结尾的函数。其次函数体外部应用了 this 关键字,代表了所要生成的对象实例。

在应用构造函数的时候,必须用 new 命令,调用 Book 函数。

new 命令的作用,就是执行构造函数,返回一个实例对象。

var Book  = function () {this.name = 'www.flydean.com';}

var b1 = new Book();
console.log(b1.name);

下面的例子输入后果:

www.flydean.com

如果咱们忘了应用 new,会产生什么状况呢?

var Book  = function () {this.name = 'www.flydean.com';}

var b2 = Book();
console.log(name);
console.log(b2.name);

第一个输入会输入 www.flydean.com

而第二个则会报一个谬误:

TypeError: Cannot read property 'name' of undefined

因为这样调用的 this 指向的是 global, 所以 this.name 变成了全局变量。

为了防止这种遗记写 new 的问题,能够在第一行加上 use strict,在严格模式中,函数外部的 this 不能指向全局对象,默认等于 undefined,导致不加 new 调用会报错。

如果不想应用 use strict, 则能够在构造函数外部判断是否应用 new 命令,如果发现没有应用,则间接返回一个实例对象。

function Person(firstname,lastname){if(!(this instanceof Person)){return new Person(firstname,lastname);
    }
    this.firstname= firstname;
    this.firstname = lastname;
}

console.log(Person("jack","ma").firstname);
console.log((new Person("jack","ma")).firstname);

new 命令的原理

应用 new 命令时,它前面的函数调用就不是失常的调用,而是顺次执行上面的步骤:

  1. 创立一个空对象,作为将要返回的对象实例
  2. 将这个空对象的原型,指向构造函数的 prototype 属性
  3. 将这个空对象赋值给函数外部的 this 关键字
  4. 开始执行构造函数外部的代码

如果构造函数外部有 return 语句,而且 return 前面跟着一个对象,new 命令会返回 return 语句指定的对象;否则,就会不论 return 语句,返回 this 对象。

var Book  = function () {
    this.name = 'www.flydean.com';
    return {author:'flydean'};
}

console.log((new Book()).author);

函数外部能够应用 new.target 属性。如果以后函数是 new 命令调用,new.target 指向以后函数,否则为 undefined。

通过 new.target 咱们也能够用来判断对象是否通过 new 来创立:

function f(){if(! new.target){throw new Error('请应用 new 命令!');
    }
}
f();

构造函数作为模板,能够生成实例对象。然而,有时只能拿到实例对象,而该对象基本就不是由构造函数生成的,这时能够应用 Object.create() 办法,间接以某个实例对象作为模板,生成一个新的实例对象。

var book2 = {
    name : '三毛流浪记',
    author : '三毛',
    getName : function () {console.log('book name is:' + this.name);
    }
}
var book3 = Object.create(book2);
console.log(book3.name);
book3.getName();

prototype 对象

构造函数有什么毛病呢?构造函数的毛病就是会将构造函数外部的对象都复制一份:

function Book(){
    this.name ='www.flydean.com';
    this.getName =function (){console.log('flydean');
    }
}

var book1 = new Book();
var book2  = new Book();

console.log(book1.getName  === book2.getName);

输入后果是 false。阐明每次 new 一个对象,对象中的办法也被拷贝了一份。而这并不是必须的。

JavaScript 的每个对象都继承另一个对象,后者称为“原型”(prototype)对象。只有 null 除外,它没有本人的原型对象。

原型对象上的所有属性和办法,都能被派生对象共享。这就是 JavaScript 继承机制的根本设计。

通过构造函数生成实例对象时,会主动为实例对象调配原型对象。每一个构造函数都有一个 prototype 属性,这个属性就是实例对象的原型对象。

function Book(name){this.name = name;}

Book.prototype.author ='flydean';
var book1 = new Book();
var book2 = new Book();
console.log(book1.author);
console.log(book2.author);

下面例子中的 author 属性会被 Book 的所有实例所继承,Book 的 prototype 对象,就是 book1 和 book2 的原型对象。

原型对象的属性不是实例对象本身的属性。只有批改原型对象,变动就立即会体现在所有实例对象上。

因为原型自身也是对象,又有本人的原型,所以造成了一条原型链(prototype chain)。

如果一层层地上溯,所有对象的原型最终都能够上溯到 Object.prototype,即 Object 构造函数的 prototype 属性指向的那个对象。

Object.prototype 对象有没有它的原型呢?答复能够是有的,就是没有任何属性和办法的 null 对象,而 null 对象没有本人的原型。

console.log(Object.getPrototypeOf(Object.prototype));
//null

prototype 对象有一个 constructor 属性,默认指向 prototype 对象所在的构造函数.

function Book(name){this.name = name;}
var book3 =new Book();
console.log(book3.constructor);
console.log(book3.constructor === Book.prototype.constructor);
console.log(book3.hasOwnProperty(constructor));

还是刚刚的 book,book3.constructor 就是 function Book 自身。它也等于 Book.prototype.constructor。

constructor 属性的作用,是分辨原型对象到底属于哪个构造函数。

因为 prototype 是一个对象,所以对象能够被赋值,也就是说 prototype 能够被扭转:

function A(){}
var a = new A();
console.log(a instanceof A);
function B(){}
A.prototype = B.prototype;
console.log(a instanceof A);

下面的例子中,咱们批改了 A.prototype, 最初 a instanceof A 值是 false。

为了保障不会呈现这样谬误匹配的问题,咱们再构建 prototype 的时候,肯定不要间接重写整个的 prototype,只须要批改其中的某个属性就好:

// 不要这样写
A.prototype  ={method1:function (){}}

// 比拟好的写法
A.prototype  ={
    constructor:A,
    method1:function (){}
}
// 更好的写法
A.prototype.method1 = function (){}

Object 的 prototype 操作

Object.getPrototypeOf

Object.getPrototypeOf 办法返回一个对象的原型。这是获取原型对象的规范办法.


// 空对象的 prototype 是 Object.prototype
console.log(Object.getPrototypeOf({}) === Object.prototype);

//function 的 prototype 是 Function.prototype
function f(){}
console.log(Object.getPrototypeOf(f)  === Function.prototype);

function F(){this.name ='flydean'}
var f1 =new F();
console.log(Object.getPrototypeOf(f1) === F.prototype);

var f2 = new f();
console.log(Object.getPrototypeOf(f2) === f.prototype);

下面 4 个的输入后果都是 true。

Object.setPrototypeOf

Object.setPrototypeOf 办法能够为现有对象设置原型,返回一个新对象。

Object.setPrototypeOf 办法承受两个参数,第一个是现有对象,第二个是原型对象。

var a = {name: 'flydean'};
var b = Object.setPrototypeOf({},a);
console.log(b.name);

Object.prototype.isPrototypeOf()

对象实例的 isPrototypeOf 办法,用来判断一个对象是否是另一个对象的原型.

var a = {name: 'flydean'};
var b = Object.setPrototypeOf({},a);
console.log(a.isPrototypeOf(b));

Object.prototype.__proto__

__proto__属性(前后各两个下划线)能够改写某个对象的原型对象。

还是方才的例子,这次咱们应用__proto__来改写对象的原型。

var a = {name: 'flydean'};

var c ={};
c.__proto__ = a;
console.log(Object.getPrototypeOf(c));

__proto__属性只有浏览器才须要部署,其余环境能够没有这个属性,而且前后的两根下划线,示意它实质是一个外部属性,不应该对使用者裸露。

因而,应该尽量少用这个属性,而是用 Object.getPrototypeof()(读取)和 Object.setPrototypeOf()(设置),进行原型对象的读写操作。

三种获取原型对象的办法

综上,咱们有三种获取原型对象的办法:

  • obj.__proto__
  • obj.constructor.prototype
  • Object.getPrototypeOf(obj)

this 对象

this 总是返回一个对象,简略说,就是返回属性或办法“以后”所在的对象。

var book = {
    name :'flydean',
    getName : function (){return '书名:'+ this.name;}
}

console.log(book.getName());
// 书名:flydean

这里 this 的指向是可变的,咱们看一个例子:

var book = {
    name :'flydean',
    getName : function (){return '书名:'+ this.name;}
}

var car ={name :'car'}

car.getName = book.getName;
console.log(car.getName());
// 书名:car

当 A 对象的办法被赋予 B 对象,该办法中的 this 就从指向 A 对象变成了指向 B 对象

下面的例子中,咱们把 book 中的 getName 办法赋值给了 car 对象,this 对象当初就指向了 car。

如果某个办法位于多层对象的外部,这时 this 只是指向以后一层的对象,而不会继承更下面的层。

var book1 = {
    name :'flydean',
    book2: {getName : function (){return '书名:'+ this.name;}
    }
}
console.log(book1.book2.getName());
// 书名:undefined

下面的例子中,this 是定义在对象中的函数中,如果是在函数中的函数中定义的 this,代表什么呢?

var book3 = {
    name :'flydean',
    book4: function(){console.log('book4');
        var getName = function (){console.log(this); //Window
        }();}
}
book3.book4();

如果在函数中的函数中应用了 this,那么内层的 this 指向的是全局的 window 对象。

所以咱们在应用的过程中要防止多层 this。因为 this 的指向是不确定的,所以切勿在函数中蕴含多层的 this。

如果在全局环境应用 this,它指的就是顶层对象 window。

数组的 map 和 foreach 办法,容许提供一个函数作为参数。这个函数外部不应该应用 this。

var book5 ={
    name : 'flydean',
    author : ['max','jacken'],
    f: function (){this.author.forEach(function (item) {console.log(this.name+' '+item);
        })
    }
}
book5.f();
//undefined max
//undefined jacken

foreach 办法的回调函数中的 this,其实是指向 window 对象,因而取不到 o.v 的值。起因跟上一段的多层 this 是一样的,就是内层的 this 不指向内部,而指向顶层对象。

怎么解决呢?咱们应用一个两头变量:

var book6 ={
    name : 'flydean',
    author : ['max','jacken'],
    f: function (){
        var that = this;
        this.author.forEach(function (item) {console.log(that.name+' '+item);
        })
    }
}
book6.f();
//flydean max
//flydean jacken

或者将 this 当作 foreach 办法的第二个参数,固定它的运行环境:

var book7 ={
    name : 'flydean',
    author : ['max','jacken'],
    f: function (){this.author.forEach(function (item) {console.log(this.name+' '+item);
        },this)
    }
}
book7.f();
//flydean max
//flydean jacken

绑定 this 的办法

JavaScript 提供了 call、apply、bind 这三个办法,来切换 / 固定 this 的指向.

call

函数实例的 call 办法,能够指定函数外部 this 的指向(即函数执行时所在的作用域),而后在所指定的作用域中,调用该函数.

var book = {};

var f = function () {return this;}
f()  === this ; //true
f.call(book) === book; //true

下面例子中,如果间接调用 f(), 那么返回的就是全局的 window 对象。如果传入 book 对象,那么返回的就是 book 对象。

call 办法的参数,应该是一个对象。如果参数为空、null 和 undefined,则默认传入全局对象。

如果 call 办法的参数是一个原始值,那么这个原始值会主动转成对应的包装对象,而后传入 call 办法。

var f = function () {return this;}

console.log(f.call(100));
//[Number: 100]

call 办法还能够承受多个参数.

func.call(thisValue,arg1,arg2, ...);

call 的第一个参数就是 this 所要指向的那个对象,前面的参数则是函数调用时所需的参数。

call 个别用在调用对象的原始办法:

var person =  {};

person.hasOwnProperty('getName');//false

// 笼罩 person 的 getName 办法
person.getName  = function(){return true;}

person.hasOwnProperty('getName');//true
Object.prototype.hasOwnProperty.call(person,'getName');//false

apply

apply 办法的作用与 call 办法相似,也是扭转 this 指向,而后再调用该函数。惟一的区别就是,它接管一个数组作为函数执行时的参数.

func.apply(thisValue,[arg1,arg2,...])

bind

call 和 apply 是扭转 this 的指向,而后调用该函数,而 bind 办法用于将函数体内的 this 绑定到某个对象,而后返回一个新函数.

var d = new Date();

console.log(d.getTime()); //1600755862787

var getTime= d.getTime;
console.log(getTime());//TypeError: this is not a Date object.

下面的例子中,getTime 办法外面调用了 this,如果间接把 d.getTime 赋值给 getTime 变量,那么 this 将会指向全局的 window 对象,导致运行谬误。

咱们能够这样批改:

var d = new Date();

console.log(d.getTime()); //1600755862787

var getTime2= d.getTime.bind(d);
console.log(getTime2());

bind 比 call 办法和 apply 办法更进一步的是,除了绑定 this 以外,还能够绑定原函数的参数。

var add = function(x,y){return x +this.m +  y + this.n;}
var addObj ={
    m: 10,
    n: 10
}

var newAdd = add.bind(addObj,2);
console.log(newAdd(3));//25

下面的例子中,bind 将两个参数的 add 办法,替换成了 1 个参数的 add 办法。

留神,bind 每次调用都会返回一个新的函数,从而导致无奈勾销之前的绑定。

继承

构造函数的继承

构造函数的继承第一步是在子类的构造函数中,调用父类的构造函数, 让子类实例具备父类实例的属性。

而后让子类的原型指向父类的原型,这样子类就能够继承父类原型。

function Person (){this.name = 'person';}

function Boy(){Person.call(this);
    this.title = 'boy';
}

Boy.prototype= Object.create(Person.prototype);
Boy.prototype.constructor=Boy;
Boy.prototype.getTitle=function (){console.log(this.title)};

var b =new Boy();
b.getTitle();
console.log(b);
~~

调用父类的构造函数是初始化实例对象的属性。子类的原型指向父类的原型是为了根底父类的原型对象的属性。另外一种写法是 Boy.prototype 等于一个父类实例:

Boy.prototype = new Person();


下面这种写法也有继承的成果,然而子类会具备父类实例的办法。有时,这可能不是咱们须要的,所以不举荐应用这种写法.

JavaScript 不提供多重继承性能,即不容许一个对象同时继承多个对象。然而,能够通过变通方法,实现这个性能:

function Person1 (){

this.name = 'person';

}
function Person2 (){

this.sex = '男';

}

function Boy(){

Person1.call(this);
Person2.call(this);
this.title = 'boy';

}

// 继承 Person1
Boy.prototype= Object.create(Person1.prototype);
// 继承链加上 Person2
Object.assign(Boy.prototype,Person2.prototype);

Boy.prototype.constructor=Boy;
Boy.prototype.getTitle=function (){console.log(this.title)};

var b =new Boy();
b.getTitle();
console.log(b);
//Boy {name: ‘person’, sex: ‘ 男 ’, title: ‘boy’}


# class

ES6 的 class 能够看作只是一个语法糖,它的绝大部分性能,ES5 都能够做到,新的 class 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已.

class Person {

constructor(name,sex) {
    this.name=name;
    this.sex =sex;
}

toString(){return this.name + ' '+ this.sex;}

}


构造函数的 prototype 属性,在 ES6 的“类”下面持续存在。事实上,类的所有办法都定义在类的 prototype 属性下面。下面的类等同于:

Person.prototype = {

   constructor(name,sex) {
    this.name=name;
    this.sex =sex;
}

toString(){return this.name + ' '+ this.sex;} 

}


## 表达式属性名

class 还反对动静的表达式属性名:

let methodName = ‘getName’;

class Person {

constructor(name,sex) {
    this.name=name;
    this.sex =sex;
}

toString(){return this.name + ' '+ this.sex;}

[methodName](){return this.name;}

}


## 静态方法

类相当于实例的原型,所有在类中定义的办法,都会被实例继承。如果在一个办法前,加上 static 关键字,就示意该办法不会被实例继承,而是间接通过类来调用,这就称为“静态方法”。

class Person {

constructor(name,sex) {
    this.name=name;
    this.sex =sex;
}

static getSex(){return '男';}

}

console.log(Person.getSex()); // 男

let p = new Person();
console.log(p.getSex());//TypeError: p.getSex is not a function


## 动态属性

动态属性指的是 Class 自身的属性,即 Class.propName,而不是定义在实例对象(this)上的属性.

class Person {

constructor(name,sex) {
    this.name=name;
    this.sex =sex;
}

}
Person.address =’address’;
console.log(Person.address);



目前,只有这种写法可行,因为 ES6 明确规定,Class 外部只有静态方法,没有动态属性.

## class 的继承

class 的继承个别应用 extends 关键字:

class Boy extends Person{

constructor(name,sex,address) {super(name,sex); // 调用父类的构造函数
    this.address =address;
}

toString() {return super.toString();// 调用父类的办法
}

}


在子类的构造函数中,只有调用 super 之后,才能够应用 this 关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有 super 办法能力返回父类实例。super 作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次 super 函数。super 作为对象时,在一般办法中,指向父类的原型对象;在静态方法中,指向父类。下面的例子,咱们在子类 Boy 中的 toString 一般办法中,调用了 super.toString(), 之前咱们也讲了,类的所有办法都定义在类的 prototype 属性下面。所以 super.toString 就是 Person 中定义的 toString 办法。因为 super 指向父类的原型对象,所以定义在父类实例上的办法或属性,是无奈通过 super 调用的。定义在父类实例上的办法或属性就是指在 constructor 中定义的办法或者属性。Person 类,在 constructor 中定义了 name 属性。咱们看一下在 Boy 中的一般办法中拜访会有什么问题:

class Boy extends Person{

constructor(name,sex,address) {super(name,sex); // 调用父类的构造函数
    console.log(super.name);  //undefined
    console.log(this.name);  //hanmeimei
    this.address =address;
}

toString() {return super.toString();// 调用父类的办法
}

getName(){console.log(super.name);  //undefined
    console.log(this.name);    //hanmeimei
}

}

var b =new Boy(‘hanmeimei’,’ 女 ’,’ 北京 ’);
b.getName();


# 总结

JS 中的面向对象次要有构造函数,原型链,类三种形式,心愿大家可能喜爱。> 本文作者:flydean 程序那些事
> 
> 本文链接:[http://www.flydean.com/object-oriented-js/](http://www.flydean.com/object-oriented-js/)
> 
> 本文起源:flydean 的博客
> 
> 欢送关注我的公众号:「程序那些事」最艰深的解读,最粗浅的干货,最简洁的教程,泛滥你不晓得的小技巧等你来发现!
退出移动版