JavaScript中的对象、原型、原型链、继承

JavaScript中的原型和原型链无疑为重点和难点
如果这两个知识点没弄明白,要写插件框架或者是看一些框架的源码,肯定比较费力甚至看不懂

我觉得有必要先来回顾一下JavaScript中对象,我们在很多资料上都看到过类似的描述:
‘JavaScript中一切皆对象’、‘JavaScript中的所有事物都是对象’

我相信很多人并不赞同这样的说法,或者不确定,或者有疑惑…
比如 var a, b = ‘str’; 难道 a b都是对象?(曾经某个面试官对我提出的疑问)
我个人认为:是的,都是对象
why ???

来看看JavaScript关于对象的定义:
‘对象是若干属性的集合’
‘对象是拥有属性和方法的数据’
‘对象是由一组无序的名值对组成的’

不管是哪种描述,相信已经在研究原型和原型链的你都能理解对象的定义!
OK,既然 a b 是对象,它们有什么属性,有什么方法?
好吧,a 没有属性方法,b 有属性方法,a 显然不符合对象的定义
BUT
再不上代码我也解释不下去了…

var n = 123;
var s = 'abc';
var b = true;
var a = [];
var f = function (){};
var o = {};
var k = null;
var u;
// 运算符:typeof
// 返回值:number、string、boolean、undefined、object、function
console.log(typeof n);// number
console.log(typeof s);// string
console.log(typeof b);// boolean
console.log(typeof a);// object
console.log(typeof f);// function
console.log(typeof o);// object
console.log(typeof k);// object
console.log(typeof u);// undefined

o = { } 对象是对象没毛病吧,a = [ ] 数组是对象没疑问吧,k = null 空对象的引用也是对象说得过去吧
(typeof null 返回 object 其实这是JavaScript最初实现的一个错误,后来被ECMAScript沿用下来)
f = function (){} 在JavaScript中函数也属于对象,相对于其他对象,函数对象比较特殊(后面再讨论这个问题)
那么 n、s、b、u 这四个怎么解释呢?
咱们先来看看 u 吧
在JavaScript中,声明变量不赋值,函数没有明确的 return 结果,这两种情况值都为 undefined
undefined 作为JavaScript的一种数据类型,其值只有一个为 undefined 本身

console.log(null == undefined);// true

what ? 这两货狼狈为奸,居然返回 true
ECMAScript认为:undefined是从null派生出来的,所以把它们定义为值相等
好了,ECMAScript规定了 undefined 是从 null 派生出来的,而且它们的值还是相等的
既然 null 是对象,那 undefined 为什么不是呢?(如果还是觉得undefined不是对象,我无力辩解)

接下来我们来看看 n、s、b 这三个好基友
在ECMAScript中有三个比较特殊的数据类型即 Boolean、Number、String
Boolean、Number、String 是js中的基本数据类型,也叫特殊的引用类型,又叫基本包装类型,还叫‘伪对象’
(当它们尝试转化为对象来时,后台会将其转换成一个临时的包装类型对象,而完成这个访问后,临时对象会被销毁掉)
不管你习惯上面哪种称呼,实际上它们是具有属性和方法的,是符合对象的定义的!

好吧,感觉越跑越偏了…把对象和数据类型的问题先搁置后议!
但是,请先记住一句话:对象分为 普通对象 和 函数对象

属性:prototype(原型)
每个函数对象(Function.prototype除外)都有一个prototype属性,这个属性指向一个对象即原型对象

var fn1 = function (){}; // 函数表达式
var fn2 = new Function(); // 实例化函数对象
function Cat(){}; // 函数声明
console.log(fn1.prototype); // Object{}
console.log(fn2.prototype); // Object{}
console.log(Cat.prototype); // Object{}
// 这里的 Object{} 就是我们所说的原型,它是一个对象也叫原型对象

为什么 Function.prototype 除外呢?看代码:

console.log(Number.prototype);
console.log(String.prototype);
console.log(Function.prototype);
console.log(Function.prototype.prototype);
// 结果看下图

这里写图片描述
我们可以看到这些内置构造函数Number、String等,它们的原型对象都是一个普通对象(Number{ }和String{ })
而Function的原型对象则指向函数对象 function () { [native code] },就是原生代码,二进制编译的!
这个函数对象(Function.prototype)是没有原型对象的,所以它的prototype返回 undefined。

简单说,(fn1.prototype)是一个原型对象,(Cat.prototype)是一个原型对象,(Number.prototype)是一个原型对象…

对于原型好像还是比较模糊?没关系,我们继续来了解它

function Cat(){};
Cat.prototype.name = '小白'; // 给原型对象添加属性
Cat.prototype.color = 'black'; // 给原型对象添加属性
Cat.prototype.sayHello = function (){ // 给原型对象添加方法
    console.log('大家好,我的名字叫'+this.name);
}
var cat1 = new Cat(); // 实例对象
var obj = Cat.prototype;// 这个,是的这个就是原型对象
console.log(obj); // Object {name:"小白", color:"black", sayHello: function...}
console.log(cat1.constructor); // function Cat(){}
console.log(obj.constructor); // function Cat(){}
console.log(Cat.prototype === cat1.constructor.prototype);// true

属性:constructor(构造器)
每个对象都有一个隐藏属性constructor,该属性指向对象的构造函数(类)
通过上面的代码我们可以看到,实例对象cat1和原型对象obj 它们的构造器都是Cat !
结论:原型对象(Cat.prototype)也是 构造函数(Cat)的一个实例。

还是不好理解???我们换一种写法:

function Cat(){}
Cat.prototype = {// 原型对象
    name: '小白',
    color: 'black',
    sayHello: function (){
        console.log('大家好,我的名字叫'+this.name);
    }
}
var cat1 = new Cat();

这样写应该很直观了吧,但是

console.log(Cat.prototype === cat1.constructor.prototype); // false
console.log(cat1.constructor===Object); // true

是不是刚刚感觉可以理解了,又懵逼了!这不是你的错…
请记住,使用对象字面量方式定义的对象,其构造器(constructor)指向的是根构造器Object

那么,原型有什么用呢?
原型的主要作用是用于继承
直接上代码

var Person = function(name){
    this.name = name;
};
Person.prototype.type = 'human';
Person.prototype.getName = function(){
    console.log(this.name);
}
var p1 = new Person('jack');
var p2 = new Person('lucy');
p1.getName(); // jack
console.log(p1.type); // 'human'
p2.getName(); // lucy
console.log(p2.type); // 'human'

示例中通过给原型对象(Person.prototype)添加属性方法
那么由 Person 实例出来的普通对象(p1 p2)就继承了这个属性方法(type getName)

再看一个示例

Object.prototype.jdk = 'abc123';
Object.prototype.sayHi = function (){
    console.log('嗨~大家好');
}
String.prototype.pin = function (){
    console.log(this+'&biubiu');
}
var str = 'yoyo';
var num = 666;
var arr = [];
var boo = true;

str.sayHi(); // 嗨~大家好
num.sayHi(); // 嗨~大家好
arr.sayHi(); // 嗨~大家好
boo.sayHi(); // 嗨~大家好
console.log(str.jdk); // abc123
console.log(num.jdk); // abc123
console.log(arr.jdk); // abc123
console.log(boo.jdk); // abc123
str.pin(); // yoyo&biubiu
num.pin(); // 报错 num.pin is not a function
arr.pin(); // 报错 arr.pin is not a function
boo.pin(); // 报错 boo.pin is not a function

看出点什么了吗?
所有对象都继承了Object.prototype原型上的属性方法(换句话说它们都是Object的实例)
str 还继承了String.prototype原型上的属性方法

再看一个比较实用的示例

Date.prototype.getWeek = function () {
    var arr = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
    var index = this.getDay();//0-6
    return arr[index];
}
var dates = new Date();
console.log(dates.getWeek()); // '星期一'

dates 日期对象继承了 getWeek 方法

原型我们了解啦,具体是怎么实现的继承,就要讲到原型链

属性:_ _ proto _ _(原型)
每个对象都有一个隐藏属性_ _ proto _ _,用于指向创建它的构造函数的原型对象
懵逼……怎么又一个原型???
别着急!!!
上面我们讲prototype是针对每个函数对象,这个_ _ proto _ _是针对每个对象 有点区别的哦~

var n = 123;
var s = 'jdk';
var b = true;
var a = [];
var f = function (){};
var o = {};
var k = null;
var u = undefined;

console.log(n.__proto__); // Number { ... }
console.log(n.__proto__ === Number.prototype);// true

console.log(s.__proto__ === String.prototype);// true
console.log(a.__proto__ === Array.prototype);// true
console.log(f.__proto__ === Function.prototype);// true  function (){}
console.log(o.__proto__ === Object.prototype);// true
console.log(b.__proto__ === Boolean.prototype);// true

对象 通过_ _ proto _ _指向原型对象,函数对象 通过prototype指向原型对象

原型链呢,链在哪?

Object.prototype.jdk = 'abc123';
Object.prototype.sayHi = function (){
    console.log('嗨~大家好');
}
var str = 'yoyo';
str.sayHi();// 嗨~大家好
console.log(str.jdk);// 'abc123'
// 前面写的示例,我们来找找原型链

首先,str 是怎么访问到 sayHi 方法和 jdk 属性的呢
其次,了解一下方法 hasOwnProperty() ,用于判断某个属性是否为该对象本身的一个成员
最后,看看大致的访问过程

console.log(str.hasOwnProperty('sayHi'));//false str自身没有sayHi方法
console.log(str.__proto__.hasOwnProperty('sayHi'));//false 原型对象也没有sayHi方法
console.log(str.__proto__.__proto__.hasOwnProperty('sayHi'));//true 原型的原型有sayHi方法并执行

‘str -> str._ _ proto _ _ -> str._ _ proto _ _ . _ _ proto _ _’ 感觉到什么了?
我们来描述一下执行过程:
str.sayHi() –> 自身查找 –> 没有sayHi方法 –> 查找原型链 str._ _ proto _ _ –> 指向 String.prototype –> 没有sayHi方法
–> 查找原型链 String.prototype._ _ proto _ _ –> 指向Object.prototype –> 找到sayHi方法 –> 执行()

环环相扣,是不是像链条一样呢?是的,这个就是我们所说的 原型链
再看看下面的示例:
这里写图片描述
原型链的最后是 null
如果还没晕,恭喜你似乎领悟到了某些人生的哲学:
《易经》– ‘太极生两仪,两仪生四象,四象生八卦’
《道德经》– ‘无,名天地之始’
是不是很熟悉,是不是很意外!

OK,下面我们来了解了解继承
继承有多种实现方式,各种方式各种资料上的称呼也不一样,怎么叫无所谓,你高兴就好…

// demo1 对象冒充继承
function Cat(n,c){ // 猫 类
    this.name=n;
    this.color=c;
    this.trait=function (){
        console.log('卖萌~');
    }
}
Cat.prototype.skill=function (){// 原型上的属性方法
    console.log('抓老鼠');
}
// 需求:狗要卖萌,狗要多管闲事-抓老鼠
function Dog(n,c,f){ // 狗 类
    this.food=f;
    Cat.call(this,n,c); // 用狗来访问猫的属性方法
}
var dog1=new Dog('二哈','yellow','shi');// 实例对象
console.log(dog1.name); // 二哈
dog1.trait(); // 卖萌
dog1.skill(); // 报错 dog1.skill is not a function

我们看到这种继承方式有局限性,父类原型上的属性方法无法继承,所以二哈没有抓老鼠的技能

// demo2 原型链继承
function Cat(n,c){ // 猫 类
    this.name=n;
    this.color=c;
    this.trait=function (){
        console.log('卖萌~');
    }
}
Cat.prototype.skill=function (){// 原型上的属性方法
    console.log('抓老鼠');
}
function Dog(n,c,f){ // 狗 类
    this.food=f;
}

Dog.prototype=new Cat(); // 在狗的原型对象上添加猫的实例

var dog1=new Dog('二哈','yellow','shi');
console.log(dog1.name); // undefined
console.log(dog1.food); // shi
dog1.trait(); // 卖萌~
dog1.skill(); // 抓老鼠
console.log(dog1.constructor); // Cat

问题一:
实例化对象的时候不能给父类传参,导致访问dog1.name没有值

问题二:
有句台词:‘人是人妈生的,妖是妖妈生的 ’
现在 dog1.constructor 指向 Cat,意味着 二哈 是猫妈生的!很显然这不合理也不环保…

// demo3 对象冒充继承与原型链继承结合
function Cat(n,c){
    this.name=n;
    this.color=c;
    this.trait=function (){
        console.log('卖萌~');
    }
}
Cat.prototype.skill=function (){
    console.log('抓老鼠');
}

function Dog(n,c,f){
    this.food=f;
    Cat.call(this,n,c);// 对象冒充继承
}

Dog.prototype=new Cat();// 原型链继承

Dog.prototype.constructor=Dog;// 指正构造器

var dog1=new Dog('二哈','yellow','shi');
console.log(dog1.name);// 二哈
console.log(dog1.food);// shi
dog1.trait();// 卖萌~
dog1.skill();// 抓老鼠
console.log(dog1.constructor);// Dog

两种方式结合可以实现相对比较完美的继承
别忘了指正构造器,不能认贼作父!

好了,简单总结一下吧
原型、原型链其实真不难理解,涉及的属性方法也不多
当你理解了这些,你会发现闭包应用和回调地狱比原型、原型链更难…
如何检验你是否真理解对象、原型、原型链、继承?
我有个方法:去看看JQ源码,会看到很多关于prototype、constructor的一些操作

最后,送上一张自己画的对象关系图:
这里写图片描述
不知道自己有没有被绕进去,如有不准确的地方,希望指出交流。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值