【机制】JavaScript的原型、原型链、继承
1.原型和原型链的概念
js在创建一个对象时,比如叫 obj
,都会给他偷偷的加上一个引用,这个引用指向的是一个对象,比如叫 yuanxing
,
这个对象可以给引用它的对象提供属性共享
,比如:yuanxing
上有个属性name,可以被 obj.name
访问到,
这个可以提供属性共享的对象,就称为前面对象的原型
而原型本身也是一个对象,所以它也会有一个自己的原型,这一层一层的延续下去,直到最后指向 null,这就形成的 原型链
那js的这一种是原型机制是怎么实现的呢?
2.原型的实现
我们先从一个例子来看:
//code-01
let obj = new Object({name:'xiaomin'})
console.log(obj.name)
console.log(obj.toString())
// xiaomin
// [object Object]
我们首先创建了一个对象obj
,它有一个属性name
属性name
是我们创建的,但是在创建obj
的时候,并没有给它创建toString
属性,为什么obj.toString()
可以访问的到呢?
prototype 属性
我们先来看一下Object.prototype
属性
我们发现这里有toString
属性,所以其实Object.prototype
就是obj
的原型,按照原型的概念,它能够为obj
提供属性共享
所以当obj.toString()
的时候,先在obj
的属性里找,没找到,就在obj
的原型Object.prototype
的属性上找,可以找到,所以调用成功
proto 属性
那么obj
是怎么找到原型的呢?我们打印obj
属性看一下:
我们发现obj
除了我们创建的name
以外,还有一个__proto__
属性,它的值是一个对象,那么它等于什么呢?
我们发现obj.__proto__
指向的就是Object.prototype
到此为止,我们可以简单总结一下js语言实现原型的基本机制了
- 在创建一个对象
obj
时,会给它加上一个__proto__
属性 __proto__
属性 指向obj
的构造函数
的prototype
对象- 当访问
obj
的某个属性时,会先在它自己的属性上找,如果没找到,就在它的原型
(其实就是__proto__
指向的对象)的属性上找
构造函数
这个有一个构造函数
的概念,其实构造函数
也就是普通函数,当这个函数被用来 new
一个新对象时,它就被称为新对象的 构造函数
就上面的例子而言,Objec
就是obj
的构造函数,
这里要区别一下Object
和object
的区别,前者是一个js内置的一个函数,后者是js的基本数据类型(number,string,function,object,undefined)
3.new 实际上做了什么
上面有说到new
关键字,那么在它实际上做了什么呢?
上面代码code-01
使用系统内置的函数Object
来创建对象的,那么我们现在用自己创建的函数来创建一个新对象看看:
//code-02
function human(name){
this.name = name
}
human.prototype.say = function(){
alert('我叫'+this.name)
}
let xiaomin = new human('xiaoming')
console.log(xiaomin.name)
xiaomin.say()
这里的human
就是新对象xiaoming
的构造函数
我们把新创建的对象xiaoming
打印出来看看:
我们看到xiaoming
有一个属性name
,并且xiaoming.__proto__
完全等于构造函数的human.prototype
,这就是它的原型
从这里我们可以总结一下new
的基本功能:
- 把
构造函数
的this指向新创建的对象xiaoming
- 为新对象创建
原型
,其实就是把新对象的__proto__
指向构造函数的prototype
手写new
上面我们了解了new
具体做了什么事情,那么它是怎么实现这些功能的呢?下面我们手写一个函数myNew
来模拟一下new
的效果:
//code-03
function human(name, age) {
this.name = name;
this.age = age;
}
human.prototype.say = function () {
console.log("my name is " + this.name);
};
xiaoming = myNew(human, "xiaoming", 27);
function myNew() {
let obj = new Object();
//取出函数的第一个参数,其实就是 human函数
let argArr = Array.from(arguments);
const constructor = argArr.shift();
// 指定原型
obj.__proto__ = constructor.prototype;
//改变函数执行环境
constructor.apply(obj, argArr);
return obj;
}
xiaoming.say();
我们先把新对象xiaoming
打印出来看一下:
我们发现这和上面的代码code-02
的效果是一样的
上面代码code-03
里面如果对apply
的作用不太熟悉的,可以另外了解一下,其实也很简单,意思就是:在obj的环境下,执行constructor函数,argArr是函数执行时的参数,也就是指定了函数的this
4.继承的实现
其实就上面的内容,就可以对js的原型机制有个基本的了解,但是一般面试的时候,如果有问到原型,接下来就会问 能不能实现 继承
的功能,所以我们来手写一下 原型的继承,其实所用到的知识点都是上面有提到的
继承的概念
我们先来说下继承
的概念:
继承
其实就是 一个构造函数(子类)可以使用另一个构造函数(父类)的属性和方法,这里有几点注意的:
- 继承是 构造函数 对 另一个构造函数而言
- 需要实现属性的继承,即
this
的转换 - 需要实现方法的继承,一般就是指
原型链
的构建
继承的实现
基于上面的3点要素,我们先直接来看代码:
// code-04
// 父级 函数
function human(name) {
this.name = name;
}
human.prototype.sayName = function () {
console.log("我的名字是:", this.name);
};
// 子级 函数
function user(args) {
this.age = args.age;
//1.私有属性的继承
human.call(this, args.name);
//2.原型的继承
Object.setPrototypeOf(user.prototype, human.prototype); //原型继承-方法1
// user.prototype.__proto__ = human.prototype; // 原型继承-方法2
}
// 因为重新赋值了prototype,所以放置 user 外部
// user.prototype = new human();//原型继承-方法3
// user.prototype = Object.create(human.prototype);//原型继承-方法4
user.prototype.sayAge = function () {
console.log("我的年龄是:", this.age);
};
let person = new human("人类");
let xiaoming = new user({ name: "xiaoming", age: 27 });
console.log("----父类-----");
console.log(person);
person.sayName();
console.log("----子类-----");
console.log(xiaoming);
xiaoming.sayName();
xiaoming.sayAge();
我们先来看下打印的结果:
从打印结果,我们可以看到xiaoming
拥有person
的属性和方法(name
,sayName
),又有自己私有的属性方法(age
,sayAge
),这是因为构造函数user
实现了对human
的继承。
其实实现的方法无非也就是我们前面有说到的 作用域的改变和原型链的构造,其中作用域的改变(this指向的改变)主要是两个方法:call和apply,原型链的构造原理只有一个,就是对象的原型等于其构造函数的prototype属性
,但是实现方法有多种,代码code-04
中有列出4种。
从上面的例子来看原型链的指向是:xiaoming
->user.prototype
->human.prototype
5.class和extends
我们可能有看到一些代码直接用 class
和 extends
关键字来实现类和继承,其实这是ES6的语法,其实是一种语法糖,本质上的原理也是相同的。我们先来看看基本用法:
用法
//code-05
class human {
//1.必须要有构造函数
constructor(name) {
this.name = name;
}//2.不能有逗号`,`
sayName() {
console.log("sayName:", this.name);
}
}
class user extends human {
constructor(params) {
//3.子类必须用`super`,调用父类的构造函数
super(params.name);
this.age = params.age;
}
sayAge() {
console.log("sayAge:", this.age);
}
}
let person = new human("人类");
let xiaoming = new user({ name: "xiaoming", age: 27 });
console.log("----<human> person-----");
console.log(person);
person.sayName();
console.log("----<user> xiaoming-----");
console.log(xiaoming);
xiaoming.sayName();
xiaoming.sayAge();
执行结果:
我们看到执行的结果和上面的代码code-04
是一样的,但是代码明显清晰了很多。几个注意的地方:
- class类中必须要有构造函数
constructor
, - class类中的函数不能用
,
分开 - 如果要继承父类的话,在子类的构造函数中,必须先执行
super
来调用的父类的构造函数
相同
上面有说class的写法其实原理上和上面是一样的,我们来验证一下
首先看看
user
和human
是什么类型
这里看出来了,所以虽然被class修饰,本质上还是函数,和代码code-04
中的user
,human
函数是一样的再来看看prototype属性
这里看出来sayName
,sayAge
都是定义在human.prototype
和user.prototype
上,和代码code-04
中也是一样的我们再来看看原型链
这与代码code-04
中的原型链的指向也是一样:xiaoming
->user.prototype
->human.prototype
差异
看完相同点,现在我们来看看不同点:
- 首先写法上的不同
- class声明的函数,必须要用new调用
- class内部的成员函数没有prototype属性,不可以用new调用
- class 内的代码自动是严格模式
- class声明不存在变量提升,这一点和 let一样,比如:
//code-06
console.log(name_var);
var name_var = "xiaoming";
//undefined,不会报错,var声明存在变量提升 console.log(name_let);
let name_let = "xiaoming";
// Uncaught ReferenceError: Cannot access 'name_let' before initialization
//报错,let声明不存在变量提升 new user();
class user {}
// Uncaught ReferenceError: Cannot access 'user' before initialization
//报错,class声明不存在变量提升 - class内的方法都是不可枚举的,比如:
//code-07
class human_class {
constructor(name) {
this.name = name;
}
sayName() {
console.log("sayName:", this.name);
}
}
function human_fun(name) {
this.name = name;
}
human_fun.prototype.sayName = function () {
console.log("sayName:", this.name);
};
console.log("----------human_class-----------");
console.log("prototype属性", human_class.prototype);
console.log("prototype 枚举", Object.keys(human_class.prototype)); console.log("----------human_fun-----------");
console.log("prototype属性", human_fun.prototype);
console.log("prototype 枚举", Object.keys(human_fun.prototype));
运行结果:
6.总结
简单总结一下:
- 每个对象在创建的时候,会被赋予一个
__proto__
属性,它指向创建这个对象的构造函数的prototype
,而prototype
本身也是对象,所以也有自己的__proto__
,这就形成了原型链,最终的指向是Object.prototype.__proto__ == null
- 可以通过
new
,Object.create()
,Object.setPrototypeOf()
,直接赋值__proto__
等方法为一个对象指定原型 new
操作符实际做的工作是:创建一个对象,把这个对象作为构造函数的this环境,并把这个对象的原型(proto)指向构造函数的prototype,最后返回这个对象继承
主要实现的功能是:this指向的绑定,原型链的构建- ES6的语法
class
,extends
可以提供更为清晰简洁的写法,但是本质上的原理大致相同
【机制】JavaScript的原型、原型链、继承的更多相关文章
- 深入浅出JavaScript之原型链&继承
Javascript语言的继承机制,它没有"子类"和"父类"的概念,也没有"类"(class)和"实例"(instanc ...
- 深入浅出理解Javascript原型概念以及继承机制(转)
在Javascript语言中,原型是一个经常被讨论到但是有非常让初学者不解的概念.那么,到底该怎么去给原型定义呢?不急,在了解是什么之前,我们不妨先来看下为什么. Javascript最开始是网景公司 ...
- JavaScript系列--浅析原型链与继承
一.前言 继承是面向对象(OOP)语言中的一个最为人津津乐道的概念.许多面对对象(OOP)语言都支持两种继承方式::接口继承 和 实现继承 . 接口继承只继承方法签名,而实现继承则继承实际的方法.由于 ...
- 一步步学习javascript基础篇(5):面向对象设计之对象继承(原型链继承)
上一篇介绍了对象创建的几种基本方式,今天我们看分析下对象的继承. 一.原型链继承 1.通过设置prototype指向“父类”的实例来实现继承. function Obj1() { this.name1 ...
- javascript中继承(一)-----原型链继承的个人理解
[寒暄]好久没有更新博客了,说来话长,因为我下定决心要从一个后台程序员转为Front End,其间走过了一段漫长而艰辛的时光,今天跟大家分享下自己对javascript中原型链继承的理解. 总的说来, ...
- javascript原型链继承
一.关于javascript原型的基本概念: prototype属性:每个函数都一个prototype属性,这个属性指向函数的原型对象.原型对象主要用于共享实例中所包含的的属性和方法. constru ...
- 对Javascript 类、原型链、继承的理解
一.序言 和其他面向对象的语言(如Java)不同,Javascript语言对类的实现和继承的实现没有标准的定义,而是将这些交给了程序员,让程序员更加灵活地(当然刚开始也更加头疼)去定义类,实现继承 ...
- Javascript 组合继承 原型链继承 寄生继承
Javascript继承通常有三种方式. 第一种:组合式继承: function SuperType(name) { this.name = name; this.colors = ["re ...
- 三张图搞懂JavaScript的原型对象与原型链 / js继承,各种继承的优缺点(原型链继承,组合继承,寄生组合继承)
摘自:https://www.cnblogs.com/shuiyi/p/5305435.html 对于新人来说,JavaScript的原型是一个很让人头疼的事情,一来prototype容易与__pro ...
- 8条规则图解JavaScript原型链继承原理
原形链是JS难点之一,而且很多书都喜欢用一大堆的文字解释给你听什么什么是原型链,就算有图配上讲解,有的图也是点到为止,很难让人不产生疑惑. 我们先来看一段程序,友情提示sublimeText看更爽: ...
随机推荐
- Moviepy音视频开发:开发视频转gif动画或jpg图片exe图形化工具的案例
☞ ░ 前往老猿Python博文目录 ░ 一.引言 老猿之所以学习和研究Moviepy的使用,是因为需要一个将视频转成动画的工具,当时在网上到处搜索查找免费使用工具,结果找了很多自称免费的工具,但转完 ...
- Python使用property函数和使用@property装饰器定义属性访问方法的异同点分析
Python使用property函数和使用@property装饰器都能定义属性的get.set及delete的访问方法,他们的相同点主要如下三点: 1.定义这些方法后,代码中对相关属性的访问实际上都会 ...
- 转:【Python3网络爬虫开发实战】6.4-分析Ajax爬取今日头条街拍美图
[摘要] 本节中,我们以今日头条为例来尝试通过分析Ajax请求来抓取网页数据的方法.这次要抓取的目标是今日头条的街拍美图,抓取完成之后,将每组图片分文件夹下载到本地并保存下来. 1. 准备工作 在本节 ...
- PyQt学习随笔:ListView控件删除一项列表项的方法
ListView控件可以通过控件对应数据存储删除列表项,具体使用: 数据存储.removeRow(元素索引位置) 删除指定位置的一个列表项. 数据存储如果不知道程序定义的数据存储名,可以通过model ...
- 对flask的学习
任务需求:一个登录,注册页面 任务环境:pycharm 2018 专业版,python3.7,win 10专业版 ------------------------------------------- ...
- Tomcat启动报错org.apache.catalina.core.StandardContext.startInternal One or more listeners failed to start. Full details will be found in the appropriate container log file
错误: 今天SVN导入新项目后启动项目时控制台报错,之后在网上搜了很多方法.下面列了一些大佬的解决方案: 1. 检查日志配置文件-logging.properties:https://www.cnbl ...
- JDBC(一)—— JDBC概述
Jdbc概述 Java DataBase connectivity(Java语言连接数据库) Jdbc本质是什么? 是Sun公司制定的一套接口,java.sql.* 接口都有调用者和实现者 面向接口调 ...
- 题解 CF611H 【New Year and Forgotten Tree】
Solution 提供一种新思路. 首先考虑如何判断一个状态是否合法. 考虑把所有十进制长度一样的数缩成一个点. 这样的点的个数 \(\le 5\). 蒟蒻猜了一个结论:只要满足对于所有缩出来的点的子 ...
- AcWing 332. 股票交易
大型补档计划 题目链接 \(f[i][j]\) 表示前 \(i\) 天,手里有 \(j\) 个股票挣得最多钱 买股票.枚举 \(u < i - W\) \(f[i][j] = max(f[u][ ...
- AcWing 324. 贿赂FIPA
题目链接 大型补档计划 \(f[i][j]\) 表示第 \(i\) 个国家,获得 \(j\) 个国家支持,用的最少花费 \(f[i][0] = 0\) \(f[i][sz[i]] = w[i]\) 对 ...