第十八节: JavaScript原型链与继承

一.原型链机制

1. 原型链的本质

只要是对象,一定有原型对象,就是说只要这个东西是个对象,那么一定有proto属性。(错的)

我们想实例的原型对象也是一个对象,那么我们迫切的想看看这个原型对象的原型对象.如果这理论没有问题的话,这个原型对象应该也有proto指向他的原型对象吧.

function People(){
    
}
var xiaoming = new People();
// 原型对象的原型对象
console.log(xiaoming.__proto__.__proto__);
// 原型对象的原型对象的构造函数是谁
console.log(xiaoming.__proto__.__proto__.constructor); //Object

// 那我们看看还能不不能再向上查原型对象
console.log(xiaoming.__proto__.__proto__.__proto__);
// 结果为null

通过上面的示例, 发现开始的一句话其实是错误的.

JS的世界中只有一个对象没有原型对象,这个对象就是Object.prototype。

现在就能想明白一些事情,一个对象天生带一些属性和方法

比如

function People(){
    
}
var xiaoming = new People();
// 小白马上可以调用toString()方法
console.log(xiaoming.toString());
// 打印 "[object Object]"

// 这个方法是小明原型对象的原型对象身上的方法,我们可以看下
console.log(xiaoming.__proto__.__proto__);

Object是一个函数,是系统内置的构造函数,用于创造对象的。Object.prototype是所有对象的原型链终点。

所以,当我们在一个对象上打点调用某个方法的时候,系统会沿着原型链去寻找它的定义,一直找到Object.prototype。

   function AA(){
     console.log('我是函数');
   }
   var aa = new AA;
   console.log(aa.__proto__);   //{}
   console.log(aa.__proto__.__proto__);   //原型链最后一个对象{}
   console.log(aa.__proto__.__proto__.__proto__); //原型链最后一个对象的原型就是null
   console.log(aa.toString());//aa没有他这个方法,aa原型上有toString方法[object Object] 
   console.log(aa.__proto__.__proto__.constructor);//创造对象的构造函数
   console.log(aa.__proto__.__proto__.constructor.prototype) //所有对象的原型链终点

Object.prototype是所有对象原型链的终点,现在我强行给它添加一个属性

Object.prototype.sayHello = function(){
    alert("你好")
}
// 现在小明能sayHello
xiaoming.sayHello();

// 岂止xiaoming能sayHello 数组也能sayHello
var arr = [1,2,3];
arr.sayHello();

// 然后世界上一切都能sayHello了
"么么哒".sayHello();

Object.prototype是所有对象的原型链的终点,所以我们直接给Object.prototype增加一个方法,那么世界上所有的对象都能调用这个方法:

2. 引用类型的构造函数

所有的引用类型值,都有内置构造函数。比如

new Object()

new Array()

new Function()

new RegExp()

new Date()

我们来看看数组的情况:

// 现在是一个数组字面量
var arr = [66,4343,23];
// arr.haha = 23;
// console.log(arr.haha); //23

console.log(arr.__proto__);   // 不用管,直接看constructor
console.log(arr.__proto__.constructor);

// 寻找原型链的终点,发现是Object.prototype
console.log(arr.__proto__.__proto__.constructor)

函数也是对象。JavaScript中函数是一等公民,函数是对象。函数也是对象,只不过自己能()执行。

function People(){
    
}
People.haha = 25;
//People.haha++;
//People.haha++;
console.log(People.haha);   //27

var xiaoming = new People();
console.log(xiaoming.haha);    // undefined

console.log(People.__proto__);   // 不用管,直接看constructor
console.log(People.__proto__.constructor);
// 我们之前说过构造函数没有__proto__,只有prototype,只有构造函数的实例才用__proto__,那是为了让你们区分类和实例之间的区别
// 现在这个函数本身就是一个对象

示例:

我们可以利用原型链的机制,给数组对象增加方法

//数组
var arr = [45,34,23,45,65,76,45];

//我们可以利用原型链的机制,给数组对象增加方法
Array.prototype.max = function(){
    var max = -Infinity;
    for(var i = 0 ; i < this.length ; i++){
        if(this[i] > max){
            max = this[i];
        }
    }
    return max;
}

//任何数组都能调用这个方法了:
console.log(arr.max());
3. 基本类型的包装类

基本类型值,也有包装类型。所谓包装类型,就是它的构造函数。

new Number()

new String()

new Boolean()

字符串

var str = new String("你好啊");
console.log(str);        //String {"你好啊"} ,类数组
console.log(str[0])
console.log(str.toString()); //"你好啊"


// 这是神经病,还不如用字面量方式创建你,new String()这个方式没人用
// 不管怎么样,我今天就是告诉你任何一个字符串字面量,它不是一个对象,但它有一个包装类

var str = "你好啊"
// 它有一个内部机制是通过上面方式出来的

// 但是js就是拧巴
console.log(str.__proto__.constructor);
console.log(str.__proto__.__proto__.constructor);

// 字符串有个包装类,你可以理解"你好啊"是包装类new出来的,没有实际意义
// 但是你不能说字符串时对象,但是它确实有__proto__


var str = "wuwei";
console.log(str.length)

字符串转成包装类才能调用方法和属性

var str = "abcdefghijklmnopqrstuvwxyz"
console.log(new String(str));
var ap =document.querySelector('p');
ap.innerHTML = (str.bold()).fontsize(30);//直接将样式插到HTML中,现在已经废弃
console.log(str.__proto__)  //str原型对象是String对象
console.log(str.__proto__.__proto__)  //str原型对象的原型对象是Object对象

数值

var num = 123;
console.log(num.__proto__.constructor);

布尔值

var a = true;
console.log(a.__proto__.constructor);

null,undefined

var b = null;
console.log(b.__proto__.constructor); //报错
// Uncaught TypeError: Cannot read property '__proto__' of null

var c = undefined;
console.log(c.__proto__.constructor); //报错
// Uncaught TypeError: Cannot read property '__proto__' of undefined


//JS中所有函数原型的构造器一览   =====>如下所示
Object.prototype.constructor === Object
Number.prototype.constructor === Number
Array.prototype.constructor === Array

//所有函数原型的原型都是Object.prototype,如下所示
Function.prototype.__proto__ === Object.prototype
Number.prototype.__proto__ === Object.prototype
Array.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null

//JS中所有函数的构造器都是Funtion,如下所示�    
Object.constructor === Function
Number.constructor === Function
Array.constructor === Function

//所有函数的原型都是Function.prototype,如下所示
Object.__proto__ === Function.prototype
Number.__proto__ === Function.prototype
Array.__proto__ === Function.prototype

二. 对象与属性

判断属性是否存在

1. 对象直接打点验证某个属性是否存在

对象打点调用属性,我们之前的课程已经讲过,遍历原型链。所以就可以看出来属性是否在自己身上或原型链上。如果在,就返回值;如果不在就返回undefined.

var obj = {
    a : 1,
    b : 2,
    c : 3
    // m: undefined
}
obj.__proto__ = {
    d : 4
}

console.log(obj.m); //undefined
console.log(obj.a); //1
console.log(obj.b); //2
console.log(obj.c); //3
console.log(obj.d); //4

有个误会,比如obj.m值就是undefined,那么obj.m还是返回undefined。不知道m属性存在不存在。此时就需要下面的方法解决了

2. in 运算符

返回一个布尔值,表示这个属性是不是对象的属性。

var obj = {
    a : 1,
    b : 2,
    c : false
}

console.log("a" in obj);    //true
console.log("b" in obj);    //true
console.log("c" in obj);    //true
console.log("d" in obj);    //false

// 如果原型上有方法
obj.__proto__ = {
    d: 20
}
console.log("d" in obj);    //true

in不仅仅检测是对象自己有没有这个属性,如果原型链上有这个属性,那么也会返回true。整个原型链如果没有这个属性,就返回false。也就是说,in操作符会进行原型链查找。

for in 这个循环,会把原型链上所有的可枚举的属性列出来:

    var obj = {
      a: 1,
      b: 2,
      c: false
    }
    obj.__proto__ = {
      d: 20
    }
    for (var key in obj) {
      console.log(key);
    }

什么是可枚举,系统默认的属性(比如constructor)都是不可枚举的。for in循环能够把自己添加的属性罗列出来,罗列的不仅仅是自己身上的属性,还有原型链上的所有属性,不方便区分。

3. hasOwnProperty方法(是不是自己的属性)

这个方法定义在了Object.prototype对象上面,所以任何一个Object都能够拥有这个方法。

这个方法返回true、false。表示自己是否拥有这个属性,不考虑原型链。就看自己身上有没有这个属性,不进行原型链查找。

var obj = {
    a : 1,
    b : 2,
    c : 3
}
obj.__proto__ = {
    d : 4
}

console.log(obj.hasOwnProperty("a")); //t
console.log(obj.hasOwnProperty("b")); //t
console.log(obj.hasOwnProperty("c")); //t
console.log(obj.hasOwnProperty("d")); //f

for……in考虑原型链,所以我们可以内嵌一个判断,把自己身上的属性输出:

for(var k in obj){
    obj.hasOwnProperty(k) && console.log(k);
}

最可靠的还是这两个结合在一起的方式

定义属性Object.defineProperty() 一个对象定义属性

js引擎允许对属性操作的控制,需要使用方法Object.defineProperty()来实现。这个方法接收三个参数:属性所在的对象、属性的名字、描述符对象。

      var obj = {
        name: "张三"
      }
      // defineProperty  对象属性的描述 ,defineProperty挂载到Object身上的
      //     第一个参数:给哪个对象添加属性
      //     第二个参数:添加的属性名 要加引号
      //     第三个参数:属性的描述对象
      Object.defineProperty(obj, 'age', {
        //  value:1111,  //表示属性值,不要与get set一起设置,控制台中age的值是浅色的
        //get set都是函数
        get: function () {     //获取
          console.log("你要获取age属性", arguments);  //arguments没有实参
          return 123;         //return决定获取的值
        },
        set: function (value) {       //设置
          console.log("你现在正在给age赋值", arguments);
        }
      })

      obj.age = "777777"  //给obj.age设置值,就会调用set函数了
      console.log(obj);//此时控制台中age的是(…)颜色是浅色的,说明值是动态获取的
// console.log(obj.age); //获取obj.age的值,实际上走了get方法

configurable:表示能否通过 delete 删除属性从而重新设置属性

 var obj = {
      name: "张三"
    }
    Object.defineProperty(obj, 'age', {
      configurable: true,//设置了configurable为true,就可以删除age属性了  
      get: function () {
        console.log("你要获取age属性", arguments);
        return 123;
      },
      set: function (value) {
        console.log("你现在正在给age赋值");
      }
    })
    console.log(obj);



    var obj = {
      name: "张三",
      sex: "女"
    }
    Object.defineProperty(obj, 'name', {
      configurable: false   //name之前是可以删除的,设置configurable:false后,不可以删除了
    })
    console.log(delete obj.name);
    console.log(obj);

是否可以利用defineProperty方法重新定义属性(正常情况下是可以反复使用该方法重新设置属性的)。默认为true。

js自带的属性都是灰色的,enumerable默认为false,不能通过for in遍历。

    var obj = {
      name: "张三",
      sex: "女"
    }
    Object.defineProperty(obj, 'age', {  
      enumerable:true,   //age在控制台中是灰色的,不会被for(var key in obj)遍历到,深色的属性才会遍历到,想要遍历到添加enumerable:true变成深色的
      get(){
           return 34;
         }
    })
    console.log(obj);
    for(var key in obj){ console.log(key)}

writable:给属性重新赋值。是否允许配置

    var obj = {
      name: "张三",
      sex: "女"
    }
    Object.defineProperty(obj, 'age', {  
      value:1111,
      writable:true
    })
    obj.age =2222;  // 此时不能赋值,添加writable:true就可以给age赋值了
    console.log(obj);

value:属性值,读一个对象的属性值时先读的是这个值。默认值为 undefined。

定义多个属性Object.defineProperties()

var book = {};
    Object.defineProperties(book, {
        _year: {
            value: 2004
        },
        edition: {
            value: 1
        },
        year: {
            get: function () {
                return this._year;
            },
            set: function (newValue) {
                if (newValue > 2004) {
                    this._year = newValue;
                    this.edition += newValue - 2004;
                }
            }
        }

    });

读取属性的描述对象Object.getOwnPropertyDescriptor()

Object.getOwnPropertyDescriptor(obj,"name")
4. instanceof 运算符

类在英语里面叫做class,实例在英语里面叫做instance。

instaceof运算符:

    function AA() { }
    var aa = new AA()
    var bb = {}
    console.log(aa instanceof AA);//true  是
    console.log(bb instanceof AA);//false  不是

验证A对象是不是B类的实例。

举例:

//类,构造函数
function Dog(){

}
//实例化一个对象
var d = new Dog();
//验证d是不是Dog的一个实例
console.log(d instanceof Dog);  // true

这里要注意一个事儿:“HelloKitty是狗”:

//类,构造函数
function Dog(){

}

function Cat(){

}
Cat.prototype = new Dog();  //继承

var hellokitty = new Cat(); //通过cat来实例一个
console.log(hellokitty.constructor); //Dog
console.log(hellokitty instanceof Cat); //true
console.log(hellokitty instanceof Dog); //true

instanceof 运算符的机理: 遍访hellokitty这个对象的原型链上的每个原型对象,如果遍访到这个原型对象,是某个构造函数的prototype,那么就认为hellokitty是这个构造函数的实例,返回true。

检查数组:

一个数组用typeof检测的时候,返回object

var arr = [];
console.log(typeof arr);

用instanceof运算符能够轻松解决数组的识别:

var arr = [];
console.log(arr instanceof Array);

ECMAScript5标准中新增了一个API验证数组:

Array.isArray(arr)


var arr=[20,30];
arr.add="加"
console.log(arr);
console.log(Array.isArray(arr));//静态方法,调用主体永远不会发生变化

IE9开始兼容。

总结一下,A instanceof B, 不能证明A是new B()出来的,因为可能是继承。

5. 检测数据类型的方法

Object.prototype.toString.call(),如下图所示

Object.prototype.toString.call(123);  //"[object Number]"
Object.prototype.toString.call("123");  //"[object String]"
Object.prototype.toString.call(true);  //"[object Boolean]"
Object.prototype.toString.call(undefined);  //"[object Undefined]"
Object.prototype.toString.call(null);  //"[object Null]"
Object.prototype.toString.call(function(){});  //"[object Function]"
Object.prototype.toString.call([]);  //"[object Array]"
Object.prototype.toString.call({});  //"[object Object]"

封装判断数据类型

function getTypeof(val){
var  str = Object.prototype.toString.call(val)
var aa = str.split(' ')[1]//按空格切割,要后面的部分
return aa.slice(0,aa.length-1)
}
console.log(getTypeof(123));
console.log(getTypeof('ss'));
console.log(getTypeof(true));
console.log(getTypeof(undefined));

三. 继承

1. 原型链继承

将父类的实例作为子类的原型

function People(name){
    this.name = name;
}
People.prototype.sayHello = function(){
    alert("你好我是" + this.name);
}

// var xiaoming = new People("小明");
// xiaoming.chifan();

function Student(name,xuehao){
    this.name = name;
    this.xuehao = xuehao;
}

//核心语句,继承的实现全靠这条语句了:
Student.prototype = new People('大明');

//Student.prototype.sayHello = function(){
//    alert("你好我是小学生,我的学号是" + this.xuehao);
//}
Student.prototype.study = function(){
    alert("好好学习,天天向上");
}

var xiaohong = new Student("小红",1001);

xiaohong.sayHello();

子类可以覆盖父类的一些方法,父类的方法不冲突,因为我们子类追加的方法,追加到了父类的实例上。

但是父类新增原型方法/原型属性,子类都能访问到,父类一变其它的都变了

2. 构造函数继承

使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)

function People(name,age){
    this.name = name;
    this.age = age
    //这个函数this默认指向window,name属性默认在window身上。想让this指向调用这个函数的对象,显示的绑定调用就可以实现People.call(this,name)
}
function Student(name,xuehao,age){
    //  核心语句
    People.call(this,name,age)//显示的绑定调用
    this.xuehao = xuehao;
}
var xiaohong = new Student("小红",1001,19);
for(var key in xiaohong){
    console.log(key);
}
console.log(xiaohong);

方法都在构造函数中定义, 只能继承父类的实例属性和方法,不能继承原型属性/方法,无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

3. 组合继承

就是将原型链继承和构造函数继承组合在一起;继承两个优点

通过调用父类构造,继承父类的属性并保留传参的优点,

然后再通过将父类实例作为子类原型,实现函数复用

function People(name){
    this.name = name;
}
People.prototype.sayHello = function(){
    alert("你好我是" + this.name);
}

function Student(name,xuehao){
    People.call(this,name);
    this.xuehao = xuehao;
}

//核心语句,继承的实现全靠这条语句了:
Student.prototype = new People('大明');

Student.prototype.study = function(){
    alert("好好学习,天天向上");
}

var xiaohong = new Student("小红",1001);

xiaohong.sayHello();

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

4. 寄生组合继承

通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点

function People(name){
    this.name = name;
}
People.prototype.sayHello = function(){
    alert("你好我是" + this.name);
}

function Student(name,xuehao){
    People.call(this,name);
    this.xuehao = xuehao;
}

//核心语句,继承的实现全靠这条语句了:
function Fn(){}
Fn.prototype = People.prototype
Student.prototype = new Fn();

Student.prototype.study = function(){
    alert("好好学习,天天向上");
}

var xiaohong = new Student("小红",1001);

xiaohong.sayHello();
5. 圣杯模式

圣杯模式就是讲借用的构造函数封装一下

function inherit(People,Student){
    function Fn(){}
    Fn.prototype = People.prototype
    Student.prototype = new Fn();
    Student.prototype.constructor = Student;
    Student.prototype.parent = People;
}
inherit(People,Student )
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容