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

2017 年 11 月 24 日 程序人生 爱吹牛逼的王小白



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


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


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 继承的内容。


如果文章里有错误请指正,我会及时更正,谢谢观看。


看完此文,你有什么想法或观点呢?欢迎在留言区留言评论。


点击图片get更多精彩

为什么不能和程序媛谈恋爱?


代码传奇 | 为了玩游戏,Dennis Ritchie创造了UNIX,据说就连乔布斯也甘拜下风


90后超模!一位程序员界的维密天使,维密天使中的扛把子!


登录查看更多
0

相关内容

一个具体事物,总是有许许多多的性质与关系,我们把一个事物的性质与关系,都叫作事物的属性。 事物与属性是不可分的,事物都是有属性的事物,属性也都是事物的属性。 一个事物与另一个事物的相同或相异,也就是一个事物的属性与另一事物的属性的相同或相异。 由于事物属性的相同或相异,客观世界中就形成了许多不同的事物类。具有相同属性的事物就形成一类,具有不同属性的事物就分别地形成不同的类。
【2020新书】使用高级C# 提升你的编程技能,412页pdf
专知会员服务
60+阅读 · 2020年6月26日
Python导论,476页pdf,现代Python计算
专知会员服务
266+阅读 · 2020年5月17日
【实用书】Python爬虫Web抓取数据,第二版,306页pdf
专知会员服务
122+阅读 · 2020年5月10日
《代码整洁之道》:5大基本要点
专知会员服务
50+阅读 · 2020年3月3日
TensorFlow Lite指南实战《TensorFlow Lite A primer》,附48页PPT
专知会员服务
70+阅读 · 2020年1月17日
【电子书】C++ Primer Plus 第6版,附PDF
专知会员服务
88+阅读 · 2019年11月25日
一文看懂怎么用 Python 做数据分析
大数据技术
24+阅读 · 2019年5月5日
你真的会正确地调试 TensorFlow 代码吗?
数据库开发
7+阅读 · 2019年3月18日
如何编写完美的 Python 命令行程序?
CSDN
5+阅读 · 2019年1月19日
Python为啥这么牛?
Python程序员
3+阅读 · 2018年3月30日
学习 | Word2Vec的迁移实践:Tag2Vec
菜鸟的机器学习
5+阅读 · 2018年3月29日
python数据分析师面试题选
数据挖掘入门与实战
6+阅读 · 2017年11月21日
人工神经网络是否模拟了人类大脑?
数说工作室
10+阅读 · 2017年7月19日
代码这样写不止于优雅(Python版)
数说工作室
4+阅读 · 2017年7月17日
人工神经网络
平均机器
15+阅读 · 2017年7月17日
Arxiv
5+阅读 · 2018年10月23日
Learning Recommender Systems from Multi-Behavior Data
Arxiv
19+阅读 · 2018年5月17日
Arxiv
6+阅读 · 2018年4月24日
Arxiv
7+阅读 · 2018年3月19日
Arxiv
6+阅读 · 2018年2月26日
Arxiv
10+阅读 · 2017年12月29日
VIP会员
最新内容
俄罗斯如何拦截乌克兰无人机与巡航导弹
专知会员服务
8+阅读 · 10月15日
《武器-目标分配问题:精确与近似求解算法》
专知会员服务
11+阅读 · 10月15日
综述|谁来改进机器人?迈向物理递归自我改进
专知会员服务
10+阅读 · 10月14日
软件定义无人机蜂群给防空带来全新压力
专知会员服务
10+阅读 · 10月13日
《半自主无人地面战车的互操作性》66页
专知会员服务
14+阅读 · 10月13日
相关VIP内容
【2020新书】使用高级C# 提升你的编程技能,412页pdf
专知会员服务
60+阅读 · 2020年6月26日
Python导论,476页pdf,现代Python计算
专知会员服务
266+阅读 · 2020年5月17日
【实用书】Python爬虫Web抓取数据,第二版,306页pdf
专知会员服务
122+阅读 · 2020年5月10日
《代码整洁之道》:5大基本要点
专知会员服务
50+阅读 · 2020年3月3日
TensorFlow Lite指南实战《TensorFlow Lite A primer》,附48页PPT
专知会员服务
70+阅读 · 2020年1月17日
【电子书】C++ Primer Plus 第6版,附PDF
专知会员服务
88+阅读 · 2019年11月25日
相关资讯
一文看懂怎么用 Python 做数据分析
大数据技术
24+阅读 · 2019年5月5日
你真的会正确地调试 TensorFlow 代码吗?
数据库开发
7+阅读 · 2019年3月18日
如何编写完美的 Python 命令行程序?
CSDN
5+阅读 · 2019年1月19日
Python为啥这么牛?
Python程序员
3+阅读 · 2018年3月30日
学习 | Word2Vec的迁移实践:Tag2Vec
菜鸟的机器学习
5+阅读 · 2018年3月29日
python数据分析师面试题选
数据挖掘入门与实战
6+阅读 · 2017年11月21日
人工神经网络是否模拟了人类大脑?
数说工作室
10+阅读 · 2017年7月19日
代码这样写不止于优雅(Python版)
数说工作室
4+阅读 · 2017年7月17日
人工神经网络
平均机器
15+阅读 · 2017年7月17日
相关论文
Arxiv
5+阅读 · 2018年10月23日
Learning Recommender Systems from Multi-Behavior Data
Arxiv
19+阅读 · 2018年5月17日
Arxiv
6+阅读 · 2018年4月24日
Arxiv
7+阅读 · 2018年3月19日
Arxiv
6+阅读 · 2018年2月26日
Arxiv
10+阅读 · 2017年12月29日
Top
微信扫码咨询专知VIP会员