一、构造函数和原型

1、构造函数、静态成员和实例成员

在ES6之前,通常用一种称为构造函数的特殊函数来定义对象及其特征,然后用构造函数来创建对象。像其他面向对象的语言一样,将抽象后的属性和方法封装到对象内部。

function Person(uname, age) {
this.uname = uname;
this.age = age;
this.say = function() {
console.log('我叫' + this.uname + ',今年' + this.age + '岁。');
}
}
var zhangsan = new Person('张三', 18);
zhangsan.say(); //输出:我叫张三,今年18岁。
var lisi = new Person('李四', 20);
lisi.say(); //输出:我叫李四,今年20岁。

在创建对象时,构造函数总与new一起使用(而不是直接调用)。new创建了一个新的对象,然后将this指向这个新对象,这样我们才能通过this为这个新对象赋值,函数体内的代码执行完毕后,返回这个新对象(不需要写return)。

构造函数内部通过this添加的成员(属性/方法),称为实例成员(属性/方法),只能通过实例化的对象来访问,构造函数上是没有这个成员的。

>> Person.uname
undefined

我们也可以给构造函数本身添加成员,称为静态成员(属性/方法),只能通过构造函数本身来访问。

2、原型

上面的例子中,我们借助构造函数创建了两个对象zhangsan和lisi,它们有各自独立的属性和方法。对于实例方法而言,由于函数是复杂数据类型,所以会专门开辟一块内存空间存放函数。又由于zhangsan和lisi的方法是独立的,所以zhangsan的say方法和lisi的say方法分别占据了两块内存,尽管它们是同一套代码,做同一件事情。

>> zhangsan.say === lisi.say
false

试想,实例方法越多,创建的对象越多,浪费的空间也就越大。为了节约空间,我们希望所有的对象调用同一个say方法。为了实现这个目的,就要用到原型。

每个构造函数都有一个prototype属性,指向另一个对象,称为原型,由于它是一个对象,也称为原型对象。(以下为了不产生混淆,将构造函数创建的对象称为实例)另一方面,实例有一个属性__proto__,通过它也会指向这个原型对象。为了区分,__proto__的指向一般叫对象的原型,prototype叫原型对象。

原型对象里还有一个constructor属性,它指回构造函数本身,以记录该原型对象引用自哪个构造函数。这样,构造函数、原型和实例就构成了一个三角关系。

引入原型对象后,构造函数改为如下方式定义:

function Person(uname, age) {
this.uname = uname;
this.age = age;
}
Person.prototype.say = function() {
return '我叫' + this.uname + ',今年' + this.age + '岁。';
}; var zhangsan = new Person('张三', 18);
console.log(zhangsan.say());
var lisi = new Person('李四', 20);
console.log(lisi.say());
console.log(zhangsan.say === lisi.say); //输出:true

在查找对象的成员时,首先在对象自己身上寻找,如果自己没有,就通过__proto__去原型对象上找。通过构造函数的原型对象定义的函数是所有实例共享的。

一般情况下,我们把实例属性定义到构造函数中,实例方法放到原型对象中。

3、原型链

原型对象也是一个对象,它也有自己的原型,指向Object的原型对象Object.prototype。

>> Person.prototype.__proto__ === Object.prototype
true
>> Person.prototype.__proto__.constructor === Object
true

也就是说,Person的原型对象是由Object这个构造函数创建的。

继续往下追溯Object.prototype的原型:

>> Object.prototype.__proto__
null

终于到头了。回过头来看,我们从zhangsan这个实例开始追溯:

zhangsan.__proto__指向Person的原型对象

zhangsan.__proto__.__proto__指向Object的原型对象Object.prototype

zhangsan.__proto__.__proto__.__proto__指向null

这种链式的结构,就称为原型链。

对象的成员查找机制依靠原型链:当访问一个对象的属性(或方法)时,首先查找这个对象自身有没有该属性;如果没有就找它的原型;如果还没有就找原型对象的原型;以此类推一直找到null为止,此时返回undefined。__proto__属性为查找机制提供了一条路线,一个方向。

有了原型链的概念之后,现在再来回顾new在执行时做了什么:

1.在内存中创建一个新的空对象;

2.将对象的__proto__属性指向构造函数的原型对象;

3.让构造函数里的this指向这个新的对象;

4.执行构造函数里的代码,给这个新对象添加成员,最后返回这个新对象。

二、继承

ES6以前,如何实现类的继承?这就要用到call方法。

function.call(thisArg, arg1, arg2, ...)

thisArg:在function函数运行时指定的this值。

arg1, arg2, ...:要传递给function的实参。

我们知道,所调用的函数内部有一个this属性,根据不同的场景指向调用者、window或undefined。call方法允许我们调用函数时指定另一个this。

var obj = {};
function f () {
console.log(this === window, this === obj);
}
f(); //输出:true false
f.call(obj); //输出:false true

以普通的方式调用f函数,this指向window;以call来调用,this就指向obj。

利用call,在子构造函数中调用父构造函数,令其内部的this由父类实例指向子类实例,从而在父构造函数中完成一部分成员的初始化。

来看例子,我们从Person继承一个Student类,不仅要有Person的一切属性和方法,还要新增一个grade属性表示年级,一个exam方法用来考试:

function Person(uname, age) {
this.uname = uname;
this.age = age;
}
Person.prototype.say = function() {
return '我叫' + this.uname + ',今年' + this.age + '岁。';
};
function Student(uname, age, grade) {
Person.call(this, uname, age);
this.grade = grade;
}
Student.prototype.exam = function() {
console.log('正在考试!');
};
var stu = new Student('张三', 16, '高一');
console.log(stu.uname, stu.age, stu.grade); //输出:张三 16 高一
stu.exam(); //输出:正在考试!

在Student中,调用了Person函数,令其内部的this指向Student的this,这样uname和age都给了Student的this,最后给原型对象加了个exam方法。注意我们的目的不是创建一个Person的实例,所以没有加new,只是把构造函数当普通函数调用而已。

接下来让我们调用父构造函数中的say方法,看看有没有被继承。

>> stu.say()
TypeError: stu.say is not a function //报错了!
>> stu.say
undefined //stu实例并没有say这个成员

哦哦,say是放在Person.prototype中的,但是stu并没有和它产生联系,得改原型链。又由于stu的原型上已经挂了exam,不能直接改变stu.__proto__的指向,只好沿着原型链修改Student.prototype.__proto__的指向(它原本指向Object.prototype):

>> Student.prototype.__proto__ = Person.prototype
>> stu.say()
"我叫张三,今年16岁。" //调用成功了!

say方法执行了,打印出了姓名、年龄,但我们的Student构造函数还新增了个grade,也需要打印出来。这可难为say方法了,毕竟当时我们定义它时是基于Person的,并没有grade属性。所以我们要覆写这个方法,让它能打印grade,同时不影响原有的say方法,也就是和exam一样挂到Student.prototye上。

>> Student.prototype.say = function() { return '我叫' + this.uname + ',今年' + this.age + '岁。' + this.grade + '学生。'; }
>> stu.say()
"我叫张三,今年16岁。高一学生。"

搞定了!我们碰了好几次壁,总算解决了继承的问题。

在很多资料中提到了“寄生组合式继承”,思路与上面的分析一样,就是原型对象+构造函数组合使用。不同之处仅在于,没有保留原有的子构造函数的原型对象,而是将它指向另一个通过Object.create()方法创建的对象:

Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;

Object.create()方法创建一个新对象,这个新对象的__proto__指向作为实参传入的Person.prototype。既然指定了另一个对象作为原型,那么constructor应该指回构造函数。

此外,还有另一种继承方式也经常被提及,称为“组合式继承”,同样要修改Student.prototype的指向:

Student.prototype = new Person(); //不用赋值,我们不关心原型里的uname和age
Student.prototype.constructor = Student;

使用父类实例作为Student.prototype的值,因为父类实例的__proto__一定指向父构造函数的原型对象。这样做的弊端在于Person总共调用了2次,并且Student.prototype中存在一部分用不到的属性。

现在,还有最后一个问题:子类的say方法中存在和父类say方法中相同的代码片段,如何优化这样的冗余?答案是,调用父构造函数原型中的say方法:

Student.prototype.say = function() {
return Person.prototype.say.call(this) + this.grade + '学生。';
};

直接调用只会打印出undefined,因为this默认指向调用者,即Student.prototype,所以要用call修改this为子类实例。

最后附上一份完整的代码,采用寄生组合式继承:

function Person(uname, age) {
this.uname = uname;
this.age = age;
}
Person.prototype.say = function() {
return '我叫' + this.uname + ',今年' + this.age + '岁。';
};
function Student(uname, age, grade) {
Person.call(this, uname, age);
this.grade = grade;
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student; //别忘了把constructor指回来
//Student.__proto__ = Person; //这里挖个坑,后面填
Student.prototype.exam = function() {
console.log('正在考试!');
};
Student.prototype.say = function() {
return Person.prototype.say.call(this) + this.grade + '学生。';
};
var stu = new Student('张三', 16, '高一');
console.log(stu.say()); //输出:我叫张三,今年16岁。高一学生。

于是原型链愈发壮大了:

最后总结一下继承的思路:

1.首先在子构造函数中用call方法调用父构造函数,修改this指向,实现继承父类的实例属性;

2.然后修改子构造函数的prototype的指向,无论是寄生组合式继承,还是组合式继承,还是我们自己探索时的修改方式,本质都是把子类的原型链挂到父构造函数的原型对象上,从而实现子类继承父类的实例方法;

3.如果需要给子类新增实例方法,挂到子构造函数的prototype上;

4.如果子类的实例方法需要调用父类的实例方法,通过父构造函数的原型调用,但是要更改this指向。

核心就是原型对象+构造函数组合使用。只使用原型对象,子类无法继承父类的实例属性;只使用构造函数,又无法继承原型对象上的方法。但是双剑合璧后,就能互补长短。打个不恰当的比方,天龙八部中虚竹救天山童姥那段,天山童姥腿断了行动不便,但自己有一定法力;虚竹学到轻功之后跑得快,但他不懂得使用内力。最后他俩都成功跑路了。_(:з」∠)_

三、ES6的类和继承

1、类

ES6中新增了类的概念,使用class关键字来定义一个类,语法和其他面向对象的语言很相似。

class Person {
constructor(uname, age) {
this.uname = uname;
this.age = age;
}
say() { //实例方法
return `我叫${this.uname},今年${this.age}岁。`; //模板字符串
}
static staticMethod() { //静态方法
console.log(`这是静态方法`);
}
}
let zhangsan = new Person('张三', 18);
console.log(zhangsan.say());

注意点:

1.实例属性定义在constructor中。constructor不写也会默认创建。

2.类中方法前面不需要加function关键字,各方法也不需要用逗号隔开。

3.静态方法前加static关键字,实例方法不需要。

4.ES6中静态属性无法在class内部定义,需使用传统的Person.xxx或Person['xxx']。

5.class没有变量提升,必须先定义类,再通过类实例化对象。

2、继承

使用extends关键字实现继承:

class Person {
constructor(uname, age) {
this.uname = uname;
this.age = age;
}
say() {
return `我叫${this.uname},今年${this.age}岁。`;
}
}
class Student extends Person {
constructor (uname, age, grade) {
super(uname, age);
this.grade = grade;
}
say() {
return `${super.say()}${this.grade}学生。`;
}
exam() {
console.log('正在考试!');
}
}
let stu = new Student('张三', 16, '高一');
console.log(stu.say());
stu.exam();

这段代码是前面ES5继承例子的ES6版本。

注意点:

1.子类的constructor中,必须调用super方法,否则新建实例时会报错。

2.constructor和say中虽然都用到了super,但是它们的意义不一样,后文会讲。

3、class的本质

先说结论:class的原理基本上还是ES5中那一套,只是写法上更加简洁明了。

使用class定义的Student仍然是一个构造函数,原型链和之前一模一样:

>> typeof Person //class定义出来的仍然是一个函数
"function"
>> Person.prototype === (new Person()).__proto__
true
>> Person.prototype.constructor === Person //三角关系一模一样
true
>> stu.__proto__ instanceof Person //stu的原型是Person的实例
true
>> Object.getOwnPropertyNames(stu)
[ "uname", "age", "grade" ]
>> Object.getOwnPropertyNames(stu.__proto__)
[ "constructor", "say", "exam" ] //Student的say和exam挂在原型里

Object.getOwnPropertyNames()方法获得指定对象的所有挂在自己身上的属性和方法的名称(不会去原型链上找),这些名称组成数组返回。我们通过stu能访问say和exam,因为它们挂在原型里。

其他的我就不一一试了,直接给结论:

1.class定义的仍然是一个构造函数;

2.class中定义的实例方法,挂在原型对象里;静态方法,挂在构造函数自己身上;

3.子类有两个地方用到了super,含义不同:constructor中,super被当做函数看待,super(uname, age)代表调用父类的构造函数,相当于Person.call(this, uname, age),另外super()只能用于constructor中;say方法中的super.say()是将super当对象看待,它指向父类的原型对象Person.prototype,super.say()相当于Person.protoype.say.call(this)。

实际上,ES6的类的绝大部分功能,在ES5中都可以实现。当然,class和extends的引入,使得JS在写法上更加简洁明了,在语法上更像其他面向对象编程的语言。所以ES6中的类就是语法糖。

4、继承内建对象

通过extends同样可以继承内建对象:

class MyArray extends Array {
constructor() {
super();
}
}
let a_es6 = new MyArray();
a_es6[1] = 'a';
console.log(a_es6.length); //输出:2

MyArray的表现和Array几乎无二。

但是如果想用ES5的做法的话,比如说组合式继承:

function MyArray2() {
Array.call(this);
}
MyArray2.prototype = new Array();
MyArray2.prototype.constructor = MyArray2;
var a_es5 = new MyArray2();
a_es5[1] = 'a';
console.log(a_es5.length); //输出:0

我们给a_es5的下标1的位置赋了个值,令人失望的是,length还是0。

为什么这两个类的行为完全不同?因为在ES5的组合继承中,首先由子类构造函数创建this的值,MyArray2的this指向新创建的对象,然后再调用父构造函数令Array内部的成员添加到this上,但这种方法无法得到Array内部的成员。来看下面这个例子的模拟:

>> let o = {}
>> Object.getOwnPropertyNames(o)
[] //空列表
>> Array.call(o)
>> Object.getOwnPropertyNames(o)
[] //仍然是空列表

我们通过Array.call(o)试图让空对象o获取Array内所有属性,但是失败了,o并没有发生什么变化。“继承”自Array的a_es5也是如此,它自己连length属性都没有,我们能访问length是因为它挂在原型上。

但在ES6的class中,通过super()创建的this首先指向父类Array的实例,接着子类再在父类实例的基础上修改值,因此ES6中this可以访问父类实例的功能。

四、函数的原型

我们知道,函数除了用function和表达式定义,还可以用new Function(参数1,参数2,...,函数体)的方式定义:

>> var f = new Function('a', 'b', 'console.log(a + b);')
>> f(1, 2)
3

换而言之,所有的函数都是Function这个构造函数的实例,都是对象,函数的内部既有prototype属性也有__proto__属性,前者指向自己的原型对象,后者指向Funtion的原型对象。当我们创建函数的时候(无论是用ES5中哪种方式去创建),new大致做了这些事情:

1.在内存中创建一个空对象,这里记作F;

2.令F.__proto__指向Function.prototype;

3.用new Object()再创建另一个对象,记作proto;

4.令proto.constructor指向F;

5.令F.prototype指向proto;

6.返回F。

特别地,ES6使用extends进行继承后,子类的__proto__将指向父类以表示继承关系,而不是Function.prototype。(还记得前面在寄生组合式继承的代码里挖了个坑吗?)

再来看Function函数。它也有原型对象,Function.prototype。另一方面,作为对象,ES5规定Function的__proto__属性就指向它自己的原型对象,即Function.__proto__全等于Function.prototype。

Function.prototype也是对象,由new Object创建,因此Function.prototype.__proto__指向Object.prototype。

现在一切都指向Object.prototype,即Object的原型对象,这是除了null以外站在原型链顶端的人,它的上面,Object.prototype.__proto__为null。

原型链终极图:

五、原型链的实际应用

除了上面介绍的继承和查找方向,原型链也可以反过来用,封掉不想给别人用的内置方法。以b漫为例,我们想把某张漫画保存下来,首先打开漫画的阅读页面:

可以看到2张图就是2个canvas。从canvas提取图像信息,我们想到了toDataUrl和toBlob方法。前者返回一个经过base64加密的data url,后者返回Blob对象,不管哪个,最后都能转换成图片文件:

>> let c = document.getElementsByTagName('canvas')[0]
>> c.toDataUrl
undefined //没了
>> c.toBlob
undefined //这个也没了

canvas对象的类为HTMLCanvasElement,toDataUrl和toBlob定义于其原型对象上。经查找,JS代码中有一个立即执行函数把这两个属性指向了undefined,就在reader.xxxxxxxxxx.js这个文件里面(这10个x是占位符,均为数字和小写英文字母之一,比如说我现在的文件名叫reader.8d59f9bef4.js)。

……(前略)
function () {
try {
HTMLCanvasElement.prototype.toDataURL = void 0,
HTMLCanvasElement.prototype.toBlob = void 0
} catch (e) {}
}(),
……(后略)

不能用ad block之类的扩展把这个js文件屏蔽掉,这将导致canvas元素都不会生成,但可以用其他方法下载图片,并非本文重点,不详述:

1.Chrome的sources页面直接就把图片展示出来了;

2.火狐给canvas加了个非标准方法mozGetAsFile(),可以转换为File对象,该方法没有被封;

3.分析前后的http请求和响应,用爬虫爬;

4.用fiddler将该文件替换为本地文件,在本地文件中你当然可以注释掉这两行代码。

六、参考资料(扩展阅读)

1.ECMAScript

2.从Object和Function说说JS的原型链

3.JavaScript(ES6) - Class

4.es5实现继承

JS原型,原型链,类,继承,class,extends,由浅到深的更多相关文章

  1. js原型链与继承(初体验)

    js原型链与继承是js中的重点,所以我们通过以下三个例子来进行详细的讲解. 首先定义一个对象obj,该对象的原型为obj._proto_,我们可以用ES5中的getPrototypeOf这一方法来查询 ...

  2. 对Javascript 类、原型链、继承的理解

    一.序言   和其他面向对象的语言(如Java)不同,Javascript语言对类的实现和继承的实现没有标准的定义,而是将这些交给了程序员,让程序员更加灵活地(当然刚开始也更加头疼)去定义类,实现继承 ...

  3. js 原型链和继承(转)

    在理解继承之前,需要知道 js 的三个东西: 什么是 JS 原型链 this 的值到底是什么 JS 的 new 到底是干什么的 1. 什么是 JS 原型链? 我们知道 JS 有对象,比如 var ob ...

  4. Js笔记(对象,构造函数,原型,原型链,继承)及一些不熟悉的语法

    对象的特性: 1.唯一标识性,即使完全不一样的对象,内存地址也不同,所以他们不相等 2.对象具有状态,同一个对象可能处在不同状态下 3.对象具有行为,即对象的状态可能因为他的行为产生变迁 Js直到es ...

  5. 小谈js原型链和继承

    原型(prototype)在js中可是担当着举足轻重的作用,原型的实现则是在原型链的基础上,理解原型链的原理后,对原型的使用会更加自如,也能体会到js语言的魅力. 本文章会涉及的内容 原型及原型对象 ...

  6. 怎么理解js的原型链继承?

    前言 了解java等面向对象语言的童鞋应该知道.面向对象的三大特性就是:封装,继承,多态. 今天,我们就来聊一聊继承.但是,注意,我们现在说的是js的继承. 在js的es6语法出来之前,我们想实现js ...

  7. Js基础知识(二) - 原型链与继承精彩的讲解

    作用域.原型链.继承与闭包详解 注意:本章讲的是在es6之前的原型链与继承.es6引入了类的概念,只是在写法上有所不同,原理是一样的. 几个面试常问的几个问题,你是否知道 instanceof的原理 ...

  8. 关于JS对象原型prototype与继承,ES6的class和extends · kesheng's personal blog

    传统方式:通过function关键字来定义一个对象类型 1234567891011 function People(name) { this.name = name}People.prototype. ...

  9. 深入理解JS原型链与继承

    我 觉得阅读精彩的文章是提升自己最快的方法,而且我发现人在不同阶段看待同样的东西都会有不同的收获,有一天你看到一本好书或者好的文章,请记得收藏起来, 隔断时间再去看看,我想应该会有很大的收获.其实今天 ...

随机推荐

  1. ReentrantReadWriteLock 可重入的读写锁

    可重入:就是同一个线程可以重复加锁,可以对同一个锁加多次,每次释放的时候会释放一次锁,直到该线程加锁次数为0,这个线程才释放锁. 读写锁: 也就是读锁可以共享,多个线程可以同时拥有读锁,但是写锁却只能 ...

  2. Linux 内核USB 接口配置

    USB 接口是自己被捆绑到配置的. 一个 USB 设备可有多个配置并且可能在它们之间转换 以便改变设备的状态. 例如, 一些允许固件被下载到它们的设备包含多个配置来实现这个. 一个配置只能在一个时间点 ...

  3. Cisco DNA网络POC

    角色名词解释 拓扑图 集成ISE

  4. Centos 7.5安装 Nginx 1.14.1

    1. 准备工作 查看系统版本 输入命令 cat /etc/redhat-release 我的Centos版本 CentOS Linux release 7.5.1804 (Core) 安装nginx所 ...

  5. 通过脚本实现对web的健康检查

    前面的文章中(https://www.cnblogs.com/zyxnhr/p/10707932.html),通过nginx的第三方模块实现对web端的一个监控,现在通过一个脚本实现对第三方的监控 脚 ...

  6. hexo+next 详细搭建

    安装node node下载地址:http://nodejs.cn/download/ 具体安装方法,这里不做详写 安装完成可以通过node -v 查看安装是否生效和node的版本 我这里使用的是v10 ...

  7. $Poj3585\ Accumulation Degree$ 树形$DP/$二次扫描与换根法

    Poj Description 有一个树形的水系,由n-1条河道与n个交叉点组成.每条河道有一个容量,联结x与y的河道容量记为c(x,y),河道的单位时间水量不能超过它的容量.有一个结点是整个水系的发 ...

  8. .Net Core Web Api实践(二).net core+Redis+IIS+nginx实现Session共享

    前言:虽说公司app后端使用的是.net core+Redis+docker+k8s部署的,但是微信公众号后端使用的是IIS部署的,虽说公众号并发量不大,但领导还是使用了负载均衡,所以在介绍docke ...

  9. 小小知识点(二十五)5G关键技术——Massive MIMO(大规模天线阵列)和beamforming(波束成形)

    转自http://www.elecfans.com/d/949864.html 多输入多输出技术(Multiple-Input Multiple-Output,MIMO)是指在发射端和接收端分别使用多 ...

  10. 小小知识点(二十四)什么是5G

    转自 https://www.ifanr.com/1149419 一个简单且神奇的公式 今天的故事,从一个公式开始讲起.这是一个既简单又神奇的公式.说它简单,是因为它一共只有 3 个字母.而说它神奇, ...