鉴别一个人是否 js 入门的标准是对 js 原型的理解

姓名:房小慧

学号:17101223361

专业:软件工程

【嵌牛导读】:学Web前端的人都会用到JavaScript,本文是对js原型的一个简单总结,希望能够帮到大家

【嵌牛鼻子】:js原型

【嵌牛提问】:js原型是什么?

【嵌牛正文】:

对象

JavaScript 是一种基于对象的编程语言,但它与一般面向对象的编程语言不同,因为他没有类(class)的概念。

对象是什么?ECMA-262 把对象定义为:「无序属性的集合,其属性可以包含基本值、对象或者函数。」简单来说,对象就是一系列的键值对(key-value),我习惯把键值对分为两种,属性(property)和方法(method)。

面向对象编程,在我的理解里是一种编程思想。这种思想的核心就是把万物都抽象成一个个对象,它并不在乎数据的类型以及内容,它在乎的是某个或者某种数据能够做什么,并且把数据和数据的行为封装在一起,构建出一个对象,而程序世界就是由这样的一个个对象构成。而类是一种设计模式,用来更好地创建对象。

举个例子,把我自己封装成一个简单的对象,这个对象拥有我的一些属性和方法。

//构造函数创建

varklaus =newObject();

klaus.name ='Klaus';

klaus.age =22;

klaus.job ='developer';

klaus.introduce =function(){

console.log('My name is '+this.name +', I\'m '+this.age +' years old.');

};

//字面量语法创建,与上面效果相同

varklaus = {

name:'Klaus',

age:22,

job:'developer',

introduce:function(){

console.log('My name is '+this.name +', I\'m '+this.age +' years old.');

}

};

这个对象中,name、age 和 job 是数据部分,introduce 是数据行为部分,把这些东西都封装在一起就构成了一个完整的对象。这种思想不在乎数据(name、age 和 job)是什么,它只在乎这些数据能做什么(introduce),并且把它们封装在了一起(klaus 对象)。

跑一下题,与面向对象编程相对应的编程思想是面向过程编程,它把数据和数据行为分离,分别封装成数据库和方法库。方法用来操作数据,根据输入的不同返回不同的结果,并且不会对输入数据之外的内容产生影响。与之相对应的设计模式就是函数式编程。

工厂模式创建对象

如果创建一个简单的对象,像上面用到的两种方法就已经够了。但是如果想要创建一系列相似的对象,这种方法就太过麻烦了。所以,就顺势产生了工厂模式。

functioncreatePerson(name, age, job){

varo =newObject();

o.name = name;

o.age = age;

o.job = job;

o.introduce =function(){

console.log('My name is '+this.name +', I\'m '+this.age +' years old.');

};

returno;

}

varklaus = createPerson('Klaus',22,'developer');

随着 JavaScript 的发展,这种模式渐渐被更简洁的构造函数模式取代了。(高程三中提到工厂模式无法解决对象识别问题,我觉得完全可以加一个_type 属性来标记对象类型)

构造函数模式创建对象

我们可以通过创建自定义的构造函数,然后利用构造函数来创建相似的对象。

functionPerson(name, age, job){

this.name = name;

this.age = age;

this.job = job;

this.introduce =function(){

console.log('My name is '+this.name +', I\'m '+this.age +' years old.');

};

}

varklaus =newPerson('Klaus',22,'developer');

console.log(klausinstanceofPerson);//true

console.log(klausinstanceofObject);//true

现在我们来看一下构造函数模式与工厂模式对比有什么不同:

函数名首字母大写:这只是一种约定,写小写也完全没问题,但是为了区别构造函数和一般函数,默认构造函数首字母都是大写。

不需要创建对象,函数最后也不需要返回创建的对象:new 操作符帮你创建对象并返回。

添加属性和方法的时候用 this:new 操作符帮你把 this 指向创建的对象。

创建的时候需要用 new 操作符来调用构造函数。

可以获取原型上的属性和方法。(下面会说)

可以用 instanceof 判断创建出的对象的类型。

new

这么看来,构造函数模式的精髓就在于这个 new 操作符上,所以这个 new 到底做了些什么呢?

创建一个空对象。

在这个空对象上调用构造函数。(所以 this 指向这个空对象)

将创建对象的内部属性__proto__指向构造函数的原型(原型,后面讲到原型会解释)。

检测调用构造函数后的返回值,如果返回值为对象(不包括 null)则 new 返回该对象,否则返回这个新创建的对象。

用代码来模仿大概是这样的:

function_new(fn){

returnfunction(){

varo =newObject();

varresult = fn.apply(o,arguments);

o.__proto__ = fn.prototype;

if(result && (typeofresult ==='object'||typeofresult ==='function')){

returnresult;

}else{

returno;

}

}

}

varklaus = _new(Person)('Klaus',22,'developer');

组合使用构造函数模式和原型模式

构造函数虽然很好,但是他有一个问题,那就是创建出的每个实例对象里的方法都是一个独立的函数,哪怕他们的内容完全相同,这就违背了函数的复用原则,而且不能统一修改已创建实例对象里的方法,所以,原型模式应运而生。

functionPerson(name, age, job){

this.name = name;

this.age = age;

this.job = job;

this.introduce =function(){

console.log('My name is '+this.name +', I\'m '+this.age +' years old.');

};

}

varklaus1 =newPerson('Klaus',22,'developer');

varklaus2 =newPerson('Klaus',22,'developer');

console.log(klaus1.introduce === klaus2.introduce);//false

什么是原型?我们每创建一个函数,他就会自带一个原型对象,这个原型对象你可以理解为函数的一个属性(函数也是对象),这个属性的 key 为 prototype,所以你可以通过 fn.prototype 来访问它。这个原型对象除了自带一个不可枚举的指向函数本身的 constructor 属性外,和其他空对象并无不同。

那这个原型对象到底有什么用呢?我们知道构造函数也是一个函数,既然是函数那它也就有自己的原型对象,既然是对象你也就可以给它添加一些属性和方法,而这个原型对象是被该构造函数所有实例所共享的,所以你就可以把这个原型对象当做一个共享仓库。下面来说说他具体是如何共享的。

上面讲 new 操作符的时候讲过有一步,将创建对象的内部属性__proto__指向构造函数的原型,这一步才是原型共享的关键。这样你就可以在新建的实例对象里访问构造函数原型对象里的数据。

functionPerson(name, age, job){

this.name = name;

this.age = age;

this.job = job;

this.introduce =this.__proto__.introduce;//这句可以省略,后面会介绍

}

Person.prototype.introduce =function(){

console.log('My name is '+this.name +', I\'m '+this.age +' years old.');

};

varklaus1 =newPerson('Klaus',22,'developer');

varklaus2 =newPerson('Klaus',22,'developer');

console.log(klaus1.introduce === klaus2.introduce);//true

这样,我们就达到了函数复用的目的,而且如果你修改了原型对象里的 introduce 函数后,所有实例的 introduce 方法都会同时更新,是不是很方便呢?但是原型绝对不止是为了这么简单的目的所创建的。

我们首先明确一点,当创建一个最简单的对象的时候,其实默认用 new 调用了 JavaScript 内置的 Objcet 构造函数,所以每个对象都是 Object 的一个实例(用 Object.create(null) 等特殊方法创建的暂不讨论)。所以根据上面的介绍,每个对象都有一个__proto__的属性指向 Object.prototype。这是理解下面属性查找机制的前提。

varklaus = {

name:'Klaus',

age:22,

job:'developer',

introduce:function(){

console.log('My name is '+this.name +', I\'m '+this.age +' years old.');

}

};

console.log(klaus.friend);//undefined

console.log(klaus.toString);//ƒ toString() { [native code] }

上面代码可以看出,如果我们访问 klaus 对象上没有定义的属性 friend,结果返回 undefined,这个可以理解。但是同样访问没定义的 toString 方法却返回了一个函数,这是不是很奇怪呢?其实一点不奇怪,这就是 JavaScript 对象的属性查找机制。

属性查找机制:当访问某对象的某个属性的时候,如果存在该属性,则返回该属性的值,如果该对象不存在该属性,则自动查找该对象的__proto__指向的对象的此属性。如果在这个对象上找到此属性,则返回此属性的值,如果__proto__指向的对象也不存在此属性,则继续寻找__proto__指向的对象的__proto__指向的对象的此属性。这样一直查下去,直到找到 Object.prototype 对象,如果还没找到此属性,则返回 undefined。(原型链查找,讲继承时会详细讲)

理解了上面的查找机制以后,也就不难理解 klaus.toString 其实也就是 klaus.__proto__.toString,也就是 Object.prototype.toString,所以就算你没有定义依然也可以拿到一个函数。

理解了这一点以后,也就理解了上面 Person 构造函数里的那一句我为什么注释了可以省略,因为访问实例的 introduce 找不到时会自动找到实例__proto__指向的对象的 introduce,也就是 Person.prototype.introduce。

这也就是原型模式的强大之处,因为你可以在每个实例上访问到构造函数的原型对象上的属性和方法,而且可以实时修改,是不是很方便呢。

除了给原型对象添加属性和方法之外,也可以直接重写原型对象(因为原型对象本质也是一个对象),只是别忘记添加 constructor 属性。

还需要注意一点,如果原型对象共享的某属性是个引用类型值,一个实例修改该属性后,其他实例也会因此受到影响。

以及,如果用 for-in 循环来遍历属性的 key 的时候,会遍历到原型对象里的可枚举属性。

functionPerson(name, age, job){

this.name = name;

this.age = age;

this.job = job;

}

Person.prototype = {

introduce:function(){

console.log('My name is '+this.name +', I\'m '+this.age +' years old.');

},

friends: ['person0','person1','person2']

};

Object.defineProperty(Person.prototype,'constructor', {

enumerable:false,

value: Person

});

varklaus1 =newPerson('Klaus',22,'developer');

varklaus2 =newPerson('Klaus',22,'developer');

console.log(klaus1.friends);//['person0', 'person1', 'person2']

klaus1.friends.push('person3');

console.log(klaus1.friends);//['person0', 'person1', 'person2', 'person3']

console.log(klaus2.friends);//['person0', 'person1', 'person2', 'person3']

for(varkeyinklaus1){

console.log(key);//name, age, job, introduce, friends

}

ES6 class

如果你有关注最新的 ES6 的话,你会发现里面提出了一个关键字 class 的用法,难道 JavaScript 要有自己类的概念了吗?

tan90°,不存在的,这只是一个语法糖而已,上面定义的 Person 构造函数可以用 class 来改写。

classPerson{

constructor(name, age, job){

this.name = name;

this.age = age;

this.job = job;

}

introduce(){

console.log('My name is '+this.name +', I\'m '+this.age +' years old.');

}

}

Person.prototype.friends = ['person0','person1','person2'];

varklaus =newPerson('Klaus',22,'developer');

很遗憾,ES6 明确规定 class 里只能有方法而不能有属性,所以像 friends 这样的属性可能只能在外面单独定义了。

下面简单举几个差异点,如果想详细了解可以去看阮一峰的《ECMAScript 6 入门》或者 Nicholas C. Zakas 的《Understanding ECMAScript 6》。

class 里的静态方法(类似于 introduce)是不可枚举的,而用 prototype 定义的是可枚举的。

class 里面默认使用严格模式。

class 已经不属于普通的函数了,所以不使用 new 调用会报错。

class 不存在变量提升。

class 里的方法可以加 static 关键字定义静态方法,这种静态方法就不是定义在 Person.prototype 上而是直接定义在 Person 上了,只能通过 Person.method() 调用而不会被实例共享。

作用域安全的构造函数

不管是高程还是其他的一些资料都提到过作用域安全的构造函数这个概念,因为构造函数如果不用 new 来调用就只是一个普通的函数而已,这样在函数调用的时候 this 会指向全局(严格模式为 undefined),这样如果错误调用构造函数就会把属性和方法定义在 window 上。为了避免这种情况,可以将构造函数稍加改造,先用 instanceof 检测 this 然后决定调用方法。

functionPerson(name, age, job){

if(thisinstanceofPerson){

this.name = name;

this.age = age;

this.job = job;

}else{

returnnewPerson(name, age, job);

}

}

varklaus1 = Person('Klaus',22,'developer');

varklaus2 =newPerson('Klaus',22,'developer');//两种方法结果一样

不过个人认为这种没什么必要,构造函数已经首字母大写来加以区分了,如果还错误调用的话那也没啥好说的了。。。

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