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




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


忘记在哪里看到过,有人说鉴别一个人是否 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',
   age22,
   job'developer',
   introducefunction(){
       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);  //true
console.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',
   age22,
   job'developer',
   introducefunction(){
       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 的时候,会遍历到原型对象里的可枚举属性。


function Person(name, age, job){
   this.name = name;
   this.age = age;
   this.job = job;
}

Person.prototype = {
   introducefunction(){
       console.log('My name is ' + this.name + ', I\'m ' + this.age + ' years old.');
   },
   friends: ['person0''person1''person2']
};

Object.defineProperty(Person.prototype, 'constructor', {
   enumerablefalse,
   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 继承的内容。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/304656.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

面向对象编程设计模式--简单工厂模式讲解(历史上最简单明白的例子)

工作之余,在看资料过程中发现一个极易理解的简单工厂模式的例子,自己亲自试练一番,感觉对这个设计模式不熟悉的朋友,一看马上就知道是什么回事了。 简单工厂模式根据提供给它的数据,返回几个可能类中的一个类的实例。通常它返的类…

.NET 6 Preview 1 开箱,带你体验新版本

最近 .NET 6 Preview 1 发布了,.NET 统一是此版本的核心。大家可以读一下原文博客:https://devblogs.microsoft.com/dotnet/announcing-net-6-preview-1/.NET 6.0 SDK 和 Runtime 下载地址:https://dotnet.microsoft.com/download/dotnet/6.0…

redis 清空缓存_「镜头回放」简直了!spring中清除redis缓存导致应用挂死

异常场景springWeb应用一直运行正常,同事最近反应,每次版本更新完毕,刷新缓存,就会导致应用挂死。只有重启redis应用才恢复正常。项目概况springWeb项目,常用配置表做了redis缓存,配置表中只有少量数据&…

25岁社招进阿里,从电商到有赞新零售,他仅1年就打开了马云一直想做的新领域!

最近关于「新零售」的声音此起彼伏:阿里巨资收购高鑫零售,腾讯确认入股永辉超市……自2016年10月马云第一次提出了「新零售」概念之后,各巨头跑马圈地,线下成为了必争之地,新零售的蓝海才刚刚打开。而李星,…

C#连接MySQL数据库实例

项目目的:连接mysql查询数据并将数据显示到界面的datagridview里面.Step1:添加动态链接库文件Visual Studio,在 项目(右键)-管理NuGet程序包(N) 然后在浏览里面搜索MySql.Data并进行安装。Step2:using所需要的库using MySql.Data.MySqlClient;step3&…

链接服务器 慢_redis服务器cpu100%的原因和解决方案

首先引起cpu100%可能的几大原因:1.redis连接数过高2.数据持久化导致的阻塞3.主从存在频繁全量同步4.value值过大5.redis慢查询为了模拟redis服务器cpu100%,临时买了一台阿里云ecs,并把那天清空前的redis备份还原到服务器上。下面我们按照顺序…

有人问我:AI这么火,要不要去追赶AI的热潮?

12月14日,吴恩达发布微博:我很高兴地宣布Landing.ai的成立,开始进入AI产业。 作为一家人工智能(AI)公司,Landing.ai旨在帮助企业在人工智能时代实现转型。这一动作让人们对AI的关注度在2017年的末尾又一次升…

过完年,又不想上班了?

大家新年好,我是Z哥。每次过完年,不少人会多增加一份焦虑,这份焦虑表面上看是“不想上班”,但实际上可能是职业规划的缺失导致。因为缺少对未来的预期、憧憬,导致对自己的工作没有热情。这种情况的另一种叫法是「职业倦…

15天助你掌握问卷统计与Spss实战

最近总是有小伙伴咨询超模君关于数据分析的问题,比如数据分析是什么、怎样才能速成数据分析等等,今天超模君就抽个空跟大家谈谈数据分析那些事儿。首先,我们先来了解了解小伙伴所提到的数据分析。数据分析是指用适当的统计分析方法对收集来的…

aswing学习笔记3-在JPanel中,如何将.png格式的图片设置为背景?

在JPanel中,如何将.png格式的图片设置为背景?2009-03-04 19:21在JPanel中,如何将.png格式的图片设置为背景? 发表于 : 周三 6月 04, 2008 3:53 pm由 tvrcaiyy在JPanel中,如何将.png格式的图片设置为背景?能…

在.NET Core 中使用 FluentValidation 进行规则验证

不用说,规则验证很重要,无效的参数,可能会导致程序的异常。如果使用Web API或MVC页面,那么可能习惯了自带的规则验证,我们的控制器很干净:public class User {[Required]public string FirstName { get; se…

scrcpy投屏_安卓投屏利器——PC一键控制多台手机

点击关注,我们共同每天进步一点点!之前给大家介绍了投屏开源工具scrcpy(Scrcpy投屏,在电脑上流畅操控你的手机!),今天要介绍的投屏工具是在scrcpy的基础上进行了二次开发,使用更加友好。《安卓投屏》基于Gi…

【干货】通俗理解神经网络中激活函数作用

推荐阅读时间8min~13min主要内容:通俗理解激活函数,主要来自我在学习会的slides,讲解了激活函数的非线性能力和组合特征的作用下面我分别对激活函数的两个作用进行解释。1加入非线性因素,解决非线性问题好吧,很容易能够…

创建第一个WCF程序

WCF的三大核心是ABC A代表Address-where(对象在哪里) B代表Binding-how(通过什么协议取得对象) C代表Contact(契约)-what(定义的对象是什么,如何操纵) 创建一个空的解决方…

.NET微服务最佳实践eShopOnContainers

本文翻译自微软Docs, 内嵌译者多年使用的参悟,如理解有误,请不吝赐教。微软与社区专家合作,开发了功能齐全的云原生微服务示例应用eShopOnContainers。该应用旨在展示使用.NET、Docker以及可选的Azure,Kubernetes技术来…

正态分布为什么常见?

统计学里面,正态分布(normal distribution)最常见。男女身高、寿命、血压、考试成绩、测量误差等等,都属于正态分布。>>>> 作者: 阮一峰以前,我认为中间状态是事物的常态,过高和过低…

Python 写各大聊天系统的屏蔽脏话功能原理

来源:Cookie-Fei www.cnblogs.com/cookie1026/p/6121363.html突然想到一个视频里面弹幕被和谐的一满屏的*号觉得很有趣,然后就想用python来试试写写看,结果还真玩出了点效果,思路是首先你得有一个脏话存放的仓库好到时候检测&…

RHEL4- SAMBA服务(四)在x-window下图形界面简单搭建samba服务器

RHEL4- SAMBA服务(四)在x-window下图形界面简单搭建samba服务器在《RHEL4- SAMBA服务(一)samba服务的安装与启动》中我讲了如何安装和启动samba服务器,这一篇我来说一说如何使用图形界面配置简单的samba服务&#xff0…

mvc identity连接mysql_asp.net MVC5,如何使用mysql数据库,使用MVC框架中自带的identity用户验证体系...

问题如题现在很多时候,即使是.net项目,我们用的数据库也未必时是 SQL Server了。但很多VS 自带的框架(如MVC 、WebAPI等框架)中的示例自带的仍然是默认Sql Server的,而且并不一定好改成别的数据库。这有时候就很苦恼。比如.net MVC框架中自带…

Dapr 知多少 | 分布式应用运行时

IntroDapr 官方团队已于最近(2021.2.17)正式发布Dapr v1.0,Dapr已正式生产可用,可以部署到自托管环境或 Kubernetes 集群。对于绝大多数开发者来说,想必对Dapr只是有所耳闻,而具体是什么(What&a…