JavaScript原型和原型链相关知识梳理

一、创建对象有几种方法

请大家尽可能多的找到创建对象的方法,如有补充欢迎在评论区留言讨论。更多参考详见:JavaScript创建对象的7种方法

第一种方式:对象字面量表示法

var o1 = {
    name: 'o1'
};
var o2 = new Object({
    name: 'o2'
});

缺点:字面量对象中默认原型链指向Object,用同一个接口创建很多对象会产生大量冗余代码。

第二种方式:使用显式构造函数

var M = function (name) {
    this.name = name;
};
var o3 = new M('o3');

第三种方式:Object.create

var P = {
    name: 'p'
};
var o4 = Object.create(P);

那么,在控制台输入o1, o2, o3, o4,会发生什么?

  1. o1, o2 输出 Object 对象{ name: 'o1' }{ name: 'o2' }
  2. o3输出 M 对象{ name: 'o3' }
  3. o4是个空对象{}
  4. o1,o2, o3, o4 均有name属性

二、原型系统的“复制操作”的实现思路

浅拷贝

并不是真的去复制一个原型对象,而是使得新对象持有一个原型的引用;

深拷贝

另一个是切实地复制对象,从此两个对象再无关联。

三、理解原型、构造函数、实例、原型链

原型

JavaScript 是基于原型的编程语言的代表,它利用原型来描述对象。JavaScript 并非第一个使用原型的语言,在它之前,self、kevo 等语言已经开始使用原型来描述对象了。 Brendan 最初的构想是将 JavaScript 设计成一个拥有基于原型的面向对象能力的 scheme 语言,而基于原型系统的独特之处是提倡运行时的原型修改

对象可以有两种成员类型:

  • 实例成员:直接存在于对象实例中
  • 原型成员:从对象原型继承

”基于类“和”基于原型“的编程之间的比较

基于类 基于原型
提倡使用一个关注分类和类之间关系的开发模型 提倡编码人员关注一系列对象实例的行为
先有类,再从类去实例化一个对象 将对象划分到最近的使用方式相似的原型对象
类之间的关系:继承、组合等 通过复制的方式创建新对象
往往与语言的类型系统整合,形成一定编译时的能力 一些语言中,复制一个空对象,就是创建一个全新的对象

JavaScript的原型

抛开JavaScript模拟Java类的复杂语法,如 new、Function Object、函数的 prototype 属性等。其原型系统可以用以下两条概括:

  1. 如果所有对象都有私有字段 [[prototype]],就是对象的原型;
  2. 读一个属性,如果对象本身没有,则会继续访问对象的原型,直到原型为空或者找到为止。

判断对象是否包含特定的实例成员hasOwnProperty("成员的名称")
确定对象是否包含特定的属性:使用in操作符(既搜索实例又搜索原型)

ES6中,JavaScript提供能直接访问操纵原型的三个方法

  • Object.create根据指定的原型创建新对象,原型可以是null
  • Object.getPrototypeOf获得一个对象的原型
  • Object.setPrototypeOf设置一个对象的原型

利用以上三个方法,我们可以完全抛开类的思维,利用原型来实现抽象和复用。

var cat = {
    say(){
        console.log("miao~miao~");
    },
    play(){
        console.log("ball")
    },
}
//布偶猫
var Ragdoll = Object.create(cat, {
    say: {
        writable: true,
        configurable: true,
        enumerable: true,
        value: function () {
            console.log("iao~iao~")
        }
    }    
})
var anotherCat = Object.create(cat);
anotherCat.say(); //miao~miao~
var anotherRagdollCat = Object.create(Ragdoll);
Ragdoll.say(); //iao~iao~

构造函数和实例

实例:对象就是一个实例。

构造函数:任何一个函数只要被new操作了,该函数就可以被叫做构造函数。

图解原型链

上图对应关系阐述:

  1. 函数都有prototype属性,指的就是原型对象。(声明一个函数时JS引擎会为这个构造函数自动添加prototype属性,该属性会初始化一个空对象,也就是原型对象。)

  2. 原型对象怎么区分出被哪一个构造函数所引用?

    原型对象中有一个构造器constructor,会默认声明的函数,即通过constructor来确定是被哪一个构造函数引用。

上图工作原理代码演示:

上面的例子中o3是实例,M是构造函数

//构造函数和原型对象的关系
M.prototype.constructor === M //true
//实例和原型对象之间的关系
o3.__proto__ === M.prototype //true

原型链

原型链就是js中数据继承的继承链。在访问一个实例的属性的时候,先在实例本身中找,如果没找到,再从这个实例对象向上找构造这个实例的相关联的对象,还没找到就再往上找,这个相关联的对象又有创造它的上一级的原型对象。以此类推,到Object.prototype终止(整个原型链的顶端),原型链通过prototype__proto__属性进行向上查找。

JavaScript object 猜想图

原型对象和原型链之间起的作用

构造函数中增加很多属性和方法。当有多个实例,想共用一个方法,不能每个实例都拷贝一份(若每个实例都要拷贝一份,会占内存,没有必要),多个实例之间有相同的方法时要考虑存到共同的东西上,这个共同的东西就是原型对象。这就是JS引擎支持的原型链的功能,任何一个实例对象通过原型链找到它上面的原型对象,上面的实例和方法都可以被共享

var M = function(name){this.name = name;};
M.prototype.say = function() {
    console.log('say hi');
}
var o5 = new M('o5');

控制台输出:

o3.say()
say hi
o5.say()
say hi

注意:

  1. 构造函数才会有prototype,对象是没有prototype 的。

  2. 实例对象才有__proto__属性。

    特殊的,函数即是函数也是对象,也有__proto__属性:

    M.__proto__ === Function.prototype //true
    

    其他的:

    Array.__proto__ === Function.prototype //true
    
    let arr = [1,2,3,4] 
    arr.__proto__ === Array.prototype //true
    
    Array.prototype.__proto__ === Object.prototype //true
    
    
    Object.prototype.__proto__ === null //true
    

    参考:JavaScript:构造函数和原型链

四、instanceof 原理

图解instanceof

原理:实例对象的__proto__属性和构造函数没什么关联,其实是引用的原型对象。instanceof用来判断实例对象的属性和构造函数的属性是不是同一个引用

原型对象上可能还会有原型链,用实例对象instanceof判断原型的构造函数,这条原型链上的函数返回都是 true:

o3 instanceof M //true
//只要是原型链上都可以看作instanceof的构造函数
o3 instanceof Object //true

解释:

o3.__proto__=== M.prototype //true
M.prototype.__proto___=== Object.prototype //true
//判断是哪个构造函数直接生成的,比instanceof更严谨
o3.__proto__.constructor === M //true
o3.__proto__.constructor === Object //false

五、new运算符

定义:JavaScript 的 new 运算符创建一个继承于其运算数的原型的新对象,然后调用该运算数,把新创建的对象绑定给this。

按照惯例,打算与 new 结合使用的函数命名应该首字母大写,并谨慎使用 new 。

new运算接收一个构造器和一组调用参数,其工作原理为:

  1. new 后面加上构造函数(构造器),一个新对象被创建,它继承自构造函数原型对象Foo.prototype属性

  2. 将this和调用参数传给构造器:

    构造函数 Foo 被执行的时候,相应的传参会被传入,同时上下文this 会被指定为这个新实例。(new Foo() 在不传递任何参数的时候可以写成 new Foo。)

  3. 如果构造函数返回了一个对象,那么这个对象会取代整个 new 出来的结果。换句话说,如果构造函数没有任何返回对象,那么new出来的结果为第一步创建的新对象;有返回对象,直接返回。

实现一个new运算符效果

var newFunc = function (func) {
    //1.创建空对象,关联指定构造函数原型对象
    var a = Object.create(func.prorotype);
    //2.把上下文转移给b对象
    var b = func.call(a);
    //3.判断执行之后的结果是不是对象类型
    if (typeof b === 'object') {
        return b;
    } else {
        return a;
    }
}

控制台输出:

o6 = newFunc(M)
o6 instanceof M //true
o6 instanceof Object //true
o6.__proto__.constructor === M //true
M.prototype.walk = function() {
    console.log('walk')
}
o6.walk()
walk
o3.walk()
walk

new 这样的行为,试图让函数对象在语法上跟类变得相似,但是,它客观上提供了两种方式,一是在构造器中添加属性,二是在构造器的 prototype 属性上添加属性。

用构造器模拟类的两种方法:

//第一种方法:直接在构造器中修改this,给this添加属性
function Cls1(){
    this.p1 = 1;
    this.p2 = function(){
        console.log(this.p1);
    }
} 
var o1 = new Cls1;
o1.p2();
//第二种方法:修改构造器的 prototype 属性指向的对象,它是从这个构造器构造出来的所有对象的原型。
function Cls2(){
}
Cls2.prototype.p1 = 1;
Cls2.prototype.p2 = function(){
    console.log(this.p1);
}

var o2 = new Cls2;
o2.p2();

没有 Object.create、Object.setPrototypeOf 的早期版本中,new 运算是唯一一个可以指定 [[prototype]] 的方法(当时的 mozilla 提供了私有属性 proto,但是多数环境并不支持),所以,当时已经有人试图用它来代替后来的 Object.create,我们甚至可以用它来实现一个 Object.create 的不完整的 pollyfill,见以下代码:

Object.create = function(prototype){
    var cls = function(){}
    cls.prototype = prototype;
    return new cls;
}

这段代码创建了一个空函数作为类,并把传入的原型挂在了它的 prototype上,最后创建了一个它的实例,根据 new 的行为,这将产生一个以传入的第一个参数为原型的对象。

六、补充问题

为什么o4直接拿不到name属性?

Object.create方法创建的对象是用原型链连接的,当JS引擎查找o4是一个空对象,这个空对象上是没有name属性的,name属性在它的原型对象上,也就是说,Object.create方法是把参数中的对象作为一个新对象的原型对象赋给o4的,o4本身不具备这个属性。

o4.__proto__ === p
true

总结:原型链真的很重要,值得反复理解推敲。

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

推荐阅读更多精彩内容