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

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

注意:本章讲的是在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. 执行构造函数,返回构造函数的内容

原文地址:https://www.cnblogs.com/homehtml/p/12037266.html

时间: 2024-08-09 18:17:16

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

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

前面把js作用域和词法分析都说了下,今天把原型链说下,写这个文章费了点时间,因为这个东西有点抽象,想用语言表达出来不是很容易,我想写的文章不是简单的是官方的API的copy,而是对自己的知识探索和总结的过程,以及在这个过程碰到的问题都一一写出来,我想大多数人应该也有这个疑惑,然后带着疑惑去找答案,当你把这个疑惑解决之后,才觉得很有成就感.下面不多说了,开始说说什么是原型链.要想了解原型链,还是要从简单的开始,什么是原型?首先看下代码: function funcA() { this.show =

JS基础知识回顾:ECMAScript的语法(二)

ECMAScript中有五种简单数据类型(也称为基本数据类型):Undefined.Null.Boolean.Number.String ECMAScript还有一种复杂数据类型——Object,Object本质上是由一组无序的名值对组成的. ECMAScript不支持任何创建自定义类型的机制,而所有值最终都将是上述六种数据类型之一,由于ECMAScript的数据类型具有动态性,因此的确没有再定义其他数据类型的必要了. 监狱ECMAScript是松散类型的,因此需要有一种手段来检测给定变量的数据

深入理解JS原型链与继承

我 觉得阅读精彩的文章是提升自己最快的方法,而且我发现人在不同阶段看待同样的东西都会有不同的收获,有一天你看到一本好书或者好的文章,请记得收藏起来, 隔断时间再去看看,我想应该会有很大的收获.其实今天要讨论的主题,有许多人写过许多精彩的文章,但是今天我还是想把自己的理解的知识记录下来.我在学习 掌握JS原型链和继承的时候,就是看得@阮一峰老师的写的文章,觉得他写的技术类的文章都容易让理解,简明概要,又好理解.他是我学习JS路程里面一个比较佩服的导师,昨天重新看了他写的<Javascript 面向

小谈js原型链和继承

原型(prototype)在js中可是担当着举足轻重的作用,原型的实现则是在原型链的基础上,理解原型链的原理后,对原型的使用会更加自如,也能体会到js语言的魅力. 本文章会涉及的内容 原型及原型对象 原型链(JavaScript核心部分) 类的继承 instanceof constructor 我们先用一个构造器来实现一个构造函数: function A(){ this.mark = "A"; this.changeMark = function(){ this.mark += &qu

JS基础知识回顾:引用类型(二)

ECMAScript中的Date类型是在早期Java中的java.util.Date类基础上构建的. 因此,Date类型使用自UTC(Coordinated Universal Time,国际协调时间)1970年1月1日午夜零点开始经过的毫秒数来保存日期. 在使用这种数据存储格式的条件下,Date类型保存的日期能够精确到1970年1月1日或之后的285616年. 要创建一个日期对象,使用new操作符和Date构造函数即可:var now=new Date(); 在调用Date构造函数而不传递参数

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

js原型链与继承是js中的重点,所以我们通过以下三个例子来进行详细的讲解. 首先定义一个对象obj,该对象的原型为obj._proto_,我们可以用ES5中的getPrototypeOf这一方法来查询obj的原型,我们通过判断obj的原型是否与Object.prototype相等来证明是否存在obj的原型,答案返回true,所以存在.然后我们定义一个函数foo(),任何一个函数都有它的prototype对象,即函数的原型,我们可以在函数的原型上添加任意属性,之后通过new一个实例化的对象可以共享

js原型链与继承

先看看JAVA中继承的定义 :Java继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类. 所以继承需要实现的是:能够拥有父类的方法和属性,也能自己定义新的方法和属性; 那么直接用原型链会有什么问题? //定义一个CarModel类 function CarModel(c){ this.color=c||"白色"; this.getColor=function(){ console.log('我的颜色是'+

浅谈js原型链与继承

js原型链与继承是js中的重点,所以我们通过以下三个例子来进行详细的讲解. 首先定义一个对象obj,该对象的原型为obj._proto_,我们可以用ES5中的getPrototypeOf这一方法来查询obj的原型,我们通过判断obj的原型是否与Object.prototype相等来证明是否存在obj的原型,答案返回true,所以存在.然后我们定义一个函数foo(),任何一个函数都有它的prototype对象,即函数的原型,我们可以在函数的原型上添加任意属性,之后通过new一个实例化的对象可以共享

js 原型链和继承(转)

在理解继承之前,需要知道 js 的三个东西: 什么是 JS 原型链 this 的值到底是什么 JS 的 new 到底是干什么的 1. 什么是 JS 原型链? 我们知道 JS 有对象,比如 var obj = { name: "obj" }; 我们通过控制台把 obj 打印出来: 我们会发现 obj 已经有几个属性(方法)了.那么问题来了:valueOf / toString / constructor 是怎么来?我们并没有给 obj.valueOf 赋值呀. 上面这个图有点难懂,我手画