作用域、原型链、继承与闭包详解

注意:本章讲的是在es6之前的原型链与继承。es6引入了类的概念,只是在写法上有所不同,原理是一样的。

几个面试常问的几个问题,你是否知道

  1. instanceof的原理
  2. 如何准确判断变量的类型
  3. 如何写一个原型链继承的例子
  4. 描述new一个对象的过程

也许有些同学知道这几个问题的答案,就会觉得很小儿科,如果你还不知道这几个问题的答案或者背后所涉及到的知识点,那就好好看完下文,想必对你会有帮助。先不说答案,下面先分析一下涉及到的知识点。

什么是构造函数

JavaScript没有类的概念,JavaScript是一种基于对象的语言,除了五中值类型(number boolean string null undefined)之外,其他的三种引用类型(object、Array、Function)本质上都是对象,而构造函数其实也是普通的函数,只是可以使用构造函数来实例化对象。

事实上,当任意一个普通函数用于创建一类对象时,它就被称作构造函数。像js的内置函数Object、Array、Date等都是构造函数。

在定义构造函数有以下几个特点:

  • 以大写字母开头定义构造函数
  • 在函数内部对新对象(this)的属性进行设置
  • 返回值必须是this,或者其它非对象类型的值

下面定义一个简单的、标准的构造函数:

function Obj(){
this.name = 'name'
return this // 默认有这一行
}
var foo = new Obj() // 使用上面定义的构造函数创建一个对象实例

原型特性

js原型有5个特点,记住这5条特点,相信你一定会弄明白长期困扰你的原型关系。

  1. 除了null所有引用类型(Object、Array、Function)都有对象特性,也就是都可以自由扩展属性。
  2. 所有引用类型都有一个_proto_属性(又称为:隐式属性),_proto_是一个普通的对象。所有的对象都会有一个constructor属性,constructor始终指向创建当前对象的构造函数
  3. 所有的函数都有一个prototype属性(又称为:显式属性),也是一个普通对象,这个prototype有一个constructor属性指向该函数。
  4. 所有的引用类型的_proto_属性指向它的构造函数的prototype属性(比如:obj._proto_指向Object.prototype,obj是定义的一个普通对象,Object是js的内置函数)
  5. 当从一个对象中获得某个属性时,如果这个对象没有该属性,就会去它的_proto_(也就是它的构造函数的prototype)中去寻找

先来解释一下这几条:
第一条的自由扩展性可以通过一个简单的例子来看

var obj = {}
obj.name = 'name'
console.log(obj) // {name:'name'}

第二条和第三条是javascript就是这么规定的,没什么好说的

第四条可以这么理解,当定义一个引用类型的变量var obj = {} 其实是var obj = new Object()的语法糖,这样Object就是obj的构造函数,根据第4条规定,obj._proto_ === Object.prototype,如果不理解可以看看上一章我们讲的js内置函数和上面讲的构造函数

第五条应该好理解,当从obj中获取某个属性时,如果obj中没有定义该属性,就会逐级去它的_proto_对象中去寻找,而它的_proto_指向Object的prototype,也就是从Object的prototype对象中去寻找。

原型链与继承

如果上面明白了原型,那么原型链就会很好理解

根据原型定义的第4条和第5条,很容易发现通过对象的_proto_和函数的prototype把我们变量和构造函数(自定义的构造函数以及内置构造函数)像链子一样链接起来,所以又叫他原型链。

有了原型链,就有了继承,继承就是一个对象像继承遗产一样继承从它的构造函数中获得一些属性的访问权。从下面一个小例子理解:

function Animal (name) {
// 属性
this.name = name || 'Animal';
// 实例方法
this.sleep = function(){
console.log(this.name + '正在睡觉!');
}
}
// 原型方法
Animal.prototype.eat = function(food) {
console.log(this.name + '正在吃:' + food);
};
// 原型继承
function Cat(){
}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';

上面例子中在Foo构造函数的prototype中自定义一个somefn函数。然后通过new Foo()创建一个对象实例并赋值给bar变量,此时bar就等于{name:'bar'}。然后bar.somefn就去bar对象中寻找somefn这个属性,发现找不到,然后就去它的_proto_(其实就是Foo的prototype)中寻找,发现somefn就在Foo的prototype中定义了,就可以愉快的调用并执行somefn了。

这里其实就是一个原型链与继承的典型例子,开发中可能构造函数复杂一点,属性定义的多一些,但是原理都是一样的。

留一个问题,根据上面例子,如果执行bar.stString(),应该去哪里找toString这个方法? (提示:prototype也是普通对象,也有自己的_proto_)

几种继承方式

这几种都是es5中的继承,es6中提供了class类,继承起来更方便。

原型继承

上述例子就是一个原型继承:

function Animal (name) {
// 属性
this.name = name || 'Animal';
// 实例方法
this.sleep = function(){
console.log(this.name + '正在睡觉!');
}
}
// 原型方法
Animal.prototype.eat = function(food) {
console.log(this.name + '正在吃:' + food);
};
// 原型继承
function Cat(){
}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat'; var cat = new Cat()
console.log(cat instanceof Animal); //true
console.log(cat instanceof Cat); //true

优点:

  • 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
  • 简单,易于实现

缺点

  • 要想为子类新增属性和方法,必须要在new Animal()这样的语句之后执行,不能放到构造器中
  • 无法实现多继承
  • 来自原型对象的引用属性是所有实例共享的(严重缺点)
  • 创建子类实例时,无法向父类构造函数传参(严重缺点)

构造继承

function Animal (name) {
// 属性
this.name = name || 'Animal';
// 实例方法
this.sleep = function(){
console.log(this.name + '正在睡觉!');
}
}
// 原型方法
Animal.prototype.eat = function(food) {
console.log(this.name + '正在吃:' + food);
};
// 构造继承
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
} // Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep()); // Tom正在睡觉!
// console.log(cat.eat('fish')); // cat.eat is not a function
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true

优点

  • 解决了1中,子类实例共享父类引用属性的问题
  • 创建子类实例时,可以向父类传递参数
  • 可以实现多继承

缺点

  • 实例并不是父类的实例,只是子类的实例
  • 只能继承父类的实例属性和方法,不能继承原型属性/方法
  • 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

实例继承

function Animal (name) {
// 属性
this.name = name || 'Animal';
// 实例方法
this.sleep = function(){
console.log(this.name + '正在睡觉!');
}
}
// 原型方法
Animal.prototype.eat = function(food) {
console.log(this.name + '正在吃:' + food);
}; // 实例继承
function Cat(name){
var instance = new Animal();
instance.name = name || 'Tom';
return instance;
}
var cat = new Cat(); // 或者可以直接var cat = Cat()
console.log(cat.name);
console.log(cat.sleep()); // Tom正在睡觉!
console.log(cat.eat('fish')); // Tom正在吃:fish
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // false

优点

  • 不限制调用方式,不管是new Cat()还是Cat(),返回的对象具有相同的效果

缺点

  • 实例是父类的实例,不是子类的实例
  • 不支持多继承

组合继承

function Animal (name) {
// 属性
this.name = name || 'Animal';
// 实例方法
this.sleep = function(){
console.log(this.name + '正在睡觉!');
}
}
// 原型方法
Animal.prototype.eat = function(food) {
console.log(this.name + '正在吃:' + food);
};
// 组合继承
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep()); // Tom正在睡觉!
console.log(cat.eat('fish')); // Tom正在吃:fish
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true

优点

  • 弥补了方式2的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
  • 既是子类的实例,也是父类的实例
  • 不存在引用属性共享问题
  • 可传参
  • 函数可复用

缺点

  • 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)

寄生继承

var ob = {name:"小明",friends:['小花','小白']};

function object(o){
function F(){}//创建一个构造函数F
F.prototype = o;
return new F();
} //上面再ECMAScript5 有了一新的规范写法,Object.create(ob) 效果是一样的 function createOb(o){
var newob = object(o);//创建对象
newob.sayname = function(){//增强对象
console.log(this.name);
} return newob;//指定对象
} var ob1 = createOb(ob);
ob1.sayname()

寄生继承原理尚不明白。

寄生组合继承

寄生组合继承有两种方式:

第一种:利用创建没有实例方法的函数

function Animal (name) {
// 属性
this.name = name || 'Animal';
// 实例方法
this.sleep = function(){
console.log(this.name + '正在睡觉!');
}
}
// 原型方法
Animal.prototype.eat = function(food) {
console.log(this.name + '正在吃:' + food);
}; //寄生组合继承
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
(function(){
// 创建一个没有实例方法的类
var Super = function(){};
Super.prototype = Animal.prototype;
//将实例作为子类的原型
Cat.prototype = new Super();
Cat.prototype.constructor = Cat;
})(); var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep()); // Tom正在睡觉!
console.log(cat.eat('fish')); // Tom正在吃:fish
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true

第二种:利用Object.create函数

// 寄生继承核心方法
function inheritPrototype(Parent, Children){
var prototype = Object.create(Parent.prototype);
prototype.constructor = Children;
Children.prototype = prototype;
}
// 父类
function Animal (name) {
// 属性
this.name = name || 'Animal';
// 实例方法
this.sleep = function(){
console.log(this.name + '正在睡觉!');
}
}
// 原型方法
Animal.prototype.eat = function(food) {
console.log(this.name + '正在吃:' + food);
}; // 子类
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
inheritPrototype(Animal, Cat) var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep()); // Tom正在睡觉!
console.log(cat.eat('fish')); // Tom正在吃:fish
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true

Object.create其实与以下代码等价

function object(o){
function f(){}
f.prototype = o;
return new f();
}

优点

  • 最完美的继承解决方案

缺点

  • 实现复杂

解答一下最一开始提出的问题

看到这里应该对原型链与继承的原理有所了解了,再回头看上面的问题,你也会发现这都是小儿科。
第一个问题:instanceof原理?

var arr = []
arr instanceof Array

instanceof原理就是利用了原型链,当执行arr instanceof Array时,会从arr的_proto_一层一层往上找,看是否能不能找到Array的prototype。
我们知道var arr = [] 其实是var arr = new Array()的语法糖,所以arr的_proto_指向Array的prototype,结果返回true

第二个问题:如何准确判断变量类型?
可以使用instanceof帮助我们判断,而不是typeof

第三个问题:如何写一个原型链继承的例子?

function Foo () {
this.name = 'name'
this.run = function () {
console.log(this.name)
}
}
function Bar () {}
Bar.prototype = new Foo() // 从构造函数Foo中继承
var baz = new Bar()
baz.run() // 打印出 'name'

第四个问题:描述new一个对象的过程

  1. 创建一个新的对象,
  2. 获得构造函数的prototype属性,并把prototype赋值给新对象的_proto_,this指向这个新对象
  3. 执行构造函数,返回构造函数的内容

Js基础知识(二) - 原型链与继承精彩的讲解的更多相关文章

  1. 第20篇 js高级知识---深入原型链

    前面把js作用域和词法分析都说了下,今天把原型链说下,写这个文章费了点时间,因为这个东西有点抽象,想用语言表达出来不是很容易,我想写的文章不是简单的是官方的API的copy,而是对自己的知识探索和总结 ...

  2. JS基础知识二

    JS控制语句 switch 语句用于基于不同的条件来执行不同的动作 <script> function myFunction(){ var x; var d=new Date().getD ...

  3. JS基础-原型链和继承

    创建对象的方法 字面量创建 构造函数创建 Object.create() var o1 = {name: 'value'}; var o2 = new Object({name: 'value'}); ...

  4. JS原型链与继承别再被问倒了

    原文:详解JS原型链与继承 摘自JavaScript高级程序设计: 继承是OO语言中的一个最为人津津乐道的概念.许多OO语言都支持两种继承方式: 接口继承 和 实现继承 .接口继承只继承方法签名,而实 ...

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

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

  6. 小谈js原型链和继承

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

  7. 【转】js原型链与继承

    原文链接:https://blog.csdn.net/u012468376/article/details/53127929 一.继承的概念 ​ 继承是所有的面向对象的语言最重要的特征之一.大部分的o ...

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

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

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

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

随机推荐

  1. python网络爬虫(3)python爬虫遇到的各种问题(python版本、进程等)

    import urllib2 源地址 在python3.3里面,用urllib.request代替urllib2 import urllib.request as urllib2 import coo ...

  2. asp.net core在发布时排除配置文件

    使用命令发布 dotnet restore dotnet publish -c Release -r win-x64 -o "D:\services" 这样发布总是是将配置文件覆盖 ...

  3. iis 8.0 HTTP 错误 404.3 server 2012

    最近在学习WCF,发现将网站WCF服务放到IIS上时不能正常运行,从网上搜了一下: 解决方法,以管理员身份进入命令行模式,运行: "%windir%\Microsoft.NET\Framew ...

  4. 三、redis学习(jedis连接池)

    一.jedis连接池 二.jedis连接池+config配置文件 三.jedis连接池+config配置文件+util工具类 util类 public class JedisPoolUtils { / ...

  5. O006、CPU和内存虚拟化原理

    参考https://www.cnblogs.com/CloudMan6/p/5263981.html   前面我们成功的把KVM跑起来了,有了些感性认识,这个对于初学者非常重要.不过还不够,我们多少要 ...

  6. MyCat配置简述以及mycat全局ID

    Mycat可以直接下载解压,简单配置后可以使用,主要配置项如下: 1. log4j2.xml:配置MyCat日志,包括位置,格式,单个文件大小 2. rule.xml: 配置分片规则 3. schem ...

  7. Java注解的继承

    注解继承的说明 1.首先要想Annotation能被继承,需要在注解定义的时候加上@Inherited,并且如果要被反射应用的话,就需要还有个事@Retention(RetentionPolicy.R ...

  8. linux 软件安装目录详解

    我一般会在/opt目录下创建 一个software目录,用来存放我们从官网下载的软件格式是.tar.gz文件,或者通过 wget+地址下载的.tar.gz文件 执行解压缩命令,这里以nginx举例 t ...

  9. Hive入门指南

    转自:http://blog.csdn.net/zhoudaxia/article/details/8842576 1.安装与配置 Hive是建立在Hadoop上的数据仓库软件,用于查询和管理存放在分 ...

  10. 查看jar包依赖树

    在eclipse执行如下命令: 可以在控制台上查看层级依赖关系