不知不觉跳入前端「大坑」也已经有大半年了,学到了很多知识。为了让知识更好地沉淀,我打算写一系列的知识总结,希望能在回顾知识的同时也能帮到别的同学。

忘记在哪里看到过,有人说鉴别一个人是否 js 入门的标准就是看他有没有理解 js 原型,所以第一篇总结就从这里出发。

对象

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

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

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

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

//构造函数创建var klaus = new Object();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.');};

//字面量语法创建,与上面效果相同var klaus = {   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 对象)。

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

工厂模式创建对象

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

function createPerson(name, age, job){   var o = new Object();   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.');   };   return o;}

var klaus = createPerson('Klaus', 22, 'developer');

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

构造函数模式创建对象

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

function Person(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.');   };}

var klaus = new Person('Klaus', 22, 'developer');console.log(klaus instanceof Person);  //trueconsole.log(klaus instanceof Object);  //true

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

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

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

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

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

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

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

new

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

  1. 创建一个空对象。

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

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

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

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

function _new(fn){   return function(){       var o = new Object();       var result = fn.apply(o, arguments);       o.__proto__ = fn.prototype;       if(result && (typeof result === 'object' || typeof result === 'function')){           return result;       }else{           return o;       }   }}

var klaus = _new(Person)('Klaus', 22, 'developer');

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

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

function Person(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.');   };}

var klaus1 = new Person('Klaus', 22, 'developer');var klaus2 = new Person('Klaus', 22, 'developer');console.log(klaus1.introduce === klaus2.introduce);  //false

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

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

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

function Person(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.');};

var klaus1 = new Person('Klaus', 22, 'developer');var klaus2 = new Person('Klaus', 22, 'developer');console.log(klaus1.introduce === klaus2.introduce);  //true

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

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

var klaus = {   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);  //undefinedconsole.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 的时候,会遍历到原型对象里的可枚举属性。

function Person(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});

var klaus1 = new Person('Klaus', 22, 'developer');var klaus2 = new Person('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(var key in klaus1){   console.log(key);  //name, age, job, introduce, friends}

ES6 class

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

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

class Person{   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'];

var klaus = new Person('Klaus', 22, 'developer');

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

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

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

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

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

  4. class 不存在变量提升。

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

作用域安全的构造函数

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

function Person(name, age, job){   if(this instanceof Person){       this.name = name;       this.age = age;       this.job = job;   }else{       return new Person(name, age, job);   }}

var klaus1 = Person('Klaus', 22, 'developer');var klaus2 = new Person('Klaus', 22, 'developer');  //两种方法结果一样

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

结语

以上就是我眼中的 JavaScript 原型,可能解释的不够清楚,大家如果还想看更详细的内容可以去看高程三的第六章或者你不知道的 JavaScript(上卷)的第二部分关于原型的内容,下一次我可能会写一些关于 JavaScript 继承的内容。

鉴别一个人是否 js 入门的标准竟然是?!相关推荐

  1. 【Three.js入门】标准网格材质、置换贴图、粗糙度贴图、金属贴图、法线贴图

    个人简介

  2. React.js入门笔记

    # React.js入门笔记 核心提示 这是本人学习react.js的第一篇入门笔记,估计也会是该系列涵盖内容最多的笔记,主要内容来自英文官方文档的快速上手部分和阮一峰博客教程.当然,还有我自己尝试的 ...

  3. 数据可视化js框架 d3.js入门

    数据可视化js框架 d3.js入门 [一] 选择元素绑定数据 1.下载.引入d3.js 2.d3.select():选择所有指定元素的第一个 3.d3.selectAll() :选择指定元素的全部 e ...

  4. D3.js入门基础教程

    什么是"框架" 什么是"应用程序" DOM (文档对象模型(Document Object Model) D3 (1)下载 D3.js 的文件 (2)直接包含网 ...

  5. web前端-JS入门

    web前端-JS入门 1.初识JavaScript 1.1 JavaScript的简单介绍 1.2 JS的三种写法 1.2.1 行内式 1.2.2 内嵌式 1.2.3 外部js 1.3 JS输入输出语 ...

  6. JS基础(三)JS入门

    文章目录 JS入门 JS内部的一些对象 Date对象 JSON对象 操作BOM 操作DOM(超级重点) 定位查看DOM节点 使用document获取节点: 使用DOM节点对象获取DOM节点 修改DOM ...

  7. html5怎么兼容js 插件,Modernizr.js入门指南(HTML5CSS3浏览器兼容插件)

    HTML5 和 CSS3 的快速发展,给我们带来了极大的便利,比如从此再也不用花费大量的时间只是为了设计一个圆角的效果. 但是!我们不能像控制机器一样来控制所有的人都一夜之间升级到现代浏览器,因为那些 ...

  8. Vue.js入门教程(适合初学者)

    Vue.js入门教程 Vue官网网址:Vue.js 中文网 Vue.js Vue.js是渐进式JavaScript 框架,是一套构建用户界面的渐进式框架.也可以说Vue.js 是一个用来构建网页界面的 ...

  9. Node.js 入门教程 6 V8 JavaScript 引擎

    Node.js 入门教程 Node.js官方入门教程 Node.js中文网 本文仅用于学习记录,不存在任何商业用途,如侵删 文章目录 Node.js 入门教程 6 V8 JavaScript 引擎 6 ...

最新文章

  1. .Net Core中利用TPL(任务并行库)构建Pipeline处理Dataflow
  2. windows 下anaconda创建环境慢的解决办法
  3. Qt for Android环境配置
  4. 《底层逻辑》--思维导图
  5. Linux系统瘦身裁剪 续
  6. 自定义vb.net 窗口位置
  7. 关于查询FileNet PE中用户Inbox信息
  8. 朱兰的质量观(转载)
  9. 北大《中文核心期刊要目总览》2020版目录人文、社会科学
  10. spring cloud 微服务调用链
  11. 域名过期后还能续费域名吗?
  12. Tortoise commit提交模板配置
  13. 初创企业如何做高效持续交付
  14. img 图片自适应大小
  15. 论文写作 1: 学术论文的基本概念
  16. 父子组件的生命周期执行顺序
  17. 记一次工信部电子5所的笔试面试记录
  18. Java工程师面试中,有些问题要这么答
  19. 蒲慕明院士:脑机融合技术或许会成为未来人工智能的一个热门方向
  20. 读取单细胞测序数据时出现错误 Error in readMM(file = matrix.loc) : file is not a MatrixMarket file

热门文章

  1. DevExpress作为企业赞助商加入.NET基金会
  2. 30分钟无坑部署K8S单Master集群
  3. 从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)
  4. 手把手教你写DI_3_小白徒手支持 Singleton 和 Scoped 生命周期
  5. 拥抱.NET Core系列:MemoryCache 初识
  6. .NET Core跨平台:使用.NET Core开发一个初心源商城总括
  7. 从抵触到力推,.Net Core的成功让微软正视开源
  8. 使用StyleCop 进行代码评审
  9. 你知道C#中的Lambda表达式的演化过程吗
  10. 微软将降低Visual Studio对操作系统的影响