关于面向对象编程:面向对象关联Association和继承Inheritance的区别

关联(Association)Association 指的是类之间的协作关系,其中一个类与另一个类进行交互,但它们之间并没有父子关系。Association 通常示意一个类须要另一个类的某些服务或数据,或者示意类之间有某种独特的特色或属性。 继承(Inheritance)Inheritance 指的是从父类到子类的属性和办法的传递过程,子类能够继承父类的属性和办法,并能够在此基础上增加本人的属性和办法。Inheritance 次要是为了实现代码的重用,防止反复编写代码。在 Inheritance 中,子类和父类之间存在一种父子关系,子类从父类继承了所有的个性,包含属性和办法。子类能够增加新的个性或办法,也能够笼罩或重载继承的办法。 例子以下通过图书馆管理系统为例来阐明 Association 和 Inheritance 的区别: Association 关系图书馆类 Library 和读者类 Reader 之间存在一种协作关系,每个读者能够在图书馆中借阅、偿还图书。此时 Library 类和 Reader 类之间就存在 Association 关系。 ┌───────────────┐ ┌───────────────┐│ Library │ │ Reader │├───────────────┤ ├───────────────┤│ │◀─────────┤ ││ borrow_book │ │ borrow_book ││ return_book │ │ return_book ││ search_book │ │ ││ │ └───────────────┘└───────────────┘Inheritance 关系在图书馆管理系统中,所有书籍都有一些独特的特色,如书名、作者、出版社等等。因而能够定义一个 Book 类作为所有具体书籍类的父类,每个具体书籍类能够继承 Book 类的属性和办法。此时,具体的书籍类与 Book 类之间存在 Inheritance 关系。 ┌───────────────┐│ Book │├───────────────┤│ title ││ author ││ publisher ││ price ││ │└───────────────┘ ▲ │┌───────────────┐│ Novel │├───────────────┤│ genre ││ language ││ is_best_seller││ ISBN │└───────────────┘ ▲ │┌───────────────┐│ Textbook │├───────────────┤│ subject ││ level ││ edition ││ ISBN │└───────────────┘

February 13, 2023 · 1 min · jiezi

关于面向对象编程:一文讲全了Python-类和对象内容

摘要:这是一个对于 Python 类和对象的全部内容。本文分享自华为云社区《从零开始学python | Python 类和对象—面向对象编程》,原文作者:Yuchuan 。 Python 在沉闷开发人员方面将超过其余语言之后,Python 开发人员的需要只会增长。 Python 遵循面向对象的编程范式。它解决申明 python 类,从它们创建对象并与用户交互。在面向对象的语言中,程序被分成独立的对象,或者你能够说成几个小程序。每个对象代表应用程序的不同局部,它们能够互相通信。 在这个python类博客中,您将按以下程序理解类和对象的各个方面: What is a Python Class?Methods and Attributes in a classWhat are Objects?OOPs Concepts:InheritancePolymorphismAbstraction什么是 Python 类?python 中的类是创立特定对象的蓝图。它使您能够以特定形式构建软件。问题来了,怎么办?类容许咱们以一种易于重用的形式对咱们的数据和函数进行逻辑分组,并在须要时进行构建。思考下图。 在第一张图片 (A) 中,它代表了一个能够被视为Class的房子的蓝图。应用雷同的蓝图,咱们能够创立多个屋宇,这些能够视为Objects。应用类,您能够为您的程序增加一致性,以便以更简洁、更无效的形式应用它们。属性是通过点表示法拜访的数据成员(类变量和实例变量)和办法。 类变量是一个类的所有不同对象/实例共享的变量。实例变量是每个实例惟一的变量。它是在办法外部定义的,并且只属于类的以后实例。办法也称为函数,它们在类中定义并形容对象的行为。当初,让咱们继续前进,看看它在 PyCharm 中是如何工作的。要开始,首先看一下 python 类的语法。 语法: class Class_name:statement-1..statement-N在这里,“ class” 语句创立了一个新的类定义。类的名称紧跟 在python中的关键字“ class”之后,后跟一个冒号。要在 python 中创立一个类,请思考以下示例: class employee: pass #no attributes and methods emp_1=employee() emp_2=employee() #instance variable can be created manually emp_1.first='aayushi' emp_1.last='Johari' emp_1.email='aayushi@edureka.co' emp_1.pay=10000 emp_2.first='test' emp_2.last='abc' emp_2.email='test@company.com' emp_2.pay=10000 print(emp_1.email) print(emp_2.email)输入– ...

June 17, 2021 · 3 min · jiezi

关于面向对象编程:面向对象的Python编程你需要知道这些

摘要:Python 没有像 java 中的“private”这样的拜访说明符。除了强封装外,它反对大多数与“面向对象”编程语言相干的术语。因而它不是齐全面向对象的。本文分享自华为云社区《从零开始学python | 面向对象编程 Python:你须要晓得的所有》,原文作者:Yuchuan 。 面向对象的编程作为一门学科在开发人员中失去了广泛的追寻。Python,一种受欢迎的编程语言,也遵循面向对象的编程范式。它解决为 OOP 概念奠定根底的 Python 类和对象的申明。这篇对于“面向对象的 Python 编程”的文章将带您理解如何申明Python 类、从它们实例化对象以及 OOP 的四种办法。 什么是面向对象编程?(Python 中的 OOP 概念) 面向对象编程是一种应用“对象”的思维来示意数据和办法的计算机编程形式。它也是一种用于创立整洁且可重用的代码而不是冗余代码的办法。程序分为独立的对象或几个小程序。每个独自的对象都代表应用程序的不同局部,它们具备本人的逻辑和数据以在它们外部进行通信。 当初,为了更分明地理解为什么咱们应用 oops 而不是 pop,我在上面列出了不同之处。 面向对象和面向过程编程的区别 这就是差别的全副,继续前进,让咱们理解 Python OOPs Conceots。 什么是 Python OOP 概念?Python 中的次要 OOP(面向对象编程)概念包含类、对象、办法、继承、多态、数据抽象和封装。 这就是差别的全副,让咱们持续理解类和对象。 什么是类和对象?类是对象的汇合,或者您能够说它是定义公共属性和行为的对象的蓝图。当初问题来了,你是怎么做到的? 嗯,它以一种使代码可重用性变得容易的形式对数据进行逻辑分组。我能够给你一个现实生活中的例子——把一个办公室把“员工”设想成一个类,以及与它相干的所有属性,比方“emp_name”、“emp_age”、“emp_salary”、“emp_id”作为Python 中的对象。让咱们从编码的角度来看看如何实例化一个类和一个对象。 类是在“类”关键字下定义的。例子: class class1(): // class 1 is the name of the class留神: Python 不辨别大小写。 对象:对象是类的实例。它是一个具备状态和行为的实体。简而言之,它是一个能够拜访数据的类的实例。 语法: obj = class1() 这里 obj 是 class1 的“对象”。 在 python 中创建对象和类:例子: class employee(): def __init__(self,name,age,id,salary): //creating a function self.name = name // self is an instance of a class self.age = age self.salary = salary self.id = id emp1 = employee("harshit",22,1000,1234) //creating objectsemp2 = employee("arjun",23,2000,2234)print(emp1.__dict__)//Prints dictionary阐明: 'emp1' 和 'emp2' 是针对类 'employee' 实例化的对象。这里,单词 (__dict__) 是一个“字典”,它依据给定的参数(姓名、年龄、薪水)打印对象 'emp1' 的所有值。(__init__) 就像一个构造函数,无论何时创建对象都会调用它。 ...

June 2, 2021 · 3 min · jiezi

浅析面向过程与面向对象

在面试时经常会被问到面向过程和面向对象有什么区别,虽然都是编程的一种思想,但是他们的侧重点不同,我们从以下几个方面进行简单总结。历史面向过程的编程语言有汇编语言、C语言。C语言,是1972年贝尔实验室的 D.M.Ritchie 在B语言的基础上设计出的一种新的语言。他们的特点就是太底层了,当你在使用面向过程的编程语言编写代码的时候,你就需要把思维转换成机器的思维,时刻要考虑开辟多大的内存,存储多大的数据,数据在使用完毕的后什么时间释放,这样写代码学习成本太高,开发效率低,不适合编程的推广与应用。 所以渐渐的就发展出来了更友好地面向对象编程语言,面向对象编程思想是很早就提出来早在1967年的时候,在挪威计算中心的Kisten Nygaard和Ole Johan Dahl开发了Simula67语言,它提供了比子程序更高一级的抽象和封装,引入了数据抽象和类的概念,这种语言被认为是第一个面向对象语言。在20世纪80年代初期美国AT&T贝尔实验室的本贾尼.斯特劳斯特卢普(Bjarne Stroustrup)博士发明并实现了C++(最初这种语言被称作“C with Classes”)。一开始C++是作为C语言的增强版出现的,从给C语言增加类开始,不断的增加新特性。目前主流的面向对象编程语言有:Java、C++、Object-C、 JavaScript、Python、Go等,降低了学习成本,易于推广,极大的激发了大家的学习热情, 可以让人们更加专注于如何使用编程语言实现想要的功能。 面向对象(Object Oriented,OO)是软件开发方法。面向对象的概念和应用已超越了程序设计和软件开发,扩展到如数据库系统、交互式界面、应用结构、应用平台、分布式系统、网络管理结构、CAD技术、人工智能等领域。面向对象是一种对现实世界理解和抽象的方法,是计算机编程技术发展到一定阶段后的产物,是一种高级的编程思想。 对应于软件开发的过程,面向对象OO衍生出3个概念:OOA、OOD和OOP。采用面向对象进行分析的方式称为OOA,采用面向对象进行设计的方式称为OOD,采用面向对象进行编码的方式称为OOP。面向过程(OP)和面向对象(OO)本质的区别在于分析方式的不同,最终导致了编码方式的不同。 编程思想案例:有一辆车,时速100km/h,行驶在长1000km的跨海大桥上,求多久能跑完? 面向过程编程思想:只关心数学逻辑。 var hours = 1000 / 100; alert(hours);//10面向对象编程思想:将生活逻辑映射到我们的程序里。 找出题目实体,抽象成对象的概念。分析实体属性和功能,给对象赋一些属性和方法。让实体相互作用得出结果,让每个对象去执行自己的方法。 var car = { speed: 100, run:function(road){ return road.length / this.speed; } } var kuahaidaqiao = { length:1000 } var hours = car.run(kuahaidaqiao); alert(hours);//10面向过程(Procedure Oriented):看名字它是注重过程的。当解决一个问题的时候,面向过程会把事情拆分成: 一个个函数和数据(用于方法的参数)。然后按照一定的顺序,执行完这些方法(每个方法看作一个个过程),等方法执行完了,事情就搞定了。 面向对象(Object Oriented):看名字它是注重对象的。当解决一个问题的时候,面向对象会把事物抽象成对象的概念,就是说这个问题里面有哪些对象,然后给对象赋一些属性和方法,然后让每个对象去执行自己的方法,问题得到解决。 语法JavaScript是一种基于对象的语言,但是它又不是一种真正的面向对象编程语言,因为它没有类(class)。类是具有一类相同特征事物的抽象概念。 在JS中一切皆对象,对象是具体的某一个实例,唯一的个体。在ECMA6语法中中新增了类这个概念。 对象的概念 在面向对象的编程思想中就是以属性和行为的方式去分析同一类事物,将其共有特性和行为的抽象出来,并封闭起来对待,而且我们封闭的同一类事物的属性和行为是互相关联的,有着内在的联系。【注】对象既能存储属性又能存储函数。 【注】我们声明的变量和函数对比对象的属性和方法,使用方式相同,唯一的区别就是使用对象的属性和方法时前面需要加对象名称,变量是自由的,属性是属于对象的。 声明对象的三种方式 var obj = new Object(); //通过new对象 var obj = Object(); //声明new var obj = {}; //直接通过对象常量声明给对象添加属性 ...

September 10, 2019 · 1 min · jiezi

学会了面向对象还怕没有对象

面向对象是一种编程思想,我们通过类(构造函数)和对象实现的面向对象编程,满足下述三个特定:封装、继承和多态。封装封装创建对象的函数封装即把实现一个功能的代码封装到一个函数中,以后实现这个功能,只需要执行该函数即可。实现低耦合,高内聚。 现在我们把属性和方法封装成一个对象: //创建一个对象 var person = new Object(); //添加属性和方法 person.name = "钢铁侠"; person.sex = "男"; person.showName = function(){ alert("我的名字叫" + this.name);//我的名字叫钢铁侠 } person.showSex = function(){ alert("我的性别是" + this.sex);//我的性别是男 } person.showName(); person.showSex();如果我们想创建一个不同性别不同姓名的对象,就需要再写一遍上述代码: //创建一个对象 var person2 = new Object(); //添加属性和方法 person2.name = "猩红女巫"; person2.sex = "女"; person2.showName = function(){ alert("我的名字叫" + this.name);//我的名字叫猩红女巫 } person2.showSex = function(){ alert("我的性别是" + this.sex);//我的性别是女 } person2.showName(); person2.showSex();如果我们想要创建多个对象的话,写起来就非常麻烦,所以要去封装创建对象的函数解决代码重复的问题。 function createPerson(name, sex){ var person = new Object(); person.name = name; person.sex = sex; person.showName = function(){ alert("我叫" + this.name); } person.showSex = function(){ alert("我是" + this.sex + "的"); } return person; }然后生成实例对象,就等于是在调用函数: ...

September 10, 2019 · 4 min · jiezi

10JavaScript-面向对象高级继承模式

JavaScript 面向对象高级——继承模式一、原型链继承方式1: 原型链继承 (1)流程: 1、定义父类型构造函数。 2、给父类型的原型添加方法。 3、定义子类型的构造函数。 4、创建父类型的对象赋值给子类型的原型。 5、将子类型原型的构造属性设置为子类型。 6、给子类型原型添加方法。 7、创建子类型的对象: 可以调用父类型的方法。 (2)关键: 子类型的原型为父类型的一个实例对象// 1.定义父类型构造函数function Supper() { this.supProp = 'Supper property'}// 2.给父类型的原型添加方法Supper.prototype.showSupperProp = function () { console.log(this.supProp)}// 3.定义子类型的构造函数function Sub() { this.subProp = 'Sub property'}// 4.子类型的原型为父类型的一个实例对象Sub.prototype = new Supper()// 5.将子类型原型的构造属性constructor指向子类型Sub.prototype.constructor = Sub// 6.给子类型原型添加方法Sub.prototype.showSubProp = function () { console.log(this.subProp)}// 7.创建子类型的对象,可以调用父类型的方法var sub = new Sub()sub.showSupperProp() // Supper propertysub.showSubProp() // Sub propertyconsole.log(sub) // Sub ...

June 27, 2019 · 1 min · jiezi

一文读懂架构整洁之道附知识脉络图

程序的世界飞速发展,今天所掌握的技能可能明年就过时了,但有一些东西是历久弥新,永远不变的,掌握了这些,在程序的海洋里就不会迷路,架构思想就是这样一种东西。 本文是《架构整洁之道》的读书笔记,文章从软件系统的价值出发,认识架构工作的价值和目标, 依次了解架构设计的基础、指导思想(设计原则)、组件拆分的方法和粒度、组件之间依赖设计、组件边界多种解耦方式以及取舍、降低组件之间通信成本的方法,从而在做出正确的架构决策和架构设计方面,给出作者自己的解读。 阿里巴巴中间件微信公众号对话框,发送“架构”,可获取《架构整洁之道》知识脉络图。直接访问,点击这里。 一、软件系统的价值架构是软件系统的一部分,所以要明白架构的价值,首先要明确软件系统的价值。软件系统的价值有两方面,行为价值和架构价值。 行为价值是软件的核心价值,包括需求的实现,以及可用性保障(功能性 bug 、性能、稳定性)。这几乎占据了我们90%的工作内容,支撑业务先赢是我们工程师的首要责任。如果业务是明确的、稳定的,架构的价值就可以忽略不计,但业务通常是不明确的、飞速发展的,这时架构就无比重要,因为架构的价值就是让我们的软件(Software)更软(Soft)。可以从两方面理解: 当需求变更时,所需的软件变更必须简单方便。变更实施的难度应该和变更的范畴(scope)成等比,而与变更的具体形状(shape)无关。当我们只关注行为价值,不关注架构价值时,会发生什么事情?这是书中记录的一个真实案例,随着版本迭代,工程师团队的规模持续增长,但总代码行数却趋于稳定,相对应的,每行代码的变更成本升高、工程师的生产效率降低。从老板的视角,就是公司的成本增长迅猛,如果营收跟不上就要开始赔钱啦。 可见架构价值重要性,接下来从著名的紧急重要矩阵出发,看我们如何处理好行为价值和架构价值的关系。 重要紧急矩阵中,做事的顺序是这样的:1.重要且紧急 > 2.重要不紧急 > 3.不重要但紧急 > 4.不重要且不紧急。实现行为价值的需求通常是 PD 提出的,都比较紧急,但并不总是特别重要;架构价值的工作内容,通常是开发同学提出的,都很重要但基本不是很紧急,短期内不做也死不了。所以行为价值的事情落在1和3(重要且紧急、不重要但紧急),而架构价值落在2(重要不紧急)。我们开发同学,在低头敲代码之前,一定要把杂糅在一起的1和3分开,把我们架构工作插进去。 二、架构工作的目标前面讲解了架构价值,追求架构价值就是架构工作的目标,说白了,就是用最少的人力成本满足构建和维护该系统的需求,再细致一些,就是支撑软件系统的全生命周期,让系统便于理解、易于修改、方便维护、轻松部署。对于生命周期里的每个环节,优秀的架构都有不同的追求: 开发阶段:组件不要使用大量复杂的脚手架;不同团队负责不同的组件,避免不必要的协作。部署阶段:部署工作不要依赖成堆的脚本和配置文件;组件越多部署工作越繁重,而部署工作本身是没有价值的,做的越少越好,所以要减少组件数量。运行阶段:架构设计要考虑到不同的吞吐量、不同的响应时长要求;架构应起到揭示系统运行的作用:用例、功能、行为设置应该都是对开发者可见的一级实体,以类、函数或模块的形式占据明显位置,命名能清晰地描述对应的功能。维护阶段:减少探秘成本和风险。探秘成本是对现有软件系统的挖掘工作,确定新功能或修复问题的最佳位置和方式。风险是做改动时,可能衍生出新的问题。三、编程范式其实所谓架构就是限制,限制源码放在哪里、限制依赖、限制通信的方式,但这些限制比较上层。编程范式是最基础的限制,它限制我们的控制流和数据流:结构化编程限制了控制权的直接转移,面向对象编程限制了控制权的间接转移,函数式编程限制了赋值,相信你看到这里一定一脸懵逼,啥叫控制权的直接转移,啥叫控制权的间接转移,不要着急,后边详细讲解。 这三个编程范式最近的一个也有半个世纪的历史了,半个世纪以来没有提出新的编程范式,以后可能也不会了。因为编程范式的意义在于限制,限制了控制权转移限制了数据赋值,其他也没啥可限制的了。很有意思的是,这三个编程范式提出的时间顺序可能与大家的直觉相反,从前到后的顺序为:函数式编程(1936年)、面向对象编程(1966年)、结构化编程(1968年)。 1.结构化编程 结构化编程证明了人们可以用顺序结构、分支结构、循环结构这三种结构构造出任何程序,并限制了 goto 的使用。遵守结构化编程,工程师就可以像数学家一样对自己的程序进行推理证明,用代码将一些已证明可用的结构串联起来,只要自行证明这些额外代码是确定的,就可以推导出整个程序的正确性。 前面提到结构化编程对控制权的直接转移进行了限制,其实就是限制了 goto 语句。什么叫做控制权的直接转移?就是函数调用或者 goto 语句,代码在原来的流程里不继续执行了,转而去执行别的代码,并且你指明了执行什么代码。为什么要限制 goto 语句?因为 goto 语句的一些用法会导致某个模块无法被递归拆分成更小的、可证明的单元。而采用分解法将大型问题拆分正是结构化编程的核心价值。 其实遵守结构化编程,工程师们也无法像数学家那样证明自己的程序是正确的,只能像物理学家一样,说自己的程序暂时没被证伪(没被找到bug)。数学公式和物理公式的最大区别,就是数学公式可被证明,而物理公式无法被证明,只要目前的实验数据没把它证伪,我们就认为它是正确的。程序也是一样,所有的 test case 都通过了,没发现问题,我们就认为这段程序是正确的。 2.面向对象编程 面向对象编程包括封装、继承和多态,从架构的角度,这里只关注多态。多态让我们更方便、安全地通过函数调用的方式进行组件间通信,它也是依赖反转(让依赖与控制流方向相反)的基础。 在非面向对象的编程语言中,我们如何在互相解耦的组件间实现函数调用?答案是函数指针。比如采用C语言编写的操作系统中,定义了如下的结构体来解耦具体的IO设备, IO 设备的驱动程序只需要把函数指针指到自己的实现就可以了。 struct FILE { void (*open)(char* name, int mode); void (*close)(); int (*read)(); void (*write)(char); void (*seek)(long index, int mode);}这种通过函数指针进行组件间通信的方式非常脆弱,工程师必须严格按照约定初始化函数指针,并严格地按照约定来调用这些指针,只要一个人没有遵守约定,整个程序都会产生极其难以跟踪和消除的 Bug。所以面向对象编程限制了函数指针的使用,通过接口-实现、抽象类-继承等多态的方式来替代。 前面提到面向对象编程对控制权的间接转移进行了限制,其实就是限制了函数指针的使用。什么叫做控制权的间接转移?就是代码在原来的流程里不继续执行了,转而去执行别的代码,但具体执行了啥代码你也不知道,你只调了个函数指针或者接口。 ...

June 12, 2019 · 1 min · jiezi

面向对象基本原则3-最少知道原则与开闭原则

面向对象基本原则(3)- 最少知道原则与开闭原则五、最少知道原则【迪米特法则】1. 最少知道原则简介最少知识原则(Least KnowledgePrinciple,LKP)也称为迪米特法则(Law of Demeter,LoD)。虽然名字不同,但描述的是同一个规则:一个对象应该对其他对象有最少的了解。 通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少,你(被耦合或调用的类)的内部是如何复杂都和我没关系,那是你的事情,我就知道你提供的这么多public方法,我就调用这么多,其他的我一概不关心。 2. 最少知道原则实现只与直接关联的类交流每个对象都必然会与其他对象有耦合关系,耦合关系的类型有很多,例如组合、聚合、依赖等。 出现在成员变量、方法的输入输出参数中的类称为直接关联的类,而出现在方法体内部的类不属于直接关联的类。 下面举例说明如何才能做到只与直接关联的类交流。 场景:老师想让班长清点女生的数量 Bad/** * 老师类 * Class Teacher */class Teacher { /** * 老师对班长发布命令,清点女生数量 * @param GroupLeader $groupLeader */ public function command(GroupLeader $groupLeader) { // 产生一个女生群体 $girlList = new \ArrayIterator(); // 初始化女生 for($i = 0; $i < 20; $i++){ $girlList->append(new Girl()); } // 告诉班长开始执行清点任务 $groupLeader->countGirls($girlList); }}/** * 班长类 * Class GroupLeader */class GroupLeader { /** * 清点女生数量 * @param \ArrayIterator $girlList */ public function countGirls($girlList) { echo "女生数量是:", $girlList->count(), "\n"; }}/** * 女生类 * Class Girl */class Girl {}$teacher= new Teacher();//老师发布命令$teacher->command(new GroupLeader()); // 女生数量是:20上面实例中,Teacher类仅有一个直接关联的类 -- GroupLeader。而Girl这个类就是出现在commond方法体内,因此不属于与Teacher类直接关联的类。方法是类的一个行为,类竟然不知道自己的行为与其他类产生依赖关系,这是不允许的,违反了迪米特法则。 ...

May 27, 2019 · 5 min · jiezi

面向对象基本原则2-里式代换原则与依赖倒置原则

面向对象基本原则(2)- 里式代换原则与依赖倒置原则三、里式代换原则0. 继承的优缺点在面向对象的语言中,继承是必不可少的、非常优秀的语言机制,它有如下优点: 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性。提高代码的重用性。子类可以形似父类,但又异于父类,“龙生龙,凤生凤,老鼠生来会打洞”是说子拥有父的“种”,“世界上没有两片完全相同的叶子”是指明子与父的不同。提高代码的可扩展性,实现父类的方法就可以“为所欲为”了,君不见很多开源框架的扩展接口都是通过继承父类来完成的;提高产品或项目的开放性。自然界的所有事物都是优点和缺点并存的,继承的缺点如下: 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法。降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束。增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大段的代码需要重构。1. 里式代换原则简介里式代换原则的英文名称是 Liskov Substitution Principle,简称LSP。 里式代换原则的英文定义是: Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.意思是:所有引用基类的地方必须能透明地使用其子类的对象。 通俗点讲,就是只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。 2. 里氏替换原则为良好的继承定义了规范子类必须完全实现父类的方法/** * 枪支抽象类 * Class AbstractGun */abstract class AbstractGun{ public abstract function shoot();}/** * 手枪 * Class Handgun */class Handgun extends AbstractGun{ public function shoot() { echo "手枪射击\n"; }}/** * 步枪 * Class Rifle */class Rifle extends AbstractGun{ public function shoot() { echo "步枪射击\n"; }}如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚合、组合等关系代替继承。 ...

May 25, 2019 · 3 min · jiezi

面向对象基本原则1-单一职责原则与接口隔离原则

面向对象基本原则(1)- 单一职责原则与接口隔离原则一、单一职责原则1. 单一职责原则简介单一职责原则的英文名称是 Single Responsibility Principle,简称SRP。 单一职责原则的原话解释是: There should never be more than one reason for a class to change.意思是:应该有且仅有一个原因引起类的变更。 2. 单一职责原则优点类的复杂性降低,实现什么职责都有清晰明确的定义。可读性提高,复杂性降低,那当然可读性提高了。可维护性提高,可读性提高,那当然更容易维护了。变更引起的风险降低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助。3. 最佳实践接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化。 注意 单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计得是否优良,但是“职责”和“变化原因”都是不可度量的,因项目而异,因环境而异。4. Show me the code代码使用PHP7.2语法编写用户业务场景IUserBo 接口负责用户属性 interface IUserBo{ public function setUserID(string $userID); public function getUserID() : string ; public function setPassword(string $password); public function getPassword() : string ; public function setUserName(string $userName); public function getUserName() : string ;}IUserBiz 接口负责用户行为 interface IUserBiz{ public function changePassword(string $password) : bool ; public function deleteUser(IUserBo $userBo) : bool ; public function mapUser(IUserBo $userBo); public function addOrg(IUserBo $userBo, int $orgID) : bool; /** * 给用户添加角色 * @param IUserBo $userBo * @param int $roleID * @return bool */ public function addRole(IUserBo $userBo, int $roleID) : bool ;}UserInfo 类实现 IUserBo, IUserBiz 两个接口 ...

May 23, 2019 · 2 min · jiezi

架构整洁之道-看这一篇就够了

阿里妹导读:程序的世界飞速发展,今天所掌握的技能可能明年就过时了,但有些知识历久弥新,掌握了它们,你在程序的海洋中就不会迷路,架构思想就是这样的知识。本文是《架构整洁之道》的读书心得,作者将书中内容拆解后再组织,不仅加入了个人的独到见解,而且用一张详细的知识脉络图帮助大家了解整本书的精华。如果你读过这本书,可以将本文当做一次思想交流,如果你还没看过这本书,更要阅读这篇文章,相信你会得到不同于以往的启发。 本篇文章我们将从软件系统的价值出发,首先认识架构工作的价值和目标, 接下来依次了解架构设计的基础、指导思想(设计原则)、组件拆分的方法和粒度、组件之间依赖设计、组件边界多种解耦方式以及取舍、降低组件之间通信成本的方法,从而最终指导我们做出正确的架构决策和架构设计。 一、软件系统的价值架构是软件系统的一部分,所以要明白架构的价值,首先要明确软件系统的价值。软件系统的价值有两方面,行为价值和架构价值。 行为价值是软件的核心价值,包括需求的实现,以及可用性保障(功能性 bug 、性能、稳定性)。这几乎占据了我们90%的工作内容,支撑业务先赢是我们工程师的首要责任。如果业务是明确的、稳定的,架构的价值就可以忽略不计,但业务通常是不明确的、飞速发展的,这时架构就无比重要,因为架构的价值就是让我们的软件(Software)更软(Soft)。可以从两方面理解: 当需求变更时,所需的软件变更必须简单方便。变更实施的难度应该和变更的范畴(scope)成等比,而与变更的具体形状(shape)无关。当我们只关注行为价值,不关注架构价值时,会发生什么事情?这是书中记录的一个真实案例,随着版本迭代,工程师团队的规模持续增长,但总代码行数却趋于稳定,相对应的,每行代码的变更成本升高、工程师的生产效率降低。从老板的视角,就是公司的成本增长迅猛,如果营收跟不上就要开始赔钱啦。 可见架构价值重要性,接下来从著名的紧急重要矩阵出发,看我们如何处理好行为价值和架构价值的关系。 重要紧急矩阵中,做事的顺序是这样的:1.重要且紧急 > 2.重要不紧急 > 3.不重要但紧急 > 4.不重要且不紧急。实现行为价值的需求通常是 PD 提出的,都比较紧急,但并不总是特别重要;架构价值的工作内容,通常是开发同学提出的,都很重要但基本不是很紧急,短期内不做也死不了。所以行为价值的事情落在1和3(重要且紧急、不重要但紧急),而架构价值落在2(重要不紧急)。我们开发同学,在低头敲代码之前,一定要把杂糅在一起的1和3分开,把我们架构工作插进去。 二、架构工作的目标前面讲解了架构价值,追求架构价值就是架构工作的目标,说白了,就是用最少的人力成本满足构建和维护该系统的需求,再细致一些,就是支撑软件系统的全生命周期,让系统便于理解、易于修改、方便维护、轻松部署。对于生命周期里的每个环节,优秀的架构都有不同的追求: 开发阶段:组件不要使用大量复杂的脚手架;不同团队负责不同的组件,避免不必要的协作。部署阶段:部署工作不要依赖成堆的脚本和配置文件;组件越多部署工作越繁重,而部署工作本身是没有价值的,做的越少越好,所以要减少组件数量。运行阶段:架构设计要考虑到不同的吞吐量、不同的响应时长要求;架构应起到揭示系统运行的作用:用例、功能、行为设置应该都是对开发者可见的一级实体,以类、函数或模块的形式占据明显位置,命名能清晰地描述对应的功能。维护阶段:减少探秘成本和风险。探秘成本是对现有软件系统的挖掘工作,确定新功能或修复问题的最佳位置和方式。风险是做改动时,可能衍生出新的问题。三、编程范式其实所谓架构就是限制,限制源码放在哪里、限制依赖、限制通信的方式,但这些限制比较上层。编程范式是最基础的限制,它限制我们的控制流和数据流:结构化编程限制了控制权的直接转移,面向对象编程限制了控制权的间接转移,函数式编程限制了赋值,相信你看到这里一定一脸懵逼,啥叫控制权的直接转移,啥叫控制权的间接转移,不要着急,后边详细讲解。 这三个编程范式最近的一个也有半个世纪的历史了,半个世纪以来没有提出新的编程范式,以后可能也不会了。因为编程范式的意义在于限制,限制了控制权转移限制了数据赋值,其他也没啥可限制的了。很有意思的是,这三个编程范式提出的时间顺序可能与大家的直觉相反,从前到后的顺序为:函数式编程(1936年)、面向对象编程(1966年)、结构化编程(1968年)。 1.结构化编程 结构化编程证明了人们可以用顺序结构、分支结构、循环结构这三种结构构造出任何程序,并限制了 goto 的使用。遵守结构化编程,工程师就可以像数学家一样对自己的程序进行推理证明,用代码将一些已证明可用的结构串联起来,只要自行证明这些额外代码是确定的,就可以推导出整个程序的正确性。 前面提到结构化编程对控制权的直接转移进行了限制,其实就是限制了 goto 语句。什么叫做控制权的直接转移?就是函数调用或者 goto 语句,代码在原来的流程里不继续执行了,转而去执行别的代码,并且你指明了执行什么代码。为什么要限制 goto 语句?因为 goto 语句的一些用法会导致某个模块无法被递归拆分成更小的、可证明的单元。而采用分解法将大型问题拆分正是结构化编程的核心价值。 其实遵守结构化编程,工程师们也无法像数学家那样证明自己的程序是正确的,只能像物理学家一样,说自己的程序暂时没被证伪(没被找到bug)。数学公式和物理公式的最大区别,就是数学公式可被证明,而物理公式无法被证明,只要目前的实验数据没把它证伪,我们就认为它是正确的。程序也是一样,所有的 test case 都通过了,没发现问题,我们就认为这段程序是正确的。 2.面向对象编程 面向对象编程包括封装、继承和多态,从架构的角度,这里只关注多态。多态让我们更方便、安全地通过函数调用的方式进行组件间通信,它也是依赖反转(让依赖与控制流方向相反)的基础。 在非面向对象的编程语言中,我们如何在互相解耦的组件间实现函数调用?答案是函数指针。比如采用C语言编写的操作系统中,定义了如下的结构体来解耦具体的IO设备, IO 设备的驱动程序只需要把函数指针指到自己的实现就可以了。 struct FILE { void (*open)(char* name, int mode); void (*close)(); int (*read)(); void (*write)(char); void (*seek)(long index, int mode);}这种通过函数指针进行组件间通信的方式非常脆弱,工程师必须严格按照约定初始化函数指针,并严格地按照约定来调用这些指针,只要一个人没有遵守约定,整个程序都会产生极其难以跟踪和消除的 Bug。所以面向对象编程限制了函数指针的使用,通过接口-实现、抽象类-继承等多态的方式来替代。 前面提到面向对象编程对控制权的间接转移进行了限制,其实就是限制了函数指针的使用。什么叫做控制权的间接转移?就是代码在原来的流程里不继续执行了,转而去执行别的代码,但具体执行了啥代码你也不知道,你只调了个函数指针或者接口。 ...

May 14, 2019 · 1 min · jiezi

Python中面向对象怎么创建一个类

文字有点长,对于不想看文字的朋友,可以去这里看视频,视频可能更好理解https://www.piqizhu.com/v/1GK... 本节课,我们来学习,如何创建一个类, 也就是怎么用python画设计方案 先来看一下 前面课程里出现过的 几张设计方案 前面女娲造人的故事里,女娲创造了6张设计方案 我们接下来根据那个故事的节奏,也来创造6张设计方案 创建类创建 物种设计图 的类先来看第一张设计方案, 物种设计方案 这张设计图,里的东西,有四肢,还有个头、身体 那么我们设计的类如下: class 物种设计方案: 头 = 1 前肢 = 2 后肢 = 2 身体 = 1要创建一个类, 和创建一个函数差不多, 先写一个class,然后一个空格, 接着写类的名字, 类的名字和变量名的命名规则一样,通常类名建议驼峰命名法 也就是要像个骆驼 比如,你的类名是 man 那么应该写成 Man 如果你的类名是 goodman 那么你应该写成 GoodMan 每个单词的首字母用大写,如果只有一个字母,首字母要大写, 如果有多个单词,每个单词首字母都要大写 这只是行业里的一个约定,并没有强制规定,你不遵守也可以 我这里的例子,为了方便大家理解,会使用中文汉字 class 后面的 物种设计方案 就是我的类名, 类名后面一个冒号 接着换行,一个缩进,我这里给这个类,增加了4个属性,就像 声明变量一样 同样为了便于大家理解,这里的变量名我也用了中文汉字 这里的 物种设计方案 就是我们创建的类的名字,简称类名 在类中的这几个变量,就是这个类的属性, 就和我们平时的变量是一样的,但这里的这几个变量,归属于这个类, 就好比,我们大家都归属于中国 如此,我们就完成了, 物种设计方案的制作 ...

May 3, 2019 · 2 min · jiezi

JavaScript基础学习面向对象对象创建之工厂模式

前言上一章回顾了JS对象的属性类型,那么除了我们常用的new Object()构造函数创建对象和字面量方式创建对象的方式外,还需要用到更多的模式来解决对象被多次复用的问题。什么意思呢?就是我们很有可能会在各个地方去使用已经创建过的对象,但是对象里的属性值有可能是不同的。举个例子: let obj = { name:"勾鑫宇", age:18, myName:function(){ console.log(this.name) }}如果我们想继续使用这个obj对象,但里面的属性值需要改变,我们可能会想到这样做: let obj2 = obj;obj2.name = "张三";但是这样做有一个问题,由于JS中引用类型的机制,当你修改obj2的同时,obj也被改变了。所以我们就不得不像下面这样再重新创建一个obj2对象。 let obj2 = { name:"张三", age:23, myName:function(){ console.log(this.name) }}这样就无形中给我们增加了很多工作,代码量也会大大增加,而这么多重复的东西是完全没必要的。于是我们就需要用到创建对象的各种设计模式来解决这个问题了,这章先讲工厂模式。 工厂模式根据书上和各种百科的解释,还是先来一个官方版本,然后写写我的理解吧。 官方解释:工厂是构造方法的抽象,抽象了创建具体对象的过程。工厂方法模式的实质是“定义一个创建对象的接口,但让实现这个接口的类来决定实例化哪个类。工厂方法让类的实例化推迟到子类中进行。 工厂模式分为三类:简单工厂模式、工厂方法模式和抽象工厂模式 我的理解:至于为什么要叫工厂模式,就是因为这个模式将我们所需要的逻辑代码给封装到了一个函数里面,然后我们只需要传入相应的参数,就能够去获取需要的结果。这个过程就如同我们向工厂要东西一样简单。比如我们需要一台电脑,只需要告诉工厂电脑的屏幕尺寸是多大、系统是Win还是Linux、内存是多少G,而不用关心屏幕是怎么制作的,系统是怎么设置的,内存条是怎么做的,最重要的是最终这个电脑是怎么组装出来的我们也不关心,只关心最后能拿到一台我们所需的成品电脑就行了。 由于《JS高编》里这一部分只讲了简单工厂模式的实现,其他两种模式就先不说,更多的可以去看《JS设计模式》。 //创建一个简单函数,就当作一个类//第一种方式是通过new Object()创建对象,最后返回它function person(name,age){ let o = new Object(); o.name = name; o.age = age; o.myName = function(){ console.log("我的名字是"+o.name) } return o;}let person1 = person("勾鑫宇",18);let person2 = person("张三",23);person1.myName();//输出“我的名字是勾鑫宇”person2.age;//输出23//第二种方式是通过字面量形式创建对象function person(name,age){ let o = { name: name; age: age; myName: function(){ console.log("我的名字是"+o.name) } } return o;}上面的方法使用简单工厂模式封装了一个类,然后我们只需要传入名字和年龄的参数就行了。那么我们还可以添加稍微复杂一点的逻辑在这个工厂里面。 ...

April 29, 2019 · 1 min · jiezi

JavaScript基础学习面向对象部分属性类型

前言JavaScript发明之始,从技术上来讲就是一门面向对象的语言,但在ES6之前,JS的很多特性和传统的面向对象语言有所不同,比如没有类的概念(ES6有了class)。今天结合《JS高编》第六章开始回顾和深入学习面向对象部分,包括对象、原型、原型链、继承等部分。 一、理解对象谈JS的对象之前,先复习一下面向对象的基础概念和特点吧。面向对象OOP(Object-oriented programming),结合维基百科和百度百科的阐述,再谈谈我的理解。 官方解释:面向对象就是基于对象概念,以对象为中心,以类和继承为构造机制,来认识、理解、刻画客观世界和设计、构建相应的软件系统 我的理解:在JavaScript的世界中,万物皆对象。任何事和物你都可以将其定义为一个对象,程序员界有个笑话就是单身狗可以new一个对象嘛......我的粗浅理解,如果我是一个上帝,这个世界的任何人和事相对于我而言都是一个对象。有了控制对象的权力,我就可以对他们进行任何操作。针对事,我可以发布一个号令,发布一个政策,告诉别人怎么执行,什么时候开始,什么时候结束。针对人,我可以把他们分为男人、女人,这就是类。然后我可以限制他们的儿子是男人还是女人,是男人那就必须有和爸爸一样的性别特征,这就是继承。我还可以控制他们什么时间做什么事等等,整个过程我都是围绕某个对象来展开的,那么这个过程叫做面向对象。 特点:1.类2.继承3.封装4.多态具体的在后面学习和复习时再谈。 二、对象的属性类型1.数据属性:[[Configurable]],[[Enumerable]],[[Writable]],[[Value]]2.访问器属性:[[Configurable]],[[Enumerable]],[[Get]],[[Set]] 书上讲到属性类型时,只是简单提了一下是为了表示对象的特性,描述了属性的特征,并且在JS中不能直接访问。光看介绍不太理解到底是干什么的,但是看了数据属性的内容之后,发现不难理解。 我的理解,数据属性就是我们可以从根源去控制一个对象的属性是否能被修改、删除、循环等,并可以通过访问器属性在别人不知道的情况下进行数据处理。通过Object.defineProperty()这个方法,我们可以去设置这些限制对象属性操作的值,从而限制别人对某个对象属性的操作。举个例子,上面的obj这个对象的name属性的值是“勾鑫宇”,从现在起我不想任何人能够修改它的值,那么我就通过数据属性来将这个属性设置为不可修改,别人用obj.name = "张三"来修改就不会生效了。而我如果想在修改name属性的值后同时让age也跟着改变,那么此时就可以用访问器属性来进行数据处理。 我们是通过Object.defineProperty()这个方法来进行两种属性的设置。那么首先了解一下Object.defineProperty()这个方法,它接收三个参数: Object.defineProperty(对象名,属性名,描述符对象)//举例Object.defineProperty(obj,"name",{ writable:false,//设置不可修改 enumerable:false//设置不可循环到该属性})可以在对象的constructor中找到该方法 同时,我们可以通过Object.getOwnPropertyDescriptor()方法来查看这四个特性的设置情况。接受两个参数: Object.getOwnPropertyDescriptor(对象名,属性名)数据属性数据属性包含一个数据值的位置。在这个位置可以读取和写入值,有4个描述其行为的特性。 下面就具体来对每个数据属性进行分析:1.[[Writable]]:英文意思译为“可写的”,可理解为“可修改的”。这个属性用来设置对象的某个属性是否能被修改,默认为true。 //举例let obj = { name:"勾鑫宇", age:23}Object.defineProperty(obj,"name",{ writable:false,//设置不可修改})//这时再进行修改就不会生效,严格模式下会报错obj.name = "张三"console.log(obj.name)//输出的还是勾鑫宇严格模式报错 2.[[Enumerable]]:英文译为“可数的,可枚举的”,是否支持for-in循环来返回属性,默认为true。 //举例let obj = { name:"勾鑫宇", age:23, gender:male}Object.defineProperty(obj,"name",{ enumerable:false,//设置不可通过for-in循环返回})//循环测试for(let i in obj){ console.log(i)//输出结果为age,gender,没有name属性,效果就像隐藏了这个属性。}//但这时我们的name属性还是存在的console.log(obj)3.[[value]]:这个就不说翻译了,大家都知道,就是值。这个特性是设置我们对象某个属性的值,读值、写值都在这里,默认值为undefined。 //举例let obj = { name:"勾鑫宇", age:23, gender:male}Object.defineProperty(obj,"name",{ value:"张三",//设置name的值为张三})console.log(obj.name)//输出为张三//设置value不影响后面再次修改值,value相当于修改了一次你最先定义的值而已。obj.name = "傻逼"concole.log(obj.name)//输出为“傻逼”4.[[Configurable]]:英文译为“可配置的”,这个和前面的Writable有什么区别呢?放到最后讲是有原因的。前面有设置修改,设置循环,设置值,但是还没有设置是否可删除。Configurable就是做这个事情的。它表示能否通过delete删除属性从而重新定义属性,默认值为true。 //举例let obj = { name:"勾鑫宇", age:23, gender:male}Object.defineProperty(obj,"name",{ configurable:false,//不允许删除属性})delete obj.name//报错"Uncaught TypeError: Cannot delete property 'name' of #<Object>"这个属性还有最重要的一个特点,就是当你设置为false过后,就不能再设为true了,即使你设置了也无效。书上说得个时候你再设置value,enumerable都不会生效,只能设置writable,那么我们来试试。 ...

April 28, 2019 · 1 min · jiezi

Python中什么是面向对象封装

文字有点长,对于不想看文字的朋友,可以去这里看视频,内容和这个文字一样的,视频可能更好理解https://www.piqizhu.com/v/yjB...回顾面向过程前面我们已经学过了 面向过程 我们知道,面向过程 的时候,我们的关注点是 步骤 面向过程做事,就是把 一件事情 分割为多个步骤, 然后依次去完成每一个步骤 这样做事可以让我们的事情变的很明朗,不会弄乱 那么, 既然有了面向过程,为什么还要跑出来一个面向对象呢?面向对象是什么东西呢?有啥好处?他和面向过程有啥关系呢? 他们两个我们应该选谁呢?带着这么3个问题,开始本节课的讲解 面向对象概述所谓面向对象, 意思就是,我们的关注点 是对象, 而非过程(步骤) 那么,这里的对象是啥意思呢? 要回答这个问题, 就必须先回到实际的案例中去讲解 上节课的我们的案例是制作一个玩具鸭子,我们的关注点是制作鸭子的每一个步骤 如果我们只是捏个泥娃娃,或者制作一个简单的玩具鸭子, 使用面向过程,是没有多大问题的 但是如果我们面对的一个复杂的事情呢? 有一家玩具公司,这家玩具公司不止生成玩具鸭,还生产玩具狗,玩具猫,玩具猫头鹰, 。。。。。 等等 100多种玩具 如果按照之前的 面向过程思路,那么我们的代码 会很长, 很杂乱, 那 怎么办呢? 这时候就需要使用面向对象的思路来解决问题了 面向对象-封装于是某一天,,这家玩具公司有了一台鸭子制造机器, 这台机器,当我们按下开关后,它就会立刻开始制作玩具鸭子, 此刻,我们不再去关注先做脚,还是先做头, 还是身体, 这台机器会帮我们搞定所有步骤, 我们只需要在机器的出口处,等着完整的玩具鸭子出来就可以了 这时候,我们的关注点,就是这台机器本身,而不是制作鸭子的某一个步骤 这台机器,此刻就是一个对象(整体),此刻,我们就开始了面向对象 可能到这里大家还是不明白,还是有点糊涂,这是正常的, 请允许我再来解释解释 这台鸭子制造机器,包含了 以前制造鸭子的所有步骤,它把制作鸭子的步骤,封装在了机器内部, 留给我们的,只有一个开关,我们只需要按开关,就可以开始制造鸭子 而以前的面向过程,我们需要关注制造鸭子的细节,需要先制作鸭头,然后制作翅膀,然后.... 但是,当我们有了一台封装了详细步骤的机器,只需要关心什么时候按开关,别的都不用关心 这就是面向对象的第一个特性(好处): 封装 封装特性,可以把复杂的信息,流程,包起来,内部处理, 让使用者不去关注细节, 只关心什么时候按开关, 如此一来当我们要制作鸭子的时候,只要按开关就可以了,是不是省心很多?? 再来举个例子比如,某一天,你以程序员的身份,去某家公司工作,老板让你开发一个网站, 此刻,老板就是面向了对象,这里的对象,在老板眼里 就是你, 因为老板只要把任务丢给你,他不关注你用什么电脑写代码,也不关注你用什么输入法,不会关注你写代码的时候听什么歌,不会关注你写代码的时候是穿拖鞋好,还是光脚丫好, 更加不会关注你今天穿什么颜色的内裤 写代码效率更高; 但是在你自己的角度,你就是面向过程的,你会关心自己用哪个电脑写代码更舒服, 你会关注自己用哪个输入法效率更高,你还会关注写代码应该听什么歌.. 等等 而,当你写代码的时候,你使用的电脑,对于你而言,也是一个封装好的对象, 当你在键盘上按下字母A, 你不会关注电脑内部究竟发生了 多么复杂的化学反应, 你只关注,我按了键盘上的字母A,电脑就要显示一个A在屏幕上 ...

April 23, 2019 · 1 min · jiezi

<<深入PHP面向对象、模式与实践>>读书笔记:面向对象设计和过程式编程

注:本文内容来<<深入PHP面向对象、模式与实践>>中6.2节。6.2 面向对象设计与过程式编程 面向对象设计和过程式编程有什么不同呢?可能有些人认为最大的不同在于面向对象编程中包含对象。事实上,这种说法不准确。在PHP中,你经常会发现过程式编程也使用对象,如使用一个数据库类,也可能遇到类中包含过程式代码的情况。类的出现并不能说明使用了面向对象设计。甚至对于Java这种强制把一切都包含在类中的语音(这个我可以证明,我在大三的时候学过Java),使用对象也不能说明使用了面向对象设计。 面向对象编程和过程式编程的一个核心区别是如何分配职责。过程式编程表现为一系列命令和方法的连续调用。控制代码根据不同的条件执行不同的职责。这种自顶向下的控制方式导致了重复和相互依赖的代码遍布于整个项目。面向对象编程则将职责从客户端代码中移到专门的对象中,尽量减少相互依赖。 为了说明以上几点,我们分别使用面向对象和过程式代码的方式来分析一个简单的问题。假设我们要创建一个用于读写配置文件的工具。为了重点关注代码的结构,示例中将忽略具体的功能实现。(文后有完整代码示例,来自于图灵社区) 我们先按过程式方式来解决这个问题。首先,用下面的格式来读写文本🔑value只需要两个函数:function readParams( $sourceFile ) { $params = array(); // 从$sourceFile中读取文本参数 return $params;}function writeParams( $params, $sourceFile ) { // 写入文本参数到$sourceFile}readParams()函数的参数为源文件的名称。该函数试图打开文件,读取每一行内容并查找键/值对,然后用键/值对构建一个关联数组。最后,该函数给控制代码返回数组。writeParams()以关联数组和指向源文件的路径作为参数,它循环遍历关联数组,将每对键/值对写入文件。下面是使用这两个函数的客户端代码:$file = ‘./param.txt’;$array[‘key1’] = ‘vall’;$array[‘key2’] = ‘val2’;$array[‘key3’] = ‘val3’;writeParams( $array, $file );$output = readParams( $file );print_r( $output );这段代码较为紧凑并且易于维护。writeParams()被调用来创建Param.txt并向其写入如下的内容:key1:val1key2:val2key3:val3现在,我们被告知这个工具需要支持如下所示XML格式:<params> <param> <key>my key</key> <val>my val</val> </param></params> 如果参数文件以.xml文件结尾,就应该以XML模式读取参数文件。虽然这不难调节,但可能会使我们的代码更难维护。这是我们有两个选择:可以在控制代码中检查文件扩展名,或者在读写函数中检测。我们使用后面那种写法。:function readParams( $source ) { $params = array(); if ( preg_match( “/.xml$/i”, $source ) ) { // 从$source中读取XML参数 } else { // $source中读取文本参数 } return $params;}function writeParams( $params, $source ) { if ( preg_match( “/.xml$/i”, $source ) ) { // 写入XML参数到$source } else { // 写入文本参数到$source }} 如上所示,我们在两个函数中都要检查XML扩展名,这样的重复性代码会产生问题。如果我们还被要求支持其他格式的参数,就要保持readParams()和writeParams()函数的一致性。 下面我们用类来处理相同的问题。首先,创建一个抽象的基类来定义类型接口:abstract class ParamHandler { protected $source; protected $params = array(); function __construct( $source ) { $this->source = $source; } function addParam( $key, $val ) { $this->params[$key] = $val; } function getAllParams() { return $this->params; } static function getInstance( $filename ) { if ( preg_match( “/.xml$/i”, $filename )) { return new XmlParamHandler( $filename ); } return new TextParamHandler( $filename ); } abstract function write(); abstract function read();} 我们定义addParam()方法来允许用户增加参数到protected属性$params, getAllParams()则用于访问该属性,获得$params的值。 我们还创建了静态的getInstance()方法来检测文件扩展名,并根据文件扩展名返回特定的子类。最重要的是,我们定义了两个抽象方法read()和write(),确保ParamHandler类的任何子类都支持这个接口。 现在,我们定义了多个子类。为了实例简洁,再次忽略实现细节:class XmlParamHandler extends ParamHandler { function write() { // 写入XML文件 // 使用$this->params } function read() { // 读取XML文件内容 // 并赋值给$this->params } }class TextParamHandler extends ParamHandler { function write() { // 写入文本文件 // 使用$this->params } function read() { // 读取文本文件内容 // 并赋值给$this->params } } 这些类简单地提供了write()和read()方法的实现。每个类都将根据适当的文件格式进行读写。客户端代码将完全自动地根据文件扩展名来写入数据到文本和XML格式的文件:$file = “./params.xml”; $test = ParamHandler::getInstance( $file );$test->addParam(“key1”, “val1” );$test->addParam(“key2”, “val2” );$test->addParam(“key3”, “val3” );$test->write(); // 写入XML格式中我们还可以从两种文件格式中读取:$test = ParamHandler::getInstance( “./params.txt” );$test->read(); // 从文本格式中读取那么,我们可以从这两种解决方案中学习到什么呢?职责 在过程式编程的例子中,控制代码的职责(duties)是判断文件格式,它判断了两次而不是一次。条件语句被绑定到函数中,但这仅是将判断的流程影藏起来。对readParams()的调用和对writeParams()的调用必须发生在不同的地方,因此我们不得不在每个函数中重复检测文件扩展名(或执行其他检测操作)。 在面向对象代码中,我们在静态方法getInstance()中进行文件格式的选择,并且仅在getInstance()中检测文件扩展名一次,就可以决定使用哪一个合适的子类。客户端代码并不负责实现读写功能。它不需要知道自己属于哪个子类就可以使用给定的对象。它只需要知道自己在使用ParamHandler对象,并且ParamHandler对象支持write()和read()的方法。过程式代码忙于处理细节,而面向对象代码只需一个接口即可工作,并且不要考虑实现的细节。由于实现由对象负责,而不是由客户端代码负责,所以我们能够很方便地增加对新格式的支持。内聚 内聚(cohesion)是一个模块内部各成分之间相互关联程度的度量。理想情况下,你应该使各个组件职责清晰、分工明确。如果代码间的关联范围太广,维护就会很困难–因为你需要在修改部分代码的同时修改相关代码。 前面的ParamHandler类将相关的处理过程集中起来。用于处理XML的类方法间可以共享数据,并且一个类方法中的改变可以很容易地反映到另一个方法中(比如改变XML元素名)。因此我们可以说ParamHandler类是高度内聚的。 另一方面,过程式的例子则把相关的过程分离开,导致处理XML的代码在多个函数中同时出现。耦合 当系统各部分代码紧密绑在一起时,就会产生精密耦合(coupling),这时在一个组件中的变化会迫使其他部件随之改变。紧密耦合不是过程式代码特有的,但是过程式代码比较容易产生耦合问题。 我们可以在过程代码中看到耦合的产生。在writeParams()和readParams()函数中,使用了相同的文件扩展名测试来决定如何处理数据。因此我们要改下一个函数,就不得不同时改写另一个函数。例如,我们要增加一种新的文件格式,就要在两个函数中按相同的方式都加上相应的扩展名检查代码,这样两个函数才能保持一致。 面向对象的示例中则将每个子类彼此分开,也将其余客户端代码分开。如果需要增加新的参数格式,只需简单地创建相应的子类,并在父类的静态方法getInstance()中增加一行文件检测代码即可。正交 (orthogonality)指将职责相关的组件紧紧结合在一起,而与外部系统环境隔开,保持独立。在<<The Pragmatic Programmer>>(中文名<<程序员修炼之道:从小工到专家 >>)一书中有所介绍。 正交主张重用组件,期望不需要任何特殊配置就能把一个组件插入到新系统中。这样的组件有明确的与环境无关的输入和输出。正交代码使修改变得更简单,因为修改一个实现只会影响到被改动的组件本身。最后,正交代码更加安全。bug的影响只局限于它的作用域之中。内部高度相互依赖的代码发生错误时,很容易在系统中引起连锁反应。 如果只有一个类,松散耦合和高聚合是无从谈起的。毕竟,我们可以把整个过程示例的全部代码塞到一个被误导的类里。(这想想就挺可怕的。)职责和耦合的英文翻译原文是没有的,我通过Goole翻译加上的。代码示例过程式编程<?php$file = “./texttest.proc.xml”; $array[‘key1’] = “val1”;$array[‘key2’] = “val2”;$array[‘key3’] = “val3”;writeParams( $array, $file );$output = readParams( $file );print_r( $output ); function readParams( $source ) { $params = array(); if ( preg_match( “/.xml$/i”, $source )) { $el = simplexml_load_file( $source ); foreach ( $el->param as $param ) { $params["$param->key"] = “$param->val”; } } else { $fh = fopen( $source, ‘r’ ); while ( ! feof( $fh ) ) { $line = trim( fgets( $fh ) ); if ( ! preg_match( “/:/”, $line ) ) { continue; } list( $key, $val ) = explode( ‘:’, $line ); if ( ! empty( $key ) ) { $params[$key]=$val; } } fclose( $fh ); } return $params;}function writeParams( $params, $source ) { $fh = fopen( $source, ‘w’ ); if ( preg_match( “/.xml$/i”, $source )) { fputs( $fh, “<params>\n” ); foreach ( $params as $key=>$val ) { fputs( $fh, “\t<param>\n” ); fputs( $fh, “\t\t<key>$key</key>\n” ); fputs( $fh, “\t\t<val>$val</val>\n” ); fputs( $fh, “\t</param>\n” ); } fputs( $fh, “</params>\n” ); } else { foreach ( $params as $key=>$val ) { fputs( $fh, “$key:$val\n” ); } } fclose( $fh );}面向对象设计<?phpabstract class ParamHandler { protected $source; protected $params = array(); function __construct( $source ) { $this->source = $source; } function addParam( $key, $val ) { $this->params[$key] = $val; } function getAllParams() { return $this->params; } protected function openSource( $flag ) { $fh = @fopen( $this->source, $flag ); if ( empty( $fh ) ) { throw new Exception( “could not open: $this->source!” ); } return $fh; } static function getInstance( $filename ) { if ( preg_match( “/.xml$/i”, $filename )) { return new XmlParamHandler( $filename ); } return new TextParamHandler( $filename ); } abstract function write(); abstract function read();}class XmlParamHandler extends ParamHandler { function write() { $fh = $this->openSource(‘w’); fputs( $fh, “<params>\n” ); foreach ( $this->params as $key=>$val ) { fputs( $fh, “\t<param>\n” ); fputs( $fh, “\t\t<key>$key</key>\n” ); fputs( $fh, “\t\t<val>$val</val>\n” ); fputs( $fh, “\t</param>\n” ); } fputs( $fh, “</params>\n” ); fclose( $fh ); return true; } function read() { $el = @simplexml_load_file( $this->source ); if ( empty( $el ) ) { throw new Exception( “could not parse $this->source” ); } foreach ( $el->param as $param ) { $this->params["$param->key"] = “$param->val”; } return true; } }class TextParamHandler extends ParamHandler { function write() { $fh = $this->openSource(‘w’); foreach ( $this->params as $key=>$val ) { fputs( $fh, “$key:$val\n” ); } fclose( $fh ); return true; } function read() { $lines = file( $this->source ); foreach ( $lines as $line ) { $line = trim( $line ); list( $key, $val ) = explode( ‘:’, $line ); $this->params[$key]=$val; } return true; } }//$file = “./texttest.xml”; $file = “./texttest.txt”; $test = ParamHandler::getInstance( $file );$test->addParam(“key1”, “val1” );$test->addParam(“key2”, “val2” );$test->addParam(“key3”, “val3” );$test->write();$test = ParamHandler::getInstance( $file );$test->read();$arr = $test->getAllParams();print_r( $arr );本文为作者自己读书总结的文章,由于作者的水平限制,难免会有错误,欢迎大家指正,感激不尽。 ...

February 23, 2019 · 4 min · jiezi

PHP面试常考内容之面向对象(2)

PHP面试专栏正式起更,每周一、三、五更新,提供最好最优质的PHP面试内容。继上一篇“PHP面试常考内容之面向对象(1)”发表后,今天更新(2),需要(1)的可以直接点击文字进行跳转获取。整个面向对象文章的结构涉及的内容模块有:一、面向对象与面向过程有什么区别?二、面向对象有什么特征?三、什么是构造函数和析构函数?四、面向对象的作用域范围有哪几种?五、PHP 中魔术方法有哪些?六、什么是对象克隆?七、this、self和parent的区别是什么?八、抽象类与接口有什么区别与联系?九、PHP面向对象的常考面试题讲解关于PHP面向对象的内容将会被分为三篇文章进行讲解完整块内容,第一篇主要讲解一到四点内容,第二篇主要讲解五到八的内容,第三篇围绕第九点进行讲解。以下正文的内容都来自《PHP程序员面试笔试宝典》书籍,如果转载请保留出处:五、PHP种魔术方法有哪些?在PHP中,把所有以__(两个下画线)开头的类方法保留为魔术方法。所以在定义类方法时,不建议使用 __ 作为方法的前缀。下面分别介绍每个魔术方法的作用。1.__get、__set、__isset、__unset这四个方法是为在类和它们的父类中没有声明的属性而设计的。1)在访问类属性的时候,若属性可以访问,则直接返回;若不可以被访问,则调用__get 函数。方法签名为:public mixed __get ( string $name )2)在设置一个对象的属性时,若属性可以访问,则直接赋值;若不可以被访问,则调用__set 函数。方法签名为:public void __set ( string $name , mixed $value )3)当对不可访问的属性调用 isset() 或 empty() 时,__isset() 会被调用。方法签名为:public bool __isset ( string $name )4)当对不可访问属性调用 unset() 时,__unset() 会被调用。方法签名为:public bool _unset ( string $name )需要注意的是,以上存在的不可访问包括属性没有定义,或者属性的访问控制为proteced或private(没有访问权限的属性)。下面通过一个例子把对象变量保存在另外一个数组中。<?php class Test { /* 保存未定义的对象变量 */ private $data = array(); public function __set($name, $value){ $this->data[$name] = $value; } public function __get($name){ if(array_key_exists($name, $this->data)) return $this->data[$name]; return NULL; } public function __isset($name){ return isset($this->data[$name]); } public function __unset($name){ unset($this->data[$name]); } } $obj = new Test; $obj->a = 1; echo $obj->a . “\n”;?>程序的运行结果为12.__construct、__destruct1)__construct 构造函数,实例化对象时被调用。2)__destruct 析构函数,当对象被销毁时调用。通常情况下,PHP只会释放对象所占有的内存和相关的资源,对于程序员自己申请的资源,需要显式地去释放。通常可以把需要释放资源的操作放在析构方法中,这样可以保证在对象被释放的时候,程序员自己申请的资源也能被释放。例如,可以在构造函数中打开一个文件,然后在析构函数中关闭文件。<?php class Test { protected $file = NULL; function __construct(){ $this->file = fopen(“test”,“r”); } function __destruct(){ fclose($this->file); } }?>3.__call()和__callStatic()1)__call( $method, $arg_array ):当调用一个不可访问的方法时会调用这个方法。2)__callStatic的工作方式与 __call() 类似,当调用的静态方法不存在或权限不足时,会自动调用__callStatic()。使用示例如下: <?php class Test { public function __call ($name, $arguments) { echo “调用对象方法 ‘$name’ “. implode(’, ‘, $arguments). “\n”; } public static function __callStatic ($name, $arguments) { echo “调用静态方法 ‘$name’ “. implode(’, ‘, $arguments). “\n”; } } $obj = new Test; $obj->method1(‘参数1’); Test::method2(‘参数2’); ?>程序的运行结果为调用对象方法 ‘method1’ 参数1 调用静态方法 ‘method2’ 参数24.__sleep()和__wakeup()1)__sleep 串行化的时候调用。2)__wakeup 反串行化的时候调用。也就是说,在执行serialize()和unserialize()时,会先调用这两个函数。例如,在序列化一个对象时,如果这个对象有一个数据库连接,想要在反序列化中恢复这个连接的状态,那么就可以通过重载这两个方法来实现。示例代码如下:<?php class Test { public $conn; private $server, $user, $pwd, $db; public function __construct($server, $user, $pwd, $db) { $this->server = $server; $this->user = $user; $this->pwd = $pwd; $this->db = $db; $this->connect(); } private function connect() { $this->conn = mysql_connect($this->server, $this->user, $this->pwd); mysql_select_db($this->db, $this->conn); } public function __sleep() { return array(‘server’, ‘user’, ‘pwd’, ‘db’); } public function __wakeup() { $this->connect(); } public function __destruct(){ mysql_close($conn); } }?>5.__toString()__toString 在打印一个对象时被调用,可以在这个方法中实现想要打印的对象的信息,使用示例如下:<?php class Test { public $age; public function __toString() { return “age:$this->age”; } } $obj = new Test(); $obj->age=20; echo $obj;?>程序的运行结果为age:206.__invoke()在引入这个魔术方法后,可以把对象名当作方法直接调用,它会间接调用这个方法,使用示例如下:<?php class Test { public function __invoke() { print “hello world”; } } $obj = new Test; $obj();?>程序的运行结果为hello world7.__set_state()调用 var_export 时被调用,用__set_state的返回值作为var_export 的返回值。使用示例如下:<?php class People { public $name; public $age; public static function __set_state ($arr) { $obj = new People; $obj->name = $arr[’name’]; $obj->age = $arr[‘aage’]; return $obj; } } $p = new People; $p->age = 20; $p->name = ‘James’; var_dump(var_export($p));?>程序的运行结果为People::__set_state(array( ’name’ => ‘James’, ‘age’ => 20,)) NULL8.__clone()这个方法在对象克隆的时候被调用,php提供的__clone()方法对一个对象实例进行浅拷贝,也就是说,对对象内的基本数值类型通过值传递完成拷贝,当对象内部有对象成员变量的时候,最好重写__clone方法来实现对这个对象变量的深拷贝。使用示例如下:<?php class People { public $age; public function __toString() { return “age:$this->age \n”; } } class MyCloneable { public $people; function __clone() { $this->people = clone $this->people; //实现对象的深拷贝 } } $obj1 = new MyCloneable(); $obj1->people = new People(); $obj1->people->age=20; $obj2 = clone $obj1; $obj2->people->age=30; echo $obj1->people; echo $obj2->people;?>程序的运行结果为age:20 age:30由此可见,通过对象拷贝后,对其中一个对象值的修改不影响另外一个对象。9.__autoload()当实例化一个对象时,如果对应的类不存在,则该方法被调用。这个方法经常的使用方法为:在方法体中根据类名,找出类文件,然后require_one 导入这个文件。由此,就可以成功地创建对象了,使用示例如下:Test.php:<?php class Test { function hello() { echo ‘Hello world’; } }?>index.php:<?php function __autoload( $class ) { $file = $class . ‘.php’; if ( is_file($file) ) { require_once($file); //导入文件 } } $obj = new Test(); $obj->hello();?>程序的运行结果为Hello world在index.php中,由于没有包含Test.php,在实例化Test对象的时候会自动调用__autoload方法,参数$class的值即为类名Test,这个函数中会把Test.php引进来,由此Test对象可以被正确地实例化。这种方法的缺点是需要在代码中文件路径做硬编码,当修改文件结构的时候,代码也要跟着修改。另一方面,当多个项目之间需要相互引用代码的时候,每个项目中可能都有自己的__autoload,这样会导致两个__autoload冲突。当然可以把__autoload修改成一个。这会导致代码的可扩展性和可维护性降低。由此从PHP5.1开始引入了spl_autoload,可以通过spl_autoload_register注册多个自定义的autoload方法,使用示例如下:index.php<?php function loadprint( $class ) { $file = $class . ‘.php’; if (is_file($file)) { require_once($file); } } spl_autoload_register( ’loadprint’ ); //注册自定义的autoload方法从而避免冲突 $obj = new Test(); $obj->hello();?>spl_autoload是_autoload()的默认实现,它会去include_path中寻找$class_name(.php/.inc) 。除了常用的spl_autoload_register外,还有如下几个方法:1)spl_autoload:_autoload()的默认实现。2)spl_autoload_call:这个方法会尝试调用所有已经注册的__autoload方法来加载请求的类。3)spl_autoload_functions:获取所有被注册的__autoload方法。4)spl_autoload_register:注册__autoload方法。5)spl_autoload_unregister:注销已经注册的__autoload方法。6)spl_autoload_extensions:注册并且返回spl_autoload方法使用的默认文件的扩展名。引申:PHP有哪些魔术常量?除了魔术变量外,PHP还定义了如下几个常用的魔术常量。1)LINE:返回文件中当前的行号。2)FILE:返回当前文件的完整路径。3)FUNCTION:返回所在函数名字。4)CLASS:返回所在类的名字。5)METHOD:返回所在类方法的名称。与__FUNCTION__不同的是,__METHOD__返回的是“class::function”的形式,而__FUNCTION__返回“function”的形式。6)DIR:返回文件所在的目录。如果用在被包括文件中,则返回被包括的文件所在的目录(PHP 5.3.0中新增)。7)NAMESPACE:返回当前命名空间的名称(区分大小写)。此常量是在编译时定义的(PHP 5.3.0 新增)。8)TRAIT:返回 Trait 被定义时的名字。Trait 名包括其被声明的作用区域(PHP 5.4.0 新增)。六、什么是对象克隆?对于对象而言,PHP用的是引用传递,也就是说,对象间的赋值操作只是赋值了一个引用的值,而不是整个对象的内容,下面通过一个例子来说明引用传递存在的问题:<?php class My_Class { public $color; } $obj1 = new My_Class (); $obj1->color = “Red”; $obj2 = $obj1; $obj2->color =“Blue”; //$obj1->color的值也会变成"Blue”?>因为PHP使用的是引用传递,所以在执行$obj2 = $obj1后,$obj1和$obj2都是指向同一个内存区(它们在内存中的关系如下图所示),任何一个对象属性的修改对另外一个对象也是可见的。在很多情况下,希望通过一个对象复制出一个一样的但是独立的对象。PHP提供了clone关键字来实现对象的复制。如下例所示:<?php class My_Class { public $color; } $obj1 = new My_Class (); $obj1->color = “Red”; $obj2 = clone $obj1; $obj2->color =“Blue”; //此时$obj1->color的值仍然为"Red”?>$obj2 = clone $obj1把obj1的整个内存空间复制了一份存放到新的内存空间,并且让obj2指向这个新的内存空间,通过clone克隆后,它们在内存中的关系如下图所示。此时对obj2的修改对obj1是不可见的,因为它们是两个独立的对象。在学习C++的时候有深拷贝和浅拷贝的概念,显然PHP也存在相同的问题,通过clone关键字克隆出来的对象只是对象的一个浅拷贝,当对象中没有引用变量的时候这种方法是可以正常工作的,但是当对象中也存在引用变量的时候,这种拷贝方式就会有问题,下面通过一个例子来进行说明:<?php class My_Class { public $color; } $c =“Red”; $obj1 = new My_Class (); $obj1->color =&$c; //这里用的是引用传递 $obj2 = clone $obj1; //克隆一个新的对象 $obj2->color=“Blue”; //这时,$obj1->color的值也变成了"Blue”?>在这种情况下,这两个对象在内存中的关系如下图所示。从上图中可以看出,虽然obj1与obj2指向的对象占用了独立的内存空间,但是对象的属性color仍然指向一个相同的存储空间,因此当修改了obj2->color的值后,意味着c的值被修改,显然这个修改对obj1也是可见的。这就是一个非常典型的浅拷贝的例子。为了使两个对象完全独立,就需要对对象进行深拷贝。那么如何实现呢,PHP提供了类似于__clone方法(类似于C++的拷贝构造函数)。把需要深拷贝的属性,在这个方法中进行拷贝:使用示例如下:<?php class My_Class { public $color; public function __clone() { $this->color = clone $this->color; } } $c =“Red”; $obj1 = new My_Class (); $obj1->color =&$c; $obj2 = clone $obj1; $obj2->color=“Blue”; //这时,$obj1->color的值仍然为"Red”?>通过深拷贝后,它们在内存中的关系如图1-4所示。通过在__clone方法中对对象的引用变量color进行拷贝,使obj1与obj2完全占用两块独立的存储空间,对obj2的修改对obj1也不可见。自己整理了一篇“如果遇到代码怎么改都没效果,怎么办?”的文章,关注公众号:“琉忆编程库”,回复:“问题”,我发给你。七、this、self和parent的区别是什么?this、self、parent三个关键字从字面上比较好理解,分别是指这、自己、父亲。其中,this指的是指向当前对象的指针(暂用C语言里面的指针来描述),self指的是指向当前类的指针,parent指的是指向父类的指针。以下将具体对这三个关键字进行分析。##1.this关键字## 1 <?php 2 class UserName { 3 private $name; // 定义成员属性 4 function __construct($name) { 5 $this->name = $name; // 这里已经使用了this指针 6 } 7 // 析构函数 8 function __destruct() { 9 } 10 // 打印用户名成员函数 11 function printName() { 12 print ($this->name."\n") ; // 又使用了this指针 13 } 14 } 15 // 实例化对象 16 $nameObject = new UserName ( “heiyeluren” ); 17 // 执行打印 18 $nameObject->printName (); // 输出: heiyeluren 19 // 第二次实例化对象 20 $nameObject2 = new UserName ( “PHP5” ); 21 // 执行打印 22 $nameObject2->printName (); // 输出:PHP5 23 ?>上例中,分别在5行和12行使用了this指针,那么this到底是指向谁呢?其实,this是在实例化的时候来确定指向谁,例如,第一次实例化对象的时候(16行),当时this就是指向$nameObject 对象,那么执行第12行打印的时候就把print($this->name)变成了print ($nameObject->name),输出"heiyeluren"。对于第二个实例化对象,print( $this- >name )变成了print( $nameObject2->name ),于是就输出了"PHP5"。所以,this就是指向当前对象实例的指针,不指向任何其他对象或类。2.self关键字先要明确一点,self是指向类本身,也就是self是不指向任何已经实例化的对象,一般self用来访问类中的静态变量。 1 <?php 2 class Counter { 3 // 定义属性,包括一个静态变量 4 private static $firstCount = 0; 5 private $lastCount; 6 // 构造函数 7 function __construct() { 8 // 使用self来调用静态变量,使用self调用必须使用::(域运算符号) 9 $this->lastCount = ++ selft::$firstCount; 10 } 11 // 打印lastCount数值 12 function printLastCount() { 13 print ($this->lastCount) ; 14 } 15 } 16 // 实例化对象 17 $countObject = new Counter (); 18 $countObject->printLastCount (); // 输出 1 19 ?>上述示例中,在第4行定义了一个静态变量$firstCount,并且初始值为0,那么在第9行的时候调用了这个值,使用的是self来调用,中间使用域运算符“::”来连接,这时候调用的就是类自己定义的静态变量$firstCount,它与下面对象的实例无关,只是与类有关,无法使用this来引用,只能使用 self来引用,因为self是指向类本身,与任何对象实例无关。3.parent关键字parent是指向父类的指针,一般使用parent来调用父类的构造函数。 1 <?php 2 // 基类 3 class Animal { 4 // 基类的属性 5 public $name; // 名字 6 // 基类的构造函数 7 public function __construct($name) { 8 $this->name = $name; 9 } 10 } 11 // 派生类 12 class Person extends Animal // Person类继承了Animal类 13 { 14 public $personSex; // 性别 15 public $personAge; // 年龄 16 // 继承类的构造函数 17 function __construct($personSex, $personAge) { 18 parent::__construct ( “heiyeluren” ); // 使用parent调用了父类的构造函数 19 $this->personSex = $personSex; 20 $this->personAge = $personAge; 21 } 22 function printPerson() { 23 print ($this->name . " is " . $this->personSex . “,this year " . $this->personAge) ; 24 } 25 } 26 // 实例化Person对象 27 $personObject = new Person ( “male”, “21” ); 28 // 执行打印 29 $personObject->printPerson (); // 输出:heiyeluren is male,this year 21 30 ?>上例中,成员属性都是public的,特别是父类的,是为了供继承类通过this来访问。第18行: parent::__construct( “heiyeluren” ),使用了parent来调用父类的构造函数进行对父类的初始化,因为父类的成员都是public的,于是就能够在继承类中直接使用 this来访问从父类继承的属性。八、抽象类与接口有什么区别与联系?抽象类应用的定义如下:abstract class ClassName{}抽象类具有以下特点:1)定义一些方法,子类必须实现父类所有的抽象方法,只有这样,子类才能被实例化,否则子类还是一个抽象类。2)抽象类不能被实例化,它的意义在于被扩展。3)抽象方法不必实现具体的功能,由子类来完成。4)当子类实现抽象类的方法时,这些方法的访问控制可以和父类中的一样,也可以有更高的可见性,但是不能有更低的可见性。例如,某个抽象方法被声明为protected的,那么子类中实现的方法就应该声明为protected或者public的,而不能声明为private。5)如果抽象方法有参数,那么子类的实现也必须有相同的参数个数,必须匹配。但有一个例外:子类可以定义一个可选参数(这个可选参数必须要有默认值),即使父类抽象方法的声明里没有这个参数,两者的声明也无冲突。下面通过一个例子来加深理解:<?php abstract class A{ abstract protected function greet($name); } class B extends A { public function greet($name, $how=“Hello “) { echo $how.$name."\n”; } } $b = new B; $b->greet(“James”); $b->greet(“James”,“Good morning “);?>程序的运行结果为Hello JamesGood morning James定义抽象类时,通常需要遵循以下规则:1)一个类只要含有至少一个抽象方法,就必须声明为抽象类。2)抽象方法不能够含有方法体。接口可以指定某个类必须实现哪些方法,但不需要定义这些方法的具体内容。在PHP中,接口是通过interface关键字来实现的,与定义一个类类似,唯一不同的是接口中定义的方法都是公有的而且方法都没有方法体。接口中所有的方法都是公有的,此外接口中还可以定义常量。接口常量和类常量的使用完全相同,但是不能被子类或子接口所覆盖。要实现一个接口,可以通过关键字implements来完成。实现接口的类中必须实现接口中定义的所有方法。虽然PHP不支持多重继承,但是一个类可以实现多个接口,用逗号来分隔多个接口的名称。下面给出一个接口使用的示例:<?php interface Fruit { const MAX_WEIGHT = 3; //静态常量 function setName($name); function getName(); } class Banana implements Fruit { private $name; function getName() { return $this->name; } function setName($_name) { $this->name = $_name; } } $b = new Banana(); //创建对象 $b->setName(“香蕉”); echo $b->getName(); echo “<br />”; echo Banana::MAX_WEIGHT; //静态常量?>程序的运行结果为香蕉 3接口和抽象类主要有以下区别:抽象类:PHP5支持抽象类和抽象方法。被定义为抽象的类不能被实例化。任何一个类,如果它里面至少有一个方法是被声明为抽象的,那么这个类就必须被声明为抽象的。被定义为抽象的方法只是声明了其调用方法和参数,不能定义其具体的功能实现。抽象类通过关键字abstract来声明。接口:可以指定某个类必须实现哪些方法,但不需要定义这些方法的具体内容。在这种情况下,可以通过interface关键字来定义一个接口,在接口中声明的方法都不能有方法体。二者虽然都是定义了抽象的方法,但是事实上两者区别还是很大的,主要区别如下:1)对接口的实现是通过关键字implements来实现的,而抽象类继承则是使用类继承的关键字extends实现的。2)接口没有数据成员(可以有常量),但是抽象类有数据成员(各种类型的成员变量),抽象类可以实现数据的封装。3)接口没有构造函数,抽象类可以有构造函数。4)接口中的方法都是public类型,而抽象类中的方法可以使用private、protected或public来修饰。5)一个类可以同时实现多个接口,但是只能实现一个抽象类。预告:PHP面试常考内容之面向对象(3)将于本周五(2019.2-15)更新。以上内容摘自《PHP程序员面试笔试宝典》书籍,该书已在天猫、京东、当当等电商平台销售。更多PHP相关的面试知识、考题可以关注公众号获取:琉忆编程库对本文有什么问题或建议都可以进行留言,我将不断完善追求极致,感谢你们的支持。 ...

February 13, 2019 · 5 min · jiezi

假装用某米赛尔号的角度看Python面向对象编程

类和对象下面我们正式创建自己的类, 这里我们使用Python自定义某米赛尔号的精灵, 代码如下:class Elf: def setName(self, name): self.name = name def getName(self): return self.name def getInfo(self): return self 类的定义就像函数定义, 用 class 语句替代了 def 语句, 同样需要执行 class 的整段代码这个类才会生效。进入类定义部分后, 会创建出一个新的局部作用域, 后面定义的类的数据属性和方法都是属于此作用域的局部变量。上面创建的类很简单, 只有一些简单的方法。当捕捉精灵后, 首先要为其起名字, 所以我们先编写函数 setName() 和 getName()。似乎函数中 self 参数有点奇怪, 我们尝试建立具体的对象来探究该参数的作用。>>> x = Elf()>>> y = Elf()>>> x.setName(‘小火猴’)>>> y.setName(‘皮皮’)>>> x.getName()小火猴>>> y.getName()皮皮>>> x.getInfo()<main.Elf instance at 0xXXXXXXXX>>>> y.getInfo()<main.Elf instance at 0xXXXXXXXX>创建对象和调用一个函数很相似, 使用类名作为关键字创建一个类的对象, 实际上, Elf() 的括号里是可以有参数的, 后面我们会讨论到。我们有两只精灵, 一只是小火猴, 一只是皮皮, 并且对他们执行 getName() , 名字正确返回。观察 getInfo() 的输出, 返回的是包含地址的具体对象信息, 可以看到两个对象的地址, 是不一样的。Python 中的self作用和 C++ 中的 this 指针类似, 在调用 精灵对象 的 setName() 和 getName() 函数时, 函数都会自动把该对象的地址作为第一个参数传入(该信息包含在参数 self 中), 这就是为什么我们调用函数时不需要写 self , 而在函数定义时需要把参数作为第一个参数。传入对象地址是相当必要的, 如果不传入地址, 程序就不知道要访问类的哪一个对象。类的每个对象都会有各自的数据属性, Elf 类中有数据属性 name, 这是通过setName() 函数中的语句 self.name = name创建的。这个语句中的两个 name 是不一样的, 它们的作用域不一样。第一个 name 通过 self 语句声明的作用域是类 Elf() 的作用域, 将其作为对象 x 的数据属性进行存储, 而后面的 name 的作用域是函数的局部作用域, 与参数中的 name 相同。而后面 getName() 函数返回的是对象中的 name。init()方法从更深层逻辑去说, 我们捕捉到精灵的那一刻应该就有名字, 而并非捕捉后去设置。所以这里我们需要的是一个初始化手段。Python中的__init__() 方法用于初始化类的实例对象。init() 函数的作用一定程度上与C++的构造函数相似, 但并不等于。C++ 的构造函数是使用该函数去创建一个类的实例对象, 而Python执行__init__() 方法时实例对象已被构造出来。init()方法会在对象构造出来后自动执行, 所以可以用于初始化我们所需要的数据属性。修改Elf 类的代码, 代码如下:class Elf: def init(self, name, gender, level): self.type = (‘fire’, None) self.gender = gender self.name = name self.level = level self.status = [10+2level, 5+1level, 5+1level, 5+1level, 5+1level, 5+1level] # 精灵体力, 攻击, 防御, 特攻, 特防, 速度 def getName(self): return self.name def getGender(self): return self.gender def getType(self): return self.type def getStatus(self): return self.status在此处我们增加了几个数据的属性: 性别、等级、能力和精灵属性。连同前面的名字, 都放在__init__()方法进行初始化。数据属性是可以使用任意数据类型的, 小火猴属性是火, 而精灵可能会有俩个属性, 假设小火猴经过两次进化称为烈焰猩猩属性为地面, 火系。为了保持数据类型的一致性, 所以我们使用元组存储, 并让小火猴的第二个属性为 None。由于小火猴的属性是固定的, 所以在__init__() 的输入参数不需要 type。而精灵的能力会随着等级的不同而不同, 所以在初始化中也需要实现这一点。我们创建实例对象测试代码:>>> x = Elf(‘小火猴’, ‘male’, 5)>>> y = Elf(‘皮皮’, ‘female’, 6) # 这里有错误, 皮皮不是火系, 可以接着往下看, 思考如何改正>>> print( x.getName(), x.getGender(), x.getStatus() )小火猴 male [20, 10, 10, 10, 10, 10]>>> print( y.getName(), y.getGender(), y.getStatus() )皮皮 female [22, 11, 11, 11, 11, 11]这时候创建对象就需要参数了, 实际上这是__init__() 函数的参数。init() 自动将数据属性进行了初始化, 然后调用相关函数能够返回我们需要的对象的数据属性。对象的方法1.方法引用类的方法和对象的方法是一样的, 我们在定义类的方时程序没有为类的方法分配内存, 而在创建具体实例对象的程序才会为对象的每个数据属性和方法分配内存, 我们已经知道定义类的方法是 def 定义的, 具体定义格式与普通函数相似, 只不过类的方法的第一个参数要为 self 参数。我们可以用普通函数实现对对象函数的引用:>>> x = Elf(‘小火猴’, ‘male’, 5)>>> getStatus1 = x.getStatus>>> getStatus1()[20, 10, 10, 10, 10, 10]虽然看上去似乎是调用了一个普通函数, 但是 getStatus1() 这个函数是引用 x.getStatus() 的, 意味着程序还是隐性地加入了 self 参数。2.私有化先敲代码:>>> x.type(‘fire’, None)>>> x.getType()(‘fire’, None)虽然这样似乎很方便, 但违反了类的封装原则。对象的状态对于类外部应该是不可以访问的。为何要这样做, 我们查看Python 的模块源码时会发现源码里定义了很多类, 模块中算法通过使用类是很常见的, 如果我们使用算法时能随意访问对象中的数据属性, 那么很有可能在不经意间修改算法中已经调好的参数, 这是十分尴尬的。尽管我们不会可以那么去做, 但这种无意的改动是常有的事。一般封装好的类都会有足够的函数接口供程序员用, 程序员没有必要访问对象的具体数据类型。为防止程序员无意间修改了对象的状态, 我们需要对类的数据属性和方法进行私有化。Python 不支持直接私有方式, 但可以使用一些小技巧达到私有特性的目的。为了让方法的数据属性或方法变为私有, 只需要在它的名字前面加上双下划线即可, 修改Elf 类代码:# 自定义类class Elf: def init(self, name, gender, level): self.__type = (‘fire’, None) self.__gender = gender self.__name = name self.__level = level self.__status = [10+2level, 5+1level, 5+1level, 5+1level, 5+1level, 5+1level] # 精灵体力, 攻击, 防御, 特攻, 特防, 速度 def getName(self): return self.__name def getGender(self): return self.__gender def getType(self): return self.__type def getStatus(self): return self.__status def level_up(self): self.__status = [s+1 for s in self.__status] self.__status[0] += 1 # HP每级增加2点, 其余增加1点 def __test(self): pass>>> x = Elf(‘小火猴’, ‘male’, 5)>>> print(x.type)Traceback (most recent call last): File “seer.py”, line 25, in <module> print(x.type)AttributeError: ‘Elf’ object has no attribute ’type’>>> print(x.getName())小火猴>>> x.test()Traceback (most recent call last): File “ser.py”, line 28, in <module> x.test()AttributeError: ‘Elf’ object has no attribute ’test’现在在程序外部直接访问私有数据是不允许的, 我们只能通过设定好的节后函数去调取对象信息。不过通过双下划綫实现的私有实际上是"伪私有化", 实际上我们还是可以做到从外部访问这些私有属性。>>> print(x.Elf__type)(‘fire’, None)Python 使用的是一种 name_mangling 技术, 将__membername 替换成 class__membername, 在外部使用原来的私有成员时, 会提示无法找到, 而上面执行的 x.Elf__type 是可以访问。简而言之, 确保其他人无法访问对象的方法和数据属性是不可能的, 但是使用这种 name_mangling 技术是一种程序员不应该从外部访问这些私有成员的强有力信号。可以看到代码中还增加了一个函数level_up(), 这个函数用于处理精灵升级是能力的提升, 我们不应该在外部修改 x 对象的 status , 所以应准备好接口去处理能力发生变化的情景, 函数 level_up() 仅仅是一个简单的例子, 而据说在工业代码中, 这样的函数接口是大量的, 程序需要对它们进行归类并附上相应的文档说明。3.迭代器Python容器对象(列表、元组、字典和字符串等)都可以可以用 for 遍历,for element in [1, 2, 3]: print(element)这种风格十分简洁, for 语句在容器对象上调用了 iter(), 该函数返回一个定义了 next() 方法的迭代器对象, 它在容器中逐一访问元素。当容器遍历完毕, next() 找不到后续元素时, next() 找不到后续元素时, next()会引发一个 StopIteration 异常, 告知for循环终止。>>> L = [1, 2, 3]>>> it = iter(L)>>> it<list_iterator object at 0x0302C530>>>> it.next()1>>> it.next()2>>> it.next()2>>> it.next()3>>> it.next()Traceback (most recent call last): File “<stdin>”, line 1, in <module>StopIteration当知道迭代器协议背后的机制后, 我们便可以吧迭代器加入到自己的类中。我们需要定义一个__iter()方法, 它返回一个有 next() 方法的对象, 如果类定义了next(), iter()可以只返回self, 再次修改类 Elf 的代码, 通过迭代器能输出对象的全部信息。class Elf: def init(self, name, gender, level): self.__type = (‘fire’, None) self.__gender = gender self.__name = name self.__level = level self.__status = [10+2level, 5+1level, 5+1level, 5+1level, 5+1level, 5+1level] self.__info = [self.__name, self.__type, self.__gender, self.__level, self.__status] self.__index = -1 # 精灵体力, 攻击, 防御, 特攻, 特防, 速度 def getName(self): return self.__name def getGender(self): return self.__gender def getType(self): return self.__type def getStatus(self): return self.__status def level_up(self): self.__status = [s+1 for s in self.__status] self.__status[0] += 1 def iter(self): print(‘名字 属性 性别 等级 能力’) return self def next(self): if self.__index == len(self.__info) - 1: raise StopIteration self.__index += 1 return self.info[self.index]继承面向对象编程的好处之一就是代码的复用, 实现这种重用的方法之一就是通过继承机制。继承是两个类或多个类之间的父子关系, 子类继承了基类的所有公有数据属性和方法, 并且可以通过编写子类的代码扩充子类的功能。可以说, 如果人类可以做到儿女继承了父母的所有才学并加以拓展, 那么人类的发展至少是现在的数万倍。继承实现了数据属性和方法的重用, 减少了代码的冗余度。那么我们如何实现继承呢??如果我们需要的类中具有公共的成员, 且具有一定的递进关系, 那么就可以使用继承, 且让结构最简单的类作为基类。一般来说, 子类是父类的特殊化, 如下关系: 哺乳类动物 ————> 猫科动物 ————> 东北虎东北虎类 继承 猫科动物类, 猫科动物类 继承 哺乳动物类, 猫科动物类编写了所有猫科动物公有的行为的方法而特定猫类则增加了该猫科动物特有的行为。不过继承也有一定弊端, 可能基类对于子类也有一定特殊的地方, 如果某种特定猫科动物不具有绝大多数猫科动物的行为, 当程序员吗,没有理清类之间的关系是, 可能使子类具有不该有的方法。另外, 如果继承链太长的话, 任何一点小的变化都会引起一连串变化, 我们使用的继承要注意控制继承链的规模。继承语法: class 子类名(基类名1, 基类名2,…), 基类卸载括号里, 如果有多个基类, 则全部写在括号里, 这种情况称为多继承。在Python中继承有以下一些特点:1) 在继承中积累初始化方法__init()函数不会被自动调用。如果希望子类调用基类的__init() 方法, 需要在子类的 init() 方法中显示调用它。这与C++差别很大。2) 在调用基类的方法时, 需要加上基类的类名前缀, 且带上 self 参数变量, 注意在类中调用该类中定义的方法时不需要self参数。3) Python总是首先查找对应类的方法, 如果在子类中没有对应的方法, Python才会在继承链的基类中按顺序查找。4) 在Python继承中, 子类不能访问基类的私有成员。这是最后一次修改类Elf的代码:class pokemon: def init(self, name, gender, level, type, status): self.__type = type self.__gender = gender self.__name = name self.__level = level self.__status = status self.__info = [self.__name, self.__type, self.__gender, self.__level, self.__status] self.__index = -1 # 精灵体力, 攻击, 防御, 特攻, 特防, 速度 def getName(self): return self.__name def getGender(self): return self.__gender def getType(self): return self.__type def getStatus(self): return self.__status def level_up(self): self.__status = [s+1 for s in self.__status] self.__status[0] += 1 def iter(self): print(‘名字 属性 性别 等级 能力’) return self def next(self): if self.__index == len(self.__info) - 1: raise StopIteration self.__index += 1 return self.__info[self.__index] class Elf(pokemon): def init(self, name, gender, level): self.__type = (‘fire’, None) self.__gender = gender self.__name = name self.__level = level self.__status = [10+2level, 5+1level, 5+1level, 5+1level, 5+1level, 5+1*level] pokemon.init(self, self.__name, self.__gender, self.__level, self.__type, self.status)>>> x = Elf(‘小火猴’, ‘male’, 5)>>> print(x.getGender())male>>> for info in x: print(info)小火猴 (‘fire’, None) male 5 [20, 10, 10, 10, 10, 10] 我们定义了Elf 类的基类pokemon, 将精灵共有的行为都放到基类中, 子类仅仅需要向基类传输数据属性即可。这样就可以很轻松地定义其他基于pokemon类的子类。因为某米赛尔号精灵有数千只, 使用继承的方法可以大大减少代码量, 且当需要对全部精灵进行整体改变时仅需改变pokemanl类的__init()即可, 并向基类传输数据, 这里注意要加self参数, Elf 类没有继承基类的私有数据属性, 因此在子类只有一个self.__type, 不会出现因继承所造成的重名情况。为了能更加清晰地描述这个问题, 这里再举一个例子:class animal: def init(self): self.__age = age def print2(self): pritn(self.age)class dog(animal): def__init(self, age): animal.init(self, age) def print2(self): print(self.__age)>>> a_animal = animal(10)>>> a_animal.print2()10>>> a_dog = dog(10)>>> a_dog.print2()Traceback (most recent call last): File “seer.py”, line 13, in <module> a_dog.print2() File “seer.py”, line 11, in print2 print(self.__age)AttributeError: ‘dog’ object has no attribute ‘_dog__age’That’s all ! ...

January 22, 2019 · 4 min · jiezi

JavaScript 进阶知识 - 高级篇

JS高级前言经过前面几篇文章的学习,相信大家已经对js有了大部分的理解了,但是要想真正的掌握好js,本篇才是关键。由于js高级阶段的知识点比较难理解,所以本篇文章花了大量的时间去理思路,有可能有一些知识点遗漏了,也有肯能有部分知识点写的不对,欢迎大家留言纠正。另外,大家在以后的学习中千万不要被一些难点所吓到,听说有些知识点很难,其实并不是真正的难,只要你静下心慢慢的理解,其实还是很简单的。1.异常处理常见的异常分类运行环境的多样性导致的异常(浏览器)语法错误,代码错误异常最大的特征,就是一旦代码出现异常,后面的代码就不会执行。1.1异常捕获捕获异常,使用try-catch语句:try{ // 这里写可能出现异常的代码}catch(e){ // e-捕获的异常对象 // 可以在此处书写出现异常后的处理代码}异常捕获语句执行的过程为:代码正常运行, 如果在try中出现了错误,try里面出现错误的语句后面的代码都不再执行, 直接跳转到catch中catch中处理错误信息然后继续执行后面的代码如果try中没有出现错误, 那么不走catch直接执行后面的代码通过try-catch语句进行异常捕获之后,代码将会继续执行,而不会中断。示例代码:console.log(‘代码开始执行’);try{ console.log(num); // num 在外部是没有定义的}catch(e){ console.log(e); console.log(‘我已经把错误处理了’);}console.log(‘代码结束执行’);效果图:从效果图中我们可以看到,num是一个没有定义的变量,如果没有放在try-catch代码块中,后面的‘代码结束执行’就不会被打印。通过把try-catch放在代码块中,出现错误后,就不会影响后面代码的运行了,他会把错误信息打印出来。注意:语法错误异常用try-catch语句无法捕获,因为在预解析阶段,语法错误会直接检测出来,而不会等到运行的时候才报错。try-catch在一般日常开发中基本用不到,但是如果要写框架什么的,用的会非常多。因为这个会让框架变得健壮异常捕获语句的完整模式异常捕获语句的完整模式为try-catch-finallytry { //可能出现错误的代码} catch ( e ) { //如果出现错误就执行} finally { //结束 try 这个代码块之前执行, 即最后执行}finally中的代码,不管有没有发生异常,都会执行。一般用在后端语言中,用来释放资源,JavaScript中很少会用到1.2抛出异常如何手动的抛出异常呢?案例:自己写的一个函数,需要一个参数,如果用户不传参数,此时想直接给用户抛出异常,就需要了解如何抛出异常。抛出异常使用throw关键字,语法如下:throw 异常对象;异常对象一般是用new Error(“异常消息”), 也可以使用任意对象示例代码:function test(para){ if(para == undefined){ throw new Error(“请传递参数”); //这里也可以使用自定义的对象 throw {“id”:1, msg:“参数未传递”}; }}try{ test();}catch(e){ console.log(e);}效果图:1.3异常的传递机制function f1 () { f2(); }function f2 () { f3();}function f3() { throw new Error( ’error’ );}f1(); // f1 称为调用者, 或主调函数, f2 称为被调用者, 或被调函数当在被调函数内发生异常的时候,异常会一级一级往上抛出。2.面向对象编程在了解面向对象编程之前,我们先来了解下什么是面向过程,什么是面向对象,他们之间的区别是什么。2.1 面向过程和面向对象的的对比举个例子:日常洗衣服1.面向过程的思维方式:面向过程编程:将解决问题的关注点放在解决问题的具体细节上,关注如何一步一步实现代码细节;step 1:收拾脏衣服step 2:打开洗衣机盖step 3:将脏衣服放进去step 4:设定洗衣程序step 5:开始洗衣服step 6:打开洗衣机盖子step 7:晒衣服2.面向对象的思维方式:面向对象编程:将解决问题的关注点放在解决问题所需的对象上,我们重点找对象;人(对象)洗衣机(对象)在面向对象的思维方式中:我们只关心要完成事情需要的对象,面向对象其实就是对面向过程的封装;示例代码:在页面上动态创建一个元素//面向过程//1-创建一个divvar div=document.createElement(‘div’);//2-div设置内容div.innerHTML=‘我是div’;//3-添加到页面中document.body.appendChild(div);//面向对象$(‘body’).append(’<div>我也是div</div>’);我们可以看出,jQ封装的其实就是对面向过程的封装。总结: 面向对象是一种解决问题的思路,一种编程思想。2.2 面向对象编程举例设置页面中的div和p的边框为'1px solid red'1、传统的处理办法// 1> 获取div标签var divs = document.getElementsByTagName( ‘div’ );// 2> 遍历获取到的div标签for(var i = 0; i < divs.length; i++) { //3> 获取到每一个div元素,设置div的样式 divs[i].style.border = “1px dotted black”;}// 4> 获取p标签var ps = document.getElementsByTagName(“p”);// 5> 遍历获取到的p标签for(var j = 0; j < ps.length; j++) { // 获取到每一个p元素 设置p标签的样式 ps[j].style.border = “1px dotted black”; }2、使用函数进行封装优化// 通过标签名字来获取页面中的元素 function tag(tagName) { return document.getElementsByTagName(tagName); }// 封装一个设置样式的函数 function setStyle(arr) { for(var i = 0; i < arr.length; i++) { // 获取到每一个div或者p元素 arr[i].style.border = “1px solid #abc”; } }var dvs = tag(“div”);var ps = tag(“p”);setStyle(dvs); setStyle(ps);3、使用面向对象的方式// 更好的做法:是将功能相近的代码放到一起 var obj = { // 命名空间 getEle: { tag: function (tagName) { return document.getElementsByTagName(tagName); }, id: function (idName) { return document.getElementById(idName); } // … }, setCss: { setStyle: function (arr) { for(var i = 0; i < arr.length; i++) { arr[i].style.border = “1px solid #abc”; } }, css: function() {}, addClass: function() {}, removeClass: function() {} // … } // 属性操作模块 // 动画模块 // 事件模块 // … };var divs = obj.getEle.tag(‘div’);obj.setCss.setStyle(divs);2.3 面向对象的三大特性面向对象的三大特性分别是:‘封装’,‘继承’,‘多态’。1、封装性对象就是对属性和方法的封装,要实现一个功能,对外暴露一些接口,调用者只需通过接口调用即可,不需要关注接口内部实现原理。js对象就是“键值对”的集合键值如果是数据( 基本数据, 复合数据, 空数据 ), 就称为属性如果键值是函数, 那么就称为方法对象就是将属性与方法封装起来方法是将过程封装起来2、继承性所谓继承就是自己没有, 别人有,拿过来为自己所用, 并成为自己的东西2.1、传统继承基于模板子类可以使用从父类继承的属性和方法。class Person { string name; int age;}class Student : Person {}var stu = new Student();stu.name即:让某个类型的对象获得另一个类型的对象的属性的方法2.2、js 继承基于对象在JavaScript中,继承就是当前对象可以使用其他对象的方法和属性。js继承实现举例:混入(mix)// 参数o1和o2是两个对象,其中o1对象继承了所有o2对象的“k”属性或者方法var o1 = {};var o2 = { name: ‘Levi’, age: 18, gender: ‘male’};function mix ( o1, o2 ) { for ( var k in o2 ) { o1[ k ] = o2[ k ]; }}mix(o1, o2);console.log(o1.name); // “Levi"3、多态性(基于强类型,js中没有多态)只做了解同一个类型的变量可以表现出不同形态,用父类的变量指向子类的对象。动物 animal = new 子类(); // 子类:麻雀、狗、猫、猪、狐狸…动物 animal = new 狗();animal.叫();2.4 创建对象的方式1、字面量 {}var student1 = { name:‘诸葛亮’, score:100, code:1,}var student2 = { name:‘蔡文姬’, score:98, code:2,}var student3 = { name:‘张飞’, score:68, code:3,}字面量创建方式,代码复用性太低,每一次都需要重新创建一个对象。2、Object()构造函数var student1 = new Object(); student1.name = ‘诸葛亮’; student1.score = 100; student1.code = 1;var student2 = new Object(); student2.name = ‘蔡文姬’; student2.score = 98; student2.code = 2; var student3 = new Object(); student3.name = ‘张飞’; student3.score = 68; student3.code = 3;代码复用性太低,字面量创建的方式其实就是代替Object()构造函数创建方式的。3、自定义构造函数自定义构造函数,可以快速创建多个对象,并且代码复用性高。// 一般为了区分构造函数与普通函数,构造函数名首字母大写function Student(name,score,code){ this.name = name; this.score = score; this.code = code;}var stu1 = new Student(‘诸葛亮’,100,1);var stu2 = new Student(‘蔡文姬’,98,2);var stu3 = new Student(‘张飞’,68,3);构造函数语法:构造函数名首字母大写;构造函数一般与关键字:new一起使用;构造函数一般不需要设置return语句,默认返回的是新创建的对象;this指向的是新创建的对象。构造函数的执行过程:new关键字,创建一个新的对象,会在内存中开辟一个新的储存空间;让构造函数中的this指向新创建的对象;执行构造函数,给新创建的对象进行初始化(赋值);构造函数执行(初始化)完成,会将新创建的对象返回。构造函数的注意点:构造函数本身也是函数;构造函数有返回值,默认返回的是新创建的对象;但是如果手动添加返回值,添加的是值类型数据的时候,构造函数没有影响。如果添加的是引用类型(数组、对象等)值的时候,会替换掉新创建的对象。function Dog(){ this.name=“哈士奇”; this.age=0.5; this.watch=function(){ console.log(‘汪汪汪,禁止入内’); } // return false; 返回值不会改变,还是新创建的对象 // return 123; 返回值不会改变,还是新创建的对象 // return [1,2,3,4,5]; 返回值发生改变,返回的是这个数组 return {aaa:‘bbbb’}; // 返回值发生改变,返回的是这个对象}var d1=new Dog(); // 新创建一个对象console.log(d1);构造函数可以当做普通函数执行,里面的this指向的是全局对象window。 function Dog(){ this.name=“husky”; this.age=0.5; this.watch=function(){ console.log(‘汪汪汪,禁止入内’); } console.log(this); // window对象 return 1;}console.log(Dog()); // 打印 12.5 面向对象案例通过一个案例,我们来了解下面向对象编程(案例中有一个prototype概念,可以学完原型那一章后再来看这个案例)。需求:实现一个MP3音乐管理案例;同种类型的MP3,厂家会生产出成百上千个,但是每个MP3都有各自的样式、使用者、歌曲;每个MP3都有一样的播放、暂停、增删歌曲的功能(方法);图解:示例代码: // 每个MP3都有自己的 主人:owner 样式:color 歌曲:list function MP3(name,color,list){ this.owner = name || ‘Levi’; // 不传值时默认使用者是‘Levi’ this.color = color || ‘pink’; this.musicList = list || [ {songName:‘男人哭吧不是罪’,singer:‘刘德华’}, {songName:‘吻别’,singer:‘张学友’}, {songName:‘对你爱不完’,singer:‘郭富城’}, {songName:‘今夜你会不会来’,singer:‘黎明’} ]; } // 所有的MP3都有 播放 暂停 音乐 增删改查的功能 MP3.prototype = { // 新增 add:function(songName,singer){ this.musicList.push({songName:songName,singer:singer}); }, // 查找 select:function(songName){ for(var i=0;i<this.musicList.length;i++){ if(this.musicList[i].songName == songName){ return this.musicList[i]; } } return null; // 如果没有搜索到返回null }, // 修改 update:function(songName,singer){ // 先找到这首歌 在修改 var result = this.select(songName); // 查找 if(result){ result.singer = singer; // 修改 } }, // 删除 delete:function(songName){ // 先找到音乐 splice(index,1) var result = this.select(songName); // 知道该音乐的索引值 // 删除 if(result){ var index = this.musicList.indexOf(result); this.musicList.splice(index,1); // 从指定索引值来删除数据 } }, // 显示 show:function(){ console.log(this.owner+‘的MP3’); for(var i=0;i<this.musicList.length;i++){ console.log(this.musicList[i].songName +’—’+this.musicList[i].singer); } } } var XiaoHong = new MP3(‘小红’); // 实例小红MP3 var XiaoMing = new MP3(‘小明’); // 实例小明MP3 var XiaoDong = new MP3(‘小东’); // 实例小东MP3 XiaoHong.add(‘十年’,‘陈奕迅’); // 小红的歌单里添加歌曲 XiaoDong.add(‘月亮之上’,‘凤凰传奇’); // 小东的歌单里添加歌曲 XiaoMing.musicList = [ // 小明的歌单替换 { songName:‘精忠报国’, singer:‘屠洪刚’ }, { songName:‘窗外’, singer:‘未知’ } ]; // 展示各自的歌单 XiaoHong.show(); XiaoMing.show(); XiaoDong.show();打印结果:3.原型3.1 传统构造函数存在问题通过自定义构造函数的方式,创建小狗对象:两个实例化出来的“小狗”,它们都用的同一个say方法,为什么最后是false呢?function Dog(name, age) { this.name = name; this.age = age; this.say = function() { console.log(‘汪汪汪’); }}var dog1 = new Dog(‘哈士奇’, 1.5);var dog2 = new Dog(‘大黄狗’, 0.5);console.log(dog1);console.log(dog2);console.log(dog1.say == dog2.say); //输出结果为false画个图理解下:每次创建一个对象的时候,都会开辟一个新的空间,我们从上图可以看出,每只创建的小狗有一个say方法,这个方法都是独立的,但是功能完全相同。随着创建小狗的数量增多,造成内存的浪费就更多,这就是我们需要解决的问题。为了避免内存的浪费,我们想要的其实是下图的效果:解决方法:这里最好的办法就是将函数体放在构造函数之外,在构造函数中只需要引用该函数即可。function sayFn() { console.log(‘汪汪汪’);}function Dog(name, age) { this.name = name; this.age = age; this.say = sayFn();}var dog1 = new Dog(‘哈士奇’, 1.5);var dog2 = new Dog(‘大黄狗’, 0.5);console.log(dog1);console.log(dog2);console.log(dog1.say == dog2.say); //输出结果为 true这样写依然存在问题:全局变量增多,会增加引入框架命名冲突的风险代码结构混乱,会变得难以维护想要解决上面的问题就需要用到构造函数的原型概念。3.2 原型的概念prototype:原型。每个构造函数在创建出来的时候系统会自动给这个构造函数创建并且关联一个空的对象。这个空的对象,就叫做原型。关键点:每一个由构造函数创建出来的对象,都会默认的和构造函数的原型关联;当使用一个方法进行属性或者方法访问的时候,会先在当前对象内查找该属性和方法,如果当前对象内未找到,就会去跟它关联的原型对象内进行查找;也就是说,在原型中定义的方法跟属性,会被这个构造函数创建出来的对象所共享;访问原型的方式:构造函数名.prototype。示例图:示例代码: 给构造函数的原型添加方法function Dog(name,age){ this.name = name; this.age = age;}// 给构造函数的原型 添加say方法Dog.prototype.say = function(){ console.log(‘汪汪汪’);}var dog1 = new Dog(‘哈士奇’, 1.5);var dog2 = new Dog(‘大黄狗’, 0.5);dog1.say(); // 汪汪汪dog2.say(); // 汪汪汪我们可以看到,本身Dog这个构造函数中是没有say这个方法的,我们通过Dog.prototype.say的方式,在构造函数Dog的原型中创建了一个方法,实例化出来的dog1、dog2会先在自己的对象先找say方法,找不到的时候,会去他们的原型对象中查找。如图所示:在构造函数的原型中可以存放所有对象共享的数据,这样可以避免多次创建对象浪费内存空间的问题。3.3 原型的使用1、使用对象的动态特性使用对象的动态属性,其实就是直接使用prototype为原型添加属性或者方法。function Person () {}Person.prototype.say = function () { console.log( ‘讲了一句话’ );};Person.prototype.age = 18;var p = new Person();p.say(); // 讲了一句话console.log(p.age); // 182、直接替换原型对象每次构造函数创建出来的时候,都会关联一个空对象,我们可以用一个对象替换掉这个空对象。function Person () {}Person.prototype = { say : function () { console.log( ‘讲了一句话’ ); },};var p = new Person();p.say(); // 讲了一句话注意:使用原型的时候,有几个注意点需要注意一下,我们通过几个案例来了解一下。使用对象.属性名去获取对象属性的时候,会先在自身中进行查找,如果没有,就去原型中查找;// 创建一个英雄的构造函数 它有自己的 name 和 age 属性function Hero(){ this.name=“德玛西亚之力”; this.age=18;}// 给这个构造函数的原型对象添加方法和属性Hero.prototype.age= 30;Hero.prototype.say=function(){ console.log(‘人在塔在!!!’);}var h1 = new Hero();h1.say(); // 先去自身中找 say 方法,没有再去原型中查找 打印:‘人在塔在!!!‘console.log(p1.name); // “德玛西亚之力"console.log(p1.age); // 18 先去自身中找 age 属性,有的话就不去原型中找了使用对象.属性名去设置对象属性的时候,只会在自身进行查找,如果有,就修改,如果没有,就添加;// 创建一个英雄的构造函数function Hero(){ this.name=“德玛西亚之力”;}// 给这个构造函数的原型对象添加方法和属性Hero.prototype.age = 18;var h1 = new Hero();console.log(h1); // {name:“德玛西亚之力”}console.log(h1.age); // 18h1.age = 30; // 设置的时候只会在自身中操作,如果有,就修改,如果没有,就添加 不会去原型中操作console.log(h1); // {name:“德玛西亚之力”,age:30}console.log(h1.age); // 30一般情况下,不会将属性放在原型中,只会将方法放在原型中;在替换原型的时候,替换之前创建的对象,和替换之后创建的对象的原型不一致!!!// 创建一个英雄的构造函数 它有自己的 name 属性function Hero(){ this.name=“德玛西亚之力”;}// 给这个构造函数的默认原型对象添加 say 方法Hero.prototype.say = function(){ console.log(‘人在塔在!!!’);}var h1 = new Hero();console.log(h1); // {name:“德玛西亚之力”}h1.say(); // ‘人在塔在!!!’// 开辟一个命名空间 obj,里面有个 kill 方法var obj = { kill : function(){ console.log(‘大宝剑’); }}// 将创建的 obj 对象替换原本的原型对象Hero.prototype = obj;var h2 = new Hero();h1.say(); // ‘人在塔在!!!‘h2.say(); // 报错h1.kill(); // 报错h2.kill(); // ‘大宝剑’画个图理解下:图中可以看出,实例出来的h1对象指向的原型中,只有say()方法,并没有kill()方法,所以h1.kill()会报错。同理,h2.say()也会报错。3.4 __proto__属性在js中以_开头的属性名为js的私有属性,以__开头的属性名为非标准属性。__proto__是一个非标准属性,最早由firefox提出来。1、构造函数的 prototype 属性之前我们访问构造函数原型对象的时候,使用的是prototype属性:function Person(){}//通过构造函数的原型属性prototype可以直接访问原型Person.prototype;在之前我们是无法通过构造函数new出来的对象访问原型的:function Person(){}var p = new Person();//以前不能直接通过p来访问原型对象2、实例对象的 proto 属性__proto__属性最早是火狐浏览器引入的,用以通过实例对象来访问原型,这个属性在早期是非标准的属性,有了__proto__属性,就可以通过构造函数创建出来的对象直接访问原型。function Person(){}var p = new Person();//实例对象的__proto__属性可以方便的访问到原型对象p.proto;//既然使用构造函数的prototype和实例对象的__proto__属性都可以访问原型对象//就有如下结论p.proto === Person.prototype;如图所示:3、__proto__属性的用途可以用来访问原型;在实际开发中除非有特殊的需求,不要轻易的使用实例对象的__proto__属性去修改原型的属性或方法;在调试过程中,可以轻易的查看原型的成员;由于兼容性问题,不推荐使用。3.5 constuctor属性constructor:构造函数,原型的constructor属性指向的是和原型关联的构造函数。示例代码:function Dog(){ this.name=“husky”;}var d=new Dog();// 获取构造函数console.log(Dog.prototype.constructor); // 打印构造函数 Dogconsole.log(d.proto.constructor); // 打印构造函数 Dog如图所示:获取复杂类型的数据类型:通过obj.constructor.name的方式,获取当前对象obj的数据类型。在一个的函数中,有个返回值name,它表示的是当前函数的函数名;function Teacher(name,age){ this.name = name; this.age = age;}var teacher = new Teacher();// 假使我们只知道一个对象teacher,如何获取它的类型呢?console.log(teacher.proto.constructor.name); // Teacherconsole.log(teacher.constructor.name); // Teacher实例化出来的teacher对象,它的数据类型是啥呢?我们可以通过实例对象teacher.proto,访问到它的原型对象,再通过.constructor访问它的构造函数,通过.name获取当前函数的函数名,所以就能得到当前对象的数据类型。又因为.__proto__是一个非标准的属性,而且实例出的对象继承原型对象的方法,所以直接可以写成:obj.constructor.name。3.6 原型继承原型继承:每一个构造函数都有prototype原型属性,通过构造函数创建出来的对象都继承自该原型属性。所以可以通过更改构造函数的原型属性来实现继承。继承的方式有多种,可以一个对象继承另一个对象,也可以通过原型继承的方式进行继承。1、简单混入继承直接遍历一个对象,将所有的属性和方法加到另一对象上。var animal = { name:“Animal”, sex:“male”, age:5, bark:function(){ console.log(“Animal bark”); }};var dog = {};for (var k in animal){ dog[k]= animal[k];}console.log(dog); // 打印的对象与animal一模一样缺点:只能一个对象继承自另一个对象,代码复用太低了。2、混入式原型继承混入式原型继承其实与上面的方法类似,只不过是将遍历的对象添加到构造函数的原型上。var obj={ name:‘zs’, age:19, sex:‘male’ }function Person(){ this.weight=50;}for(var k in obj){ // 将obj里面的所有属性添加到 构造函数 Person 的原型中 Person.prototype[k] = obj[k];}var p1=new Person();var p2=new Person();var p3=new Person();console.log(p1.name); // ‘zs’console.log(p2.age); // 19console.log(p3.sex); // ‘male’面向对象思想封装一个原型继承我们可以利用面向对象的思想,将面向过程进行封装。function Dog(){ this.type = ‘yellow Dog’;}// 给构造函数 Dog 添加一个方法 extendDog.prototype.extend = function(obj){ // 使用混入式原型继承,给 Dog 构造函数的原型继承 obj 的属性和方法 for (var k in obj){ this[k]=obj[k]; }}// 调用 extend 方法Dog.prototype.extend({ name:“二哈”, age:“1.5”, sex:“公”, bark:function(){ console.log(‘汪汪汪’); }});3、替换式原型继承替换式原型继承,在上面已经举过例子了,其实就是将一个构造函数的原型对象替换成另一个对象。function Person(){ this.weight=50;}var obj={ name:‘zs’, age:19, sex:‘male’}// 将一个构造函数的原型对象替换成另一个对象Person.prototype = obj;var p1=new Person();var p2=new Person();var p3=new Person();console.log(p1.name); // ‘zs’console.log(p2.age); // 19console.log(p3.sex); // ‘male’之前我们就说过,这样做会产生一个问题,就是替换的对象会重新开辟一个新的空间。替换式原型继承时的bug替换原型对象的方式会导致原型的constructor的丢失,constructor属性是默认原型对象指向构造函数的,就算是替换了默认原型对象,这个属性依旧是默认原型对象指向构造函数的,所以新的原型对象是没有这个属性的。解决方法:手动关联一个constructor属性function Person() { this.weight = 50;}var obj = { name: ‘zs’, age: 19, sex: ‘male’}// 在替换原型对象函数之前 给需要替换的对象添加一个 constructor 属性 指向原本的构造函数obj.constructor = Person;// 将一个构造函数的原型对象替换成另一个对象Person.prototype = obj;var p1 = new Person();console.log(p1.proto.constructor === Person); // true4、Object.create()方法实现原型继承当我们想把对象1作为对象2的原型的时候,就可以实现对象2继承对象1。前面我们了解了一个属性:proto,实例出来的对象可以通过这个属性访问到它的原型,但是这个属性只适合开发调试时使用,并不能直接去替换原型对象。所以这里介绍一个新的方法:Object.create()。语法: var obj1 = Object.create(原型对象);示例代码: 让空对象obj1继承对象obj的属性和方法var obj = { name : ‘盖伦’, age : 25, skill : function(){ console.log(‘大宝剑’); }}// 这个方法会帮我们创建一个原型是 obj 的对象var obj1 = Object.create(obj);console.log(obj1.name); // “盖伦"obj1.skill(); // “大宝剑"兼容性:由于这个属性是ECMAScript5的时候提出来的,所以存在兼容性问题。利用浏览器的能力检测,如果存在Object.create则使用,如果不存在的话,就创建构造函数来实现原型继承。// 封装一个能力检测函数function create(obj){ // 判断,如果浏览器有 Object.create 方法的时候 if(Object.create){ return Object.create(obj); }else{ // 创建构造函数 Fun function Fun(){}; Fun.prototype = obj; return new Fun(); }}var hero = { name: ‘盖伦’, age: 25, skill: function () { console.log(‘大宝剑’); }}var hero1 = create(hero);console.log(hero1.name); // “盖伦"console.log(hero1.proto == hero); // true4.原型链对象有原型,原型本身又是一个对象,所以原型也有原型,这样就会形成一个链式结构的原型链。4.1 什么是原型链示例代码: 原型继承练习// 创建一个 Animal 构造函数function Animal() { this.weight = 50; this.eat = function() { console.log(‘蜂蜜蜂蜜’); }}// 实例化一个 animal 对象var animal = new Animal();// 创建一个 Preson 构造函数function Person() { this.name = ‘zs’; this.tool = function() { console.log(‘菜刀’); }}// 让 Person 继承 animal (替换原型对象)Person.prototype = animal;// 实例化一个 p 对象 var p = new Person();// 创建一个 Student 构造函数function Student() { this.score = 100; this.clickCode = function() { console.log(‘啪啪啪’); }}// 让 Student 继承 p (替换原型对象)Student.prototype = p;//实例化一个 student 对象var student = new Student();console.log(student); // 打印 {score:100,clickCode:fn}// 因为是一级级继承下来的 所以最上层的 Animate 里的属性也是被继承的console.log(student.weight); // 50student.eat(); // 蜂蜜蜂蜜student.tool(); // 菜刀如图所示:我们将上面的案例通过画图的方式展现出来后就一目了然了,实例对象animal直接替换了构造函数Person的原型,以此类推,这样就会形成一个链式结构的原型链。完整的原型链结合上图,我们发现,最初的构造函数Animal创建的同时,会创建出一个原型,此时的原型是一个空的对象。结合原型链的概念:“原型本身又是一个对象,所以原型也有原型”,那么这个空对象往上还能找出它的原型或者构造函数吗?我们如何创建一个空对象? 1、字面量:{};2、构造函数:new Object()。我们可以简单的理解为,这个空的对象就是,构造函数Object的实例对象。所以,这个空对象往上面找是能找到它的原型和构造函数的。// 创建一个 Animal 构造函数function Animal() { this.weight = 50; this.eat = function() { console.log(‘蜂蜜蜂蜜’); }}// 实例化一个 animal 对象var animal = new Animal();console.log(animal.proto); // {}console.log(animal.proto.proto); // {}console.log(animal.proto.proto.constructor); // function Object(){}console.log(animal.proto.proto.proto); // null如图所示:4.2 原型链的拓展1、描述出数组“[]”的原型链结构// 创建一个数组var arr = new Array();// 我们可以看到这个数组是构造函数 Array 的实例对象,所以他的原型应该是:console.log(Array.prototype); // 打印出来还是一个空数组// 我们可以继续往上找 console.log(Array.prototype.proto); // 空对象// 继续console.log(Array.prototype.proto.proto) // null如图所示:2、扩展内置对象给js原有的内置对象,添加新的功能。注意:这里不能直接给内置对象的原型添加方法,因为在开发的时候,大家都会使用到这些内置对象,假如大家都是给内置对象的原型添加方法,就会出现问题。错误的做法:// 第一个开发人员给 Array 原型添加了一个 say 方法Array.prototype.say = function(){ console.log(‘哈哈哈’);}// 第二个开发人员也给 Array 原型添加了一个 say 方法Array.prototype.say = function(){ console.log(‘啪啪啪’);}var arr = new Array();arr.say(); // 打印 “啪啪啪” 前面写的会被覆盖为了避免出现这样的问题,只需自己定义一个构造函数,并且让这个构造函数继承数组的方法即可,再去添加新的方法。// 创建一个数组对象 这个数组对象继承了所有数组中的方法var arr = new Array();// 创建一个属于自己的构造函数function MyArray(){}// 只需要将自己创建的构造函数的原型替换成 数组对象,就能继承数组的所有方法MyArray.prototype = arr;// 现在可以单独的给自己创建的构造函数的原型添加自己的方法MyArray.prototype.say = function(){ console.log(‘这是我自己添加的say方法’);}var arr1 = new MyArray();arr1.push(1); // 创建的 arr1 对象可以使用数组的方法arr1.say(); // 也可以使用自己添加的方法 打印“这是我自己添加的say方法”console.log(arr1); // [1]4.3 属性的搜索原则当通过对象名.属性名获取属性时,会遵循以下属性搜索的原则:1-首先去对象自身属性中找,如果找到直接使用,2-如果没找到,去自己的原型中找,如果找到直接使用,3-如果没找到,去原型的原型中继续找,找到直接使用,4-如果没有会沿着原型不断向上查找,直到找到null为止。5.Object.prototype成员介绍我们可以看到所有的原型最终都会继承Object的原型:Object.prototype。打印看看Object的原型里面有什么:// Object的原型console.log(Object.prototype)如图所示:我们可以看到Object的原型里有很多方法,下面就来介绍下这些方法的作用。5.1 constructor 属性指向了和原型相关的构造函数5.2 hasOwnProperty 方法判断对象自身是否拥有某个属性,返回值:布尔类型。示例代码:function Hero() { this.name = ‘盖伦’; this.age = ‘25’; this.skill = function () { console.log(‘盖伦使用了大宝剑’); }}var hero = new Hero();console.log(hero.name); // ‘盖伦’hero.skill(); // ‘盖伦使用了大宝剑’console.log(hero.hasOwnProperty(“name”)); // trueconsole.log(hero.hasOwnProperty(“age”)); // trueconsole.log(hero.hasOwnProperty(“skill”)); // trueconsole.log(hero.hasOwnProperty(“toString”)); // false toString是在原型链当中的方法,并不是这里对象的方法console.log(’toString’ in hero); // true in方法 判断对象自身或者原型链中是否有某个属性5.3 isPrototypeOf 方法对象1.isPrototypeOf(对象2),判断对象1是否是对象2的原型,或者对象1是否是对象2原型链上的原型。示例代码:var obj = { age: 18}var obj1 = {};// 创建一个构造函数function Hero() { this.name = ‘盖伦’;}// 将这个构造函数的原型替换成 objHero.prototype = obj;// 实例化一个 hero 对象var hero = new Hero();console.log(obj.isPrototypeOf(hero)); // true 判断 obj 是否是 hero 的原型console.log(obj1.isPrototypeOf(hero)); // false 判断 obj1 是否是 hero 的原型console.log(Object.prototype.isPrototypeOf(hero)); // true 判断 Object.prototype 是否是 hero 的原型// 注意 这里的 Object.prototype 是原型链上最上层的原型对象5.4 propertyIsEnumerable 方法对象.propertyIsEnumerable(‘属性或方法名’),判断一个对象是否有该属性,并且这个属性可以被for-in遍历,返回值:布尔类型。示例代码:// 创建一个构造函数function Hero (){ this.name = ‘盖伦’; this.age = 25; this.skill = function(){ console.log(‘盖伦使用了大宝剑’); }}// 创建一个对象var hero = new Hero();// for-in 遍历这个对象 我们可以看到分别打印了哪些属性和方法for(var k in hero){ console.log(k + ‘—’ + hero[k]); // “name-盖伦” “age-25” “skill-fn()”}// 判断一个对象是否有该属性,并且这个属性可以被 for-in 遍历console.log(hero.propertyIsEnumerable(’name’)); // trueconsole.log(hero.propertyIsEnumerable(‘age’)); // trueconsole.log(hero.propertyIsEnumerable(’test’)); // false5.5 toString 和 toLocalString 方法两种方法都是将对象转成字符串的,只不过toLocalString是按照本地格式进行转换。示例代码:// 举个例子,时间的格式可以分为世界时间的格式和电脑本地的时间格式var date = new Date();// 直接将创建的时间对象转换成字符串console.log(date.toString());// 将创建的时间对象按照本地格式进行转换console.log(date.toLocaleString());效果图:5.6 valueOf 方法返回指定对象的原始值。MDN官方文档6.静态方法和实例方法静态方法和实例方法这两个概念其实也是从面相对象的编程语言中引入的,对应到JavaScript中的理解为:静态方法: 由构造函数调用的在js中,我们知道有个Math构造函数,他有一个Math.abs()的方法,这个方法由构造函数调用,所以就是静态方法。Math.abs();实例方法: 由构造函数创建出来的对象调用的var arr = new Array();// 由构造函数 Array 实例化出来的对象 arr 调用的 push 方法,叫做实例方法arr.push(1);示例代码:function Hero(){ this.name=‘亚索’; this.say=function(){ console.log(‘哈撒ki’); }}Hero.prototype.skill=function(){ console.log(‘吹风’);}// 直接给构造函数添加一个 run 方法(函数也是对象,可以直接给它加个方法)Hero.run=function(){ console.log(‘死亡如风,常伴吾身’);}var hero = new Hero();hero.say();hero.skill(); //实例方法Hero.run(); //静态方法如果这个方法是对象所有的,用实例方法。一般的工具函数,用静态方法,直接给构造函数添加方法,不需要实例化,通过构造函数名直接使用即可;7.作用域“域”,表示的是一个范围,“作用域”就是作用范围。作用域说明的是一个变量可以在什么地方被使用,什么地方不能被使用。7.1 块级作用域在ES5及ES5之前,js中是没有块级作用域的。{ var num = 123; { console.log( num ); // 123 }}console.log( num ); // 123上面这段代码在JavaScript中是不会报错的,但是在其他的编程语言中(C#、C、JAVA)会报错。这是因为,在JavaScript中没有块级作用域,使用{}标记出来的代码块中声明的变量num,是可以被{}外面访问到的。但是在其他的编程语言中,有块级作用域,那么{}中声明的变量num,是不能在代码块外部访问的,所以报错。注意:会计作用域只在在ES5及ES5之前不起作用,但是在ES6开始,js中是存在块级作用域的。7.2 词法作用域词法( 代码 )作用域,就是代码在编写过程中体现出来的作用范围。代码一旦写好,不用执行,作用范围就已经确定好了,这个就是所谓词法作用域。在js中词法作用域规则:函数允许访问函数外的数据;整个代码结构中只有函数可以限定作用域;作用域规则首先使用提升规则分析;如果当前作用规则中有名字了,就不考虑外面的名字。作用域练习:第一题var num=250;function test(){ // 会现在函数内部查找有没有这个num变量,有的话调用,没有的话会去全局中查找,有就返回,没有就返回undefined console.log(num); // 打印 250}function test1(){ var num=222; test();}test1(); 第二题if(false){ var num = 123;}console.log(num); // undefined // {}是没有作用域的 但是有判断条件,var num会提升到判断语句外部 所以不会报错 打印的是undefined第三题var num = 123;function foo() { var num = 456; function func() { console.log( num ); } func();}foo(); // 456// 调用foo时,在函数内部调用了func,打印num的时候,会先在func中查找num 没有的时候会去外层作用域找,找到即返回,找不到即再往上找。第四题var num1 = 123;function foo1() { var num1 = 456; function foo2() { num1 = 789; function foo3 () { console.log( num1 ); // 789 自己的函数作用域中没有就一层层往上找 } foo3(); } foo2();}foo1();console.log( num1 ); // 123 7.3 变量提升(预解析)JavaScript是解释型的语言,但是它并不是真的在运行的时候逐句的往下解析执行。我们来看下面这个例子:func();function func(){ alert(“函数被调用了”);}在上面这段代码中,函数func的调用是在其声明之前,如果说JavaScript代码真的是逐句的解析执行,那么在第一句调用的时候就会出错,然而事实并非如此,上面的代码可以正常执行,并且alert出来"函数被调用了”。所以,可以得出结论,JavaScript并非仅在运行时简简单单的逐句解析执行!JavaScript预解析JavaScript引擎在对JavaScript代码进行解释执行之前,会对JavaScript代码进行预解析,在预解析阶段,会将以关键字var和function开头的语句块提前进行处理。关键问题是怎么处理呢?当变量和函数的声明处在作用域比较靠后的位置的时候,变量和函数的声明会被提升到当前作用域的开头。示例代码:函数名提升正常函数书写方式function func(){ alert(“函数被调用了”);}func();预解析之后,函数名提升func();function func(){ alert(“函数被调用了”);}示例代码:变量名提升正常变量书写方式alert(a); // undefined var a = 123;// 由于JavaScript的预解析机制,上面这段代码,alert出来的值是undefined,// 如果没有预解析,代码应该会直接报错a is not defined,而不是输出值。不是说要提前的吗?那不是应该alert出来123,为什么是undefined?// 变量的时候 提升的只是变量声明的提升,并不包括赋值var a; // 这里是声明alert(a); // 变量声明之后并未有初始化和赋值操作,所以这里是 undefineda = 123; // 这里是赋值注意:特殊情况1、函数不能被提升的情况函数表达式创建的函数不会提升test(); // 报错 “test is not a function"var test = function(){ console.log(123);}new Function创建的函数也不会被提升test(); // 报错 “test is not a function"var test = new Function(){ console.log(123);}2、出现同名函数test(); // 打印 ‘好走的都是下坡路’// 两个函数重名,这两个函数都会被提升,但是后面的函数会覆盖掉前面的函数function test(){ console.log(‘众里寻她千百度,他正在自助烤肉….’);}function test(){ console.log(‘好走的都是下坡路’);}3、函数名与变量名同名// 如果函数和变量重名,只会提升函数,变量不会被提升console.log(test); // 打印这个test函数function test(){ console.log(‘我是test’);}var test=200;再看一种情况:var num = 1;function num () { console.log(num); // 报错 “num is not a function”}num();直接上预解析后的代码:function num(){ console.log(num);}num = 1;num();4、条件式的函数声明// 如果是条件式的函数申明, 这个函数不会被预解析test(); // test is not a functionif(true){ function test(){ console.log(‘只是在人群中多看了我一眼,再也忘不掉我容颜…’); }}预解析是分作用域的声明提升并不是将所有的声明都提升到window 对象下面,提升原则是提升到变量运行的当前作用域中去。示例代码:function showMsg(){ var msg = ‘This is message’;}alert(msg); // 报错“Uncaught ReferenceError: msg is not defined”预解析之后:function showMsg(){ var msg; // 因为函数本身就会产生一个作用域,所以变量声明在提升的时候,只会提升在当前作用域下最前面 msg = ‘This is message’;}alert(msg); // 报错“Uncaught ReferenceError: msg is not defined”预解析是分段的分段,其实就分script标签的<script>func(); // 输出 AA2;function func(){ console.log(‘AA1’);}function func(){ console.log(‘AA2’);}</script><script>function func(){ console.log(‘AA3’);}</script>在上面代码中,第一个script标签中的两个func进行了提升,第二个func覆盖了第一个func,但是第二个script标签中的func并没有覆盖上面的第二个func。所以说预解析是分段的。tip: 但是要注意,分段只是单纯的针对函数,变量并不会分段预解析。函数预解析的时候是分段的,但是执行的时候不分段<script> //变量预解析是分段的 ,但是函数的执行是不分段 var num1=100; // test3(); 报错,函数预解析的时候分段,执行的时候才不分段 function test1(){ console.log(‘我是test1’); } function test2(){ console.log(‘我是test2’); }</script><script> var num2=200; function test3(){ console.log(’test3’); } test1(); // 打印 ‘我是test1’ 函数执行的时候不分段 console.log(num1); // 100</script>7.4 作用域链什么是作用域链?只有函数可以制造作用域结构,那么只要是代码,就至少有一个作用域, 即全局作用域。凡是代码中有函数,那么这个函数就构成另一个作用域。如果函数中还有函数,那么在这个作用域中就又可以诞生一个作用域。将这样的所有的作用域列出来,可以有一个结构: 函数内指向函数外的链式结构。就称作作用域链。例如:function f1() { function f2() { }}var num = 456;function f3() { function f4() { }}示例代码:var num=200;function test(){ var num=100; function test1(){ var num=50; function test2(){ console.log(num); } test2(); } test1();}test(); // 打印 “50”如图所示:绘制作用域链的步骤:看整个全局是一条链, 即顶级链, 记为0级链看全局作用域中, 有什么变量和函数声明, 就以方格的形式绘制到0级练上再找函数, 只有函数可以限制作用域, 因此从函数中引入新链, 标记为1级链然后在每一个1级链中再次往复刚才的行为变量的访问规则:首先看变量在第几条链上, 在该链上看是否有变量的定义与赋值, 如果有直接使用如果没有到上一级链上找( n - 1 级链 ), 如果有直接用, 停止继续查找.如果还没有再次往上刚找… 直到全局链( 0 级 ), 还没有就是 is not defined注意,同级的链不可混合查找来点案例练练手第一题:function foo() { var num = 123; console.log(num); //123}foo();console.log(num); // 报错第二题:var scope = “global”;function foo() { console.log(scope); // undefined var scope = “local”; console.log(scope); // ’local’}foo();// 预解析之后// var scope = “global”;// function foo() {// var scope;// console.log(scope); // undefined// scope = “local”;// console.log(scope); // local// }第三题:if(“a” in window){ var a = 10;}console.log(a); // 10// 预解析之后// var a;// if(“a” in window){// a = 10; // 判断语句不产生作用域// }// console.log(a); // 10第四题:if(!“a” in window){ var a = 10;}console.log(a); // undefined// 预解析之后// var a;// if(!“a” in window){// a = 10; // 判断语句不产生作用域// }// console.log(a); // undefined第五题// console.log(num); 报错 虽然num是全局变量 但是不会提升function test(){ num = 100; }test();console.log(num); // 100第六题var foo = 1;function bar() { if(!foo) { var foo = 10; } console.log(foo); // 10}bar();// 预解析之后// var foo=1;// function bar(){// var foo;// if(!foo){// foo=10;// }// console.log(foo); // 10// }// bar();8.FunctionFunction是函数的构造函数,你可能会有点蒙圈,没错,在js中函数与普通的对象一样,也是一个对象类型,只不过函数是js中的“一等公民”。这里的Function类似于Array、Object等8.1 创建函数的几种方式1、函数字面量(直接声明函数)创建方式function test(){ // 函数体} // 类似于对象字面量创建方式:{}2、函数表达式var test = function(){ // 函数体}3、Function构造函数创建// 构造函数创建一个空的函数var fn = new Function();fn1(); // 调用函数函数扩展名有没有一种可能,函数表达式声明函数时,function 也跟着一个函数名,如:var fn = function fn1(){}? 答案是可以的,不过fn1只能在函数内部使用,并不能在外部调用。var fn = function fn1(a,b,c,d){ console.log(‘当前函数被调用了’); // 但是,fn1可以在函数的内部使用 console.log(fn1.name); console.log(fn1.length); // fn1(); 注意,这样调用会引起递归!!! 下面我们会讲到什么是递归。}// fn1(); // 报错,fn1是不能在函数外部调用的fn(); // “当前函数被调用了”// 函数内部使用时打印:// “当前函数被调用了”// console.log(fn1.name); => “fn1”// console.log(fn1.length); => 48.2 Function 构造函数创建函数上面我们知道了如何通过Function构造函数创建一个空的函数,这里我们对它的传参详细的说明下。1、不传参数时// 不传参数时,创建的是一个空的函数var fn1 = new Function();fn1(); // 调用函数2、只传一个参数// 只传一个参数的时候,这个参数就是函数体// 语法:var fn = new Function(函数体);var fn2 = new Function(‘console.log(2+5)’);f2(); // 73、传多个参数// 传多个参数的时候,最后一个参数为函数体,前面的参数都是函数的形参名// 语法:var fn = new Function(arg1,arg2,arg3…..argn,metthodBody);var fn3 = new Function(’num1’,’num2’,‘console.log(num1+num2)’);f3(5,2); // 78.3 Function 的使用1、用Function创建函数的方式封装一个计算m - n之间所有数字的和的函数//求 m-n之间所有数字的和//var sum=0;//for (var i = m; i <=n; i++) {// sum+=i;//}var fn = new Function(’m’,’n’,‘var sum=0;for (var i = m; i <=n; i++) {sum+=i;} console.log(sum);’);fn(1,100); // 5050函数体参数过长问题:函数体过长时,可读性很差,所以介绍解决方法:1)字符串拼接符“+”var fn = new Function( ’m’, ’n’, ‘var sum=0;’+ ‘for (var i = m; i <=n; i++) {’+ ‘sum += i;’+ ‘}’+ ‘console.log(sum);’ );fn(1,100); // 50502)ES6中新语法“ ”,(在esc键下面)表示可换行字符串的界定符,之前我们用的是单引号或者双引号来表示一个字符串字面量,在ES6中可以用反引号来表示该字符串可换行。new Function( 'm', 'n', var sum=0; for (var i = m; i <=n; i++) { sum+=i; } console.log(sum);`);3)模板方式<!– 新建一个模板 –><script type=“text/template” id=“tmp”> var sum=0; for (var i = m; i <=n; i++) { sum += i; } console.log(sum);</script><script> // 获取模板内的内容 var methodBody = document.querySelector(’#tmp’).innerHTML; console.log(methodBody); var fn = new Function(’m’,’n’,methodBody); fn(2,6); // 20</script>2、eval 函数eval函数可以直接将把字符串的内容,作为js代码执行,前提是字符串代码符合js代码规范。这里主要是用作跟Function传参比较。eval 和 Function 的区别:Function();中,方法体是字符串,必须调用这个函数才能执行eval(); 可以直接执行字符串中的js代码存在的问题:性能问题因为eval里面的代码是直接执行的,所以当在里面定义一个变量的时候,这个变量是不会预解析的,所以会影响性能。// eval 里面的代码可以直接执行,所以下面的打印的 num 可以访问到它// 但是这里定义的 num 是没有预解析的,所以变量名不会提升,从而性能可能会变慢eval(‘var num = 123;’);console.log(num); // 123安全问题主要的安全问题是可能会被利用做XSS攻击(跨站脚本攻击(Cross Site Scripting)),eval也存在一个安全问题,因为它可以执行传给它的任何字符串,所以永远不要传入字符串或者来历不明和不受信任源的参数。示例代码: 实现一个简单的计算器<!– html 部分 –><input type=“text” class=“num1”><select class=“operator”> <option value="+">+</option> <option value=”-">-</option> <option value=”"></option> <option value=”/">/</option></select><input type=“text” class=“num2”><button>=</button><input type=“text” class=“result”><!– js 部分 –><script> document.querySelector(‘button’).onclick=function(){ var num1 = document.querySelector(’.num1’).value; var num2 = document.querySelector(’.num2’).value; var operator = document.querySelector(’.operator’).value; // result其实最终获得的就是 num1 + operator + num2的字符串 但是他能够直接执行并计算 var result = eval(num1 + operator + num2); //计算 document.querySelector(’.result’).value = result; //显示 }</script>效果图:8.4 Function 的原型链结构在7.2章节中我们知道函数也还可以通过构造函数的方式创建出来,既然可以通过构造函数的方式创建,那么函数本身也是有原型对象的。示例代码:// 通过Function构造函数创建一个函数testvar test = new Function();// 既然是通过构造函数创建的,那么这个函数就有指向的原型console.log(test.proto); // 打印出来的原型是一个空的函数console.log(test.proto.proto); // 空的函数再往上找原型是一个空的对象console.log(test.proto.proto.proto); // 再往上找就是null了// 函数原型链: test() —> Function.prototype —> Object.prototype —> null如图所示:通过上图,可以直观的看出,函数也是有原型的。那一个完整的原型链究竟是什么样子的呢?下面我们一起做个总结。8.5 完整的原型链绘制完整原型链的步骤:1、先将一个对象的原型画出来2、再把对象的原型的原型链画出来 ,到null结束3、把对象的构造函数的原型链画出来4、把Function和Object的原型关系给画出来示例代码:// 创建一个构造函数function Person(){ this.name = ‘Levi’; this.age = 18;}// 实例化一个对象var p = new Person();如图所示:总结:Function构造函数的原型,在Object的原型链上;Object构造函数的原型,在Function的原型链上;9.arguments对象在每一个函数调用的过程中, 函数代码体内有一个默认的对象arguments, 它存储着实际传入的所有参数。示例代码:// 封装一个加法函数function add(num1,num2){ console.log(num1+num2);}add(1); // NaNadd(1,2); // 3add(1,2,3); // 3在调用函数时,实参和形参的个数可以不一样,但是没有意义。在函数内部有个arguments对象(注意:是在函数内部),arguments是一个伪数组对象。它表示在函数调用的过程中传入的所有参数(实参)的集合。在函数调用过程中不规定参数的个数与类型,可以使得函数调用变得非常灵活性。function add(num1,num2){ console.log(arguments); // 打印的是一个伪数组}add(1,2,3,4); length:表示的是实参的个数;callee:指向的就是arguments对象所在的函数;示例代码:封装一个求最大值的函数,因为不知道需要传进多少实参,所以直接用伪数组arguments获取调用的实参function max(){ // 假使实参的第一个数字最大 var maxNum = arguments[0]; // 循环这个伪数组 for(var i = 0; i < arguments.length; i++){ if(maxNUm < arguments[i]){ maxNUm = arguments[i]; } return maxNum; } }// 调用console.log(max(1,9,12,8,22,5)); // 2210. 函数的四种调用模式四种调用模式分别是:“函数调用模式”、“方法调用模式”、“构造器调用模式”、“上下文调用模式”。其实就是分析this是谁的问题。只看函数是怎么被调用的,而不管函数是怎么来的。分析this属于哪个函数;分析这个函数是以什么方式调用的;什么是函数? 什么是方法?如果一个函数是挂载到一个对象中,那么就把这个函数称为方法如果一个函数直接放在全局中,由Window对象调用,那么他就是一个函数。// 函数function fn() {}var f = function() {};fn();f();// 方法var obj = { say: function() {}};obj.say();fn和f都是函数,say是一个方法10.1 函数模式函数模式其实就是函数调用模式,this是指向全局对象window的。this -> window示例代码:// 函数调用模式:// 创建的全局变量相当于window的属性var num = 999;var fn = function () { console.log(this); // this 指向的是 window 对象 console.log(this.num); // 999};fn();10.2 方法模式方法模式其实就是方法调用模式,this是指向调用方法的对象。this -> 调用方法的对象示例代码:// this指向的是obj var age = 38;var obj = { age: 18, getAge: function () { console.log(this); // this指向的是对象obj {age:18,getAge:f()} console.log(this.age); // 18 } };obj.getAge(); // getAge() 是对象 obj 的一个方法10.3 构造器模式构造器模式其实就是构造函数调用模式,this指向新创建出来的实例对象。this -> 新创建出来的实例对象示例代码:// this指向的是实例化出来的对象function Person(name){ this.name = name; console.log(this);}var p1 = new Person(‘Levi’); // Person {name: “Levi”}var p2 = new Person(‘Ryan’); // Person {name: “Ryan”}构造函数的返回值:如果返回的是基本类型function Person() { return 1;}var p1 = new Person();console.log(p1); // 打印Person {}构造函数内有返回值,且是基本类型的时候,返回值会被忽略掉,返回的是实例出来的对象。如果返回的是引用类型function Person() { return { name: ’levi’, age: 18 };}var p1 = new Person();console.log(p1); // 此时打印 Object {name: ’levi’, age: 18}构造函数内的返回值是一个引用类型的时候,返回的就是这个指定的引用类型。10.4 上下文(借用方法)模式上下文,即环境,用于指定方法内部的this,上下文调用模式中,this可以被随意指定为任意对象。上下文模式有两种方法,是由函数调用的:函数名.apply( … );函数名.call( … );1、apply 方法语法:fn.apply(thisArg, array);参数:第一个参数:表示函数内部this的指向(或者:让哪个对象来借用这个方法)第二个参数:是一个数组(或者伪数组),数组中的每一项都将作为被调用方法的参数示例代码:// 没有参数function fn (){ console.log(this.name);}var obj = { name : ‘Levi丶’}// this 指向 obj,fn 借用obj方法里面的 name 属性fn.apply(obj); // 打印 ‘Levi丶’// 有参数function fn (num1, num2){ console.log(num1 + num2);}var obj = {}// this 指向 obj,数组中的数据是方法 fn 的参数fn.apply(obj, [1 , 2]); // 打印 3注意:apply方法的第一个参数,必须是一个对象!如果传入的参数不是一个对象,那么这个方法内部会将其转化为一个包装对象。function fn() { console.log(this);}fn.apply(1); // 包装对象fn.apply(‘abc’); // 包装对象fn.apply(true); // 包装对象指向window的几种方式:function fn(){ }fn.apply(window);fn.apply();fn.apply(null);fn.apply(undefined);具体应用:求数组中的最大数// 以前的方法,假设第一项最大,然后与后面每一项比较,得到最大的项var arr = [1, 3, 6, 10, 210, 23, 33, 777, 456];var maxNum = arr[0];for(var i = 1; i < arr.length; i++) { if(maxNum < arr[i]) { maxNum = arr[i]; }}console.log(maxNum); // 777// 利用 内置对象的 apply 的方法var arr = [1, 3, 6, 10, 210, 23, 33, 777, 456];// max 是内置对象 Math 求最大值的一个方法var maxNum = Math.max.apply(null, arr);console.log(maxNum); // 777将传进的参数每一项之间用“-”连接// 思考:参数个数是用户随机传的,没有具体的一个值,这时候就需要用到 arguments 的概念了function fn (){ // 数组原型中有一个join方法,他的接收的参数是一个字符串 // join.apply的第一个参数指向 arguments 对象,第二个参数是jion方法需要的参数 return Array.prototype.join.apply(arguments, [’-’]);}var ret = fn(‘a’, ‘b’, ‘c’, ’d’, ’e’);console.log(ret); // ‘a-b-c-d-e'2、call 方法call方法的作用于apply方法的作用相同,唯一不同的地方就是第二个参数不同。语法:fn.apply(thisArg, parm1,parm2,parm3,…);参数:第一个参数:表示函数内部this的指向(或者:让哪个对象来借用这个方法)第二个及后面的参数:不是之前数组的形式了,对应方法调用的每一个参数示例代码:function fn(num1, num2, num3) { console.log(num1, num2, num3);}var obj = {};fn.call(obj, [1, 3, 9], 0, 1); // [1, 3, 9] 0 1fn.call(obj, [1, 3, 9]); // [1, 3, 9] undefined undefined3、apply 和 call 的区别两者在功能上一模一样,唯一的区别就是第二个参数传递的类型不一样。什么时候用apply?什么时候用call呢?其实用哪个都可以,在参数少的情况下,我们可以使用call方法,但是如果参数是伪数组或者是数组的时候,call方法就不适用了,还需要将伪数组中的每一项取出来作为方法的参数,此时apply更加实用。10.5 面试题分析面试题1:var age = 38;var obj = { age: 18, getAge: function() { function foo() { console.log(this.age); // 这里的this属于函数 foo; 打印 38 } foo(); // foo函是Window对象调用的 }};obj.getAge();面试题2:// 只看函数是怎么被调用的,而不管函数是怎么来的var age = 38;var obj = { age: 18, getAge: function() { alert(this.age); }};var f = obj.getAge;f(); // 函数是Window对象调用的,所以this指向Window对象。打印:38面试题3:var length = 10;function fn(){ console.log(this.length);}var obj = { length: 5, method: function (fn) { fn(); // window对象调用 打印 10 arguments0; // 方法调用模式,是arguments对象调用的 // this指向arguments,所以arguments.length = 2; (arguments.length:实参的个数)所以打印 2 }};obj.method(fn, 123);面试题4:怎么使用call或者apply方法实现构造函数的复用呢?function Person(name, age, gender) { this.name = name; this.age = age; this.gender = gender;}function Teacher(name, age, gender, workYear, subject) { this.name = name; this.age = age; this.gender = gender; this.workYear = workYear; this.subject = subject;}function Student(name, age, gender, stuNo, score) { this.name = name; this.age = age; this.gender = gender; this.stuNo = stuNo; this.score = score;}var tec = new Teacher(‘张老师’, 32, ‘male’, ‘7年’, ‘语文’);var stu = new Student(‘xiaowang’, 18, ‘male’, 10001, 99);console.log(tec); // Teacher {name: “张老师”, age: 32, gender: “male”, workYear: “7年”, subject: “语文”}console.log(stu); // Student {name: “xiaowang”, age: 18, gender: “male”, stuNo: 10001, score: 99}上面的代码中一个Teacher构造函数,一个Student构造函数,他们都有一些公共的属性,跟Person构造函数里面的属性重复,我们能否使用call或者apply方法,简化上面的代码呢?function Person(name, age, gender) { this.name = name; this.age = age; this.gender = gender;}function Teacher(name, age, gender, workYear, subject) { // 借用 Person 函数来给当前对象添加属性 Person.call(this, name, age, gender); // 这里的this指向的就是当前的Teacher构造函数 this.workYear = workYear; this.subject = subject;}function Student(name, age, gender, stuNo, score) { Person.call(this, name, age, gender); // 这里的this指向的就是当前的Student构造函数 this.stuNo = stuNo; this.score = score;}var tec = new Teacher(‘张老师’, 32, ‘male’, ‘7年’, ‘语文’);var stu = new Student(‘xiaowang’, 18, ‘male’, 10001, 99);console.log(tec); // Teacher {name: “张老师”, age: 32, gender: “male”, workYear: “7年”, subject: “语文”}console.log(stu); // Student {name: “xiaowang”, age: 18, gender: “male”, stuNo: 10001, score: 99}11.递归11.1 什么是递归什么是递归?递归就是函数直接自己调用自己或者间接的调用自己。举个例子:函数直接调用自己function fn(){ fn();}fn();函数间接调用自己function fn1(){ fn2();}function fn2(){ fn1();}递归示例代码:function fn (){ console.log(‘从前有座山,’); console.log(‘山里有座庙,’); console.log(‘庙里有个老和尚,’); console.log(‘老和尚给小和尚讲,’); fn();}fn(); // 产生递归,无限打印上面的内容这样做会进入到无限的死循环当中。11.2 化归思想化归思想是将一个问题由难化易,由繁化简,由复杂化简单的过程称为化归,它是转化和归结的简称。合理使用递归的注意点:函数调用了自身必须有结束递归的条件,这样程序就不会一直运行下去了示例代码: 求前n项的和求前n项的和其实就是:1 + 2 + 3 +…+ n;寻找递推关系,就是n与n-1, 或n-2之间的关系:sum(n) == n + sum(n - 1);加上结束的递归条件,不然会一直运行下去。function sum(n){ if(n == 1) return 1; // 递归结束条件 return n + sum(n - 1);}sum(100); // 打印 5050递推关系:11.3 递归练习1、求n的阶乘:思路:f(n) = n * f(n - 1);f(n - 1) = (n - 1) * f(n - 2);示例代码:function product(n){ if(n == 1) { return 1; } return n * product(n-1);}console.log(product(5)); // 打印 1202、求m的n次幂:思路:f(m,n) = m * f(m,n-1);示例代码:function pow(m,n){ if(n==1){ return m; } return m * pow(m,n-1);}console.log(pow(2, 10)); // 打印 10243、斐波那契数列思路:什么是斐波那契数列?1 , 1 , 2 , 3 , 5 , 8 , 13 , 21 , 34 , 55,… 数字从第三项开始,每一项都等于前两项的和。可得出公式:fn = f(n-1) + f(n-2),结束递归的条件:当n <= 2时,fn = 1。示例代码:function fib(n){ if(n<=2) return 1; // 结束递归的条件 return fib(n-1) + fib(n-2);}console.log(fib(5)); // 5console.log(fib(10)); // 55console.log(fib(25)); // 75025 // 数值太大会影响性能问题存在问题:数值太大时会影响性能,怎么影响的呢?function fib(n){ if(n<=2) return 1; return fib(n-1) + fib(n-2); // 当我们在计算一个值的时候,都是通过计算他的fib(n-1) 跟 fib(n-2)项之后再去进行相加,得到最终的值 // 这时候就需要调用两次这个函数,在计算fib(n-1)的时候,其实也是调用了两次这个函数,得出fib(n-1)的值}// 记录执行的次数var count=0;function fib(n){ count++; if(n<=2) return 1; return fib(n-1)+fib(n-2);}console.log(fib(5)); // 5console.log(count); // 9 求第五项的时候就计算了9次//console.log(fib(20)); // 6765//console.log(count); // 13529 求第20项的时候就计算了13529次这个问题在下面讲闭包的时候解决。4.获取页面所有的元素,并加上边框页面结构:<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <meta name=“viewport” content=“width=device-width, initial-scale=1.0”> <meta http-equiv=“X-UA-Compatible” content=“ie=edge”> <title>Document</title></head><body> <div> <p> <span>我是span标签</span> <span>我是span标签</span> <span>我是span标签</span> </p> <p> <span>我是span标签</span> <span>我是span标签</span> <span>我是span标签</span> </p> </div> <div> <p> <span>我是span标签</span> <span>我是span标签</span> <span>我是span标签</span> </p> <p> <span>我是span标签</span> <span>我是span标签</span> <span>我是span标签</span> </p> </div></body></html>结构图:js代码:// 封装一个方法,获取到所有的标签,并且给这些标签加上边框function childrenTag(ele){ var eleArr = []; // 用于存放所有的获取到的标签 var elements = ele.children; // 获取传入元素下的直接子元素 (伪数组) for(var i = 0; i < elements.length; i++){ eleArr.push(elements[i]); // 获取子元素下的直接子元素 var temp = childrenTag(elements[i]); // 一层层的递推下去 eleArr = eleArr.concat(temp); // 将获取的子元素的拼接到一起 } return eleArr;}console.log(childrenTag(document.body)); // 打印的就是页面body下所有的标签// 获取所有标签var tags=childrenTag(document.body);// 给所有标签添加边框for(var i=0;i<tags.length;i++){ tags[i].style.border=‘1px solid cyan’;}效果图:12. JS 内存管理本章引用自:《MDN-内存管理》12.1 内存生命周期不管是什么程序语言,内存生命周期基本是一致的:分配你所需要的内存;使用分配到的内存(读、写);不需要时将其释放、归还。JavaScript 的内存分配:为了不让程序员费心分配内存,JavaScript在定义变量时就完成了内存分配。var n = 123; // 给数值变量分配内存var s = “Levi”; // 给字符串分配内存var o = { a: 1, b: null}; // 给对象及其包含的值分配内存// 给数组及其包含的值分配内存(就像对象一样)var a = [1, null, “abra”]; function f(a){ return a + 2;} // 给函数(可调用的对象)分配内存// 函数表达式也能分配一个对象someElement.addEventListener(‘click’, function(){ someElement.style.backgroundColor = ‘blue’;}, false);使用值:使用值的过程实际上是对分配内存进行读取与写入的操作。读取与写入可能是写入一个变量或者一个对象的属性值,甚至传递函数的参数。当内存不再需要使用时释放:大多数内存管理的问题都在这个阶段。在这里最艰难的任务是找到“所分配的内存确实已经不再需要了”。它往往要求开发人员来确定在程序中哪一块内存不再需要并且释放它。高级语言解释器嵌入了“垃圾回收器”,它的主要工作是跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它。这只能是一个近似的过程,因为要知道是否仍然需要某块内存是无法判定的(无法通过某种算法解决)。12.2 垃圾回收如上所述,自动寻找是否一些内存“不再需要”的问题是无法判定的。因此,垃圾回收实现只能有限制的解决一般问题。本节将解释必要的概念,了解主要的垃圾回收算法和它们的局限性。1、引用:垃圾回收算法主要依赖于引用的概念。在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。例如,一个Javascript对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。在这里,“对象”的概念不仅特指JavaScript对象,还包括函数作用域(或者全局词法作用域)。2、引用计数垃圾收集:这是最天真的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。示例代码var o = { a: { b:2 }}; // 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量o// 很显然,没有一个可以被垃圾收集var o2 = o; // o2变量是第二个对“这个对象”的引用o = 1; // 现在,“这个对象”的原始引用o被o2替换了var oa = o2.a; // 引用“这个对象”的a属性// 现在,“这个对象”有两个引用了,一个是o2,一个是oao2 = “yo”; // 最初的对象现在已经是零引用了 // 他可以被垃圾回收了 // 然而它的属性a的对象还在被oa引用,所以还不能回收oa = null; // a属性的那个对象现在也是零引用了 // 它可以被垃圾回收了限制:循环引用该算法有个限制:无法处理循环引用。在下面的例子中,两个对象被创建,并互相引用,形成了一个循环。它们被调用之后会离开函数作用域,所以它们已经没有用了,可以被回收了。然而,引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收。function f(){ var o = {}; var o2 = {}; o.a = o2; // o 引用 o2 o2.a = o; // o2 引用 o return “azerty”;}f();实际例子:IE 6, 7 使用引用计数方式对DOM对象进行垃圾回收。该方式常常造成对象被循环引用时内存发生泄漏:var div;window.onload = function(){ div = document.getElementById(“myDivElement”); div.circularReference = div; div.lotsOfData = new Array(10000).join(”*”);};在上面的例子里,myDivElement这个DOM元素里的circularReference属性引用了myDivElement,造成了循环引用。如果该属性没有显示移除或者设为null,引用计数式垃圾收集器将总是且至少有一个引用,并将一直保持在内存里的DOM元素,即使其从DOM树中删去了。如果这个DOM元素拥有大量的数据(如上的lotsOfData属性),而这个数据占用的内存将永远不会被释放。3、标记-清除算法这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。这个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。这个算法比前一个要好,因为“有零引用的对象”总是不可获得的,但是相反却不一定,参考“循环引用”。从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。所有对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进,并没有改进标记-清除算法本身和它对“对象是否不再需要”的简化定义。循环引用不再是问题了在上面的示例中,函数调用返回之后,两个对象从全局对象出发无法获取。因此,他们将会被垃圾回收器回收。第二个示例同样,一旦div和其事件处理无法从根获取到,他们将会被垃圾回收器回收。限制: 那些无法从根对象查询到的对象都将被清除尽管这是一个限制,但实践中我们很少会碰到类似的情况,所以开发者不太会去关心垃圾回收机制。一般情况下, 如果需要手动释放变量占用的内存, 就将这个变量赋值为:null13. 闭包了解闭包之前,先了解下另外两个知识点:1、函数基础知识1、函数内部的代码在调用的时候执行2、函数返回值类型可以是任意类型3、怎么理解函数的返回值将函数内部声明的变量暴露到函数外部函数内用来返回数据,相当于没有函数的时候直接使用该数据不同之处在于:函数形成作用域,变量为局部变量function foo() { var o = {age: 12}; return o;}var o1 = foo();// 相当于: var o1 = {age: 18};2、作用域的结论1、JavaScript的作用域是词法作用域2、函数才会形成作用域(函数作用域)3、词法作用域:变量(变量和函数)的作用范围在代码写出来的就已经决定, 与运行时无关4、函数内部可以访问函数外部的变量(函数外部不能访问函数内部的变量)5、变量搜索原则:从当前链开始查找直到0级链6、当定义了一个函数,当前的作用域链就保存起来,并且成为函数的内部状态的一部分。13.1 闭包的概念闭包从字面意思理解就是闭合,包起来。简单的来说闭包就是,一个具有封闭的对外不公开的包裹结构或空间。在JavaScript中函数可以构成闭包。一般函数是一个代码结构的封闭结构,即包裹的特性。同时根据作用域规则, 只允许函数访问外部的数据,外部无法访问函数内部的数据,即封闭的对外不公开的特性。因此说函数可以构成闭包。闭包的其他解释在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。实例:function fn() { var num = 123; return function foo() { console.log(num); };}// bar1就是闭包的一个实例var bar1 = fn();// bar2就是闭包的另外一个实例var bar2 = fn();bar1(); // 123bar2(); // 123闭包的构成闭包包括两部分:1、函数体(函数自身的代码);2、环境(函数的作用域)。闭包的说明1、JS中函数形成了闭包2、闭包是函数作用域的应用3、对于闭包来说,只关注创建函数的作用域,不关注调用函数的位置闭包的作用对函数内部的变量起到保护作用除了返回的函数以外,没有任何手段能够获取或者修改这个变量的值13.2 闭包模型function foo() { var num = 0; // 函数会产生一个作用域,所以外部的程序想要访问函数内部的变量,一般情况下是不行的 // 通过闭包的方式可以使外部访问到函数内部的变量 // 具体做法就是在函数内部返回一个函数,并且这个函数使用了这个变量 // 当用户调用最外层的函数的时候,使用的这个变量就会随着返回的函数返回给用户 return function() { return ++num; };}// 函数foo的返回值就是一个函数,所以,就可以调用getNum这个函数了!var getNum = foo();console.log(getNum()); // 113.3 闭包的使用目标:想办法(在外部)访问到函数内部的数据利用函数返回值function foo() { var num = Math.random(); return num;}var num1 = foo();var num2 = foo();console.log(num1 === num2); // 随机数 相同的情况很小很小普通的函数返回值说明两次调用函数,返回的数据并不是同一个数据。原因:函数在每次调用的时候,内部的数据会被新创建一次游戏充值案例示例图片:示例代码:<button id=“pay”>充值</button><button id=“play”>玩游戏</button><script> // 需求: // 1-需要对充值的金额起到保护作用,这个存放数值的变量不能暴露在全局,否则谁都会去修改这个金额 // var money = 0; // 2-点击充值按钮的时候,每次充值10元 // 3-点击玩游戏按钮的时候,每玩一次金额减少一元 function fn (){ var money = 0; // money用来存储充值的钱,放在函数内部,不会暴露在全局 // 一般的闭包返回值是一个函数,但是这里有两个功能,一个是玩游戏,一个是充值; // 两个功能分开,但是金额之间还是关联的,所以这里返回一个对象,里面存放两个方法 return { // 充值的函数 recharge:function(value){ money += value; console.log(‘尊敬的黄金会员,您本次充值:’ + value, ‘,您的总余额为:’ + money); }, // 玩游戏的函数 play:function(){ if(money <= 0){ console.log(‘余额不足无法继续游戏,请充值!’); return; } money–; console.log(‘您还剩余 ’ + money + ’ 条命!’); } }; } var obj = fn(); // 点击“充值”按钮 var pay = document.getElementById(‘pay’); pay.addEventListener(‘click’, function () { obj.recharge(10); }); // 点击“玩游戏”按钮 var play = document.getElementById(‘play’) play.addEventListener(‘click’, function () { obj.play(); })</script>优化,多个角色进行充值玩游戏<div> <button id=“pay”>小明:充值</button> <button id=“play”>小明:玩游戏</button></div><div> <button id=“pay1”>小华:充值</button> <button id=“play1”>小华:玩游戏</button></div><script> // 1 需要对充值的钱起到保护作用 // var money = 0; // 2 充值: 每次充值20 // 3 玩游戏: 每玩一次,金额少1 // 整个fn()形成一个函数作用域,对里面的变量起到保护作用 function fn() { // money 用来存储充值的钱 var money = 0; // 充值的函数: function recharge(value) { // money += 20; money += value; console.log(‘尊敬的黄金会员,您本次充值:’ + value, ‘,您的总余额为:’ + money); } // 玩游戏的函数 function play() { money–; if (money < 0) { console.log(‘余额不足,请充值!’); } else { console.log(‘您还剩余 ’ + money + ’ 条命!’); } } return { recharge: recharge, play: play }; } // 小明充值玩游戏的函数 var obj; obj = fn(); // 小明玩游戏: var pay = document.getElementById(‘pay’); pay.addEventListener(‘click’, function () { obj.recharge(20); }); var play = document.getElementById(‘play’) play.addEventListener(‘click’, function () { obj.play(); }); // 小华(新的闭包实例): var obj1 = fn(); // 小华玩游戏: var pay1 = document.getElementById(‘pay1’); pay1.addEventListener(‘click’, function () { obj1.recharge(20); }); var play1 = document.getElementById(‘play1’) play1.addEventListener(‘click’, function () { obj1.play(); });</script>优化的案例我们可以看到,只要重新定义一个变量,接收函数 fn(),就能重新开辟一个新的空间,且多个用户之间不受任何影响。13.4 闭包里的缓存从内存看闭包函数调用也是需要内存的!因为函数中声明了一些变量,这些变量在函数调用过程中是可以使用的,所以, 这个变量是存储到了函数调用时候分配的内存中了!因为没有任何变量来引用这块内存,所以,函数调用结束。 函数调用占用的内存就会被回收掉。虽然,此时的函数有返回值(返回了一个普通的变量),并且这个函数调用结束以后这个函数占用的内存还是被回收了!但是, 存储函数的内存还在。闭包的内存占用:作用域的引用是对函数整个作用域来说的,而不是针对作用域中的某个变量!!!即便没有任何的变量,也是有作用域( 作用域的引用 )。function fn() { var num = 123; return function() { console.log(num); };}// 此时, 函数fn调用时候占用的内存, 是不会被释放掉的!!!var foo = fn();// 调用 foo() 此时, 因为返回函数的作用域对外层函数fn的作用域有引用// 所以, 即使是 fn() 调用结束了, 因为 返回函数作用域引用的关系, 所以// 函数fn()调用时候, 产生的内存是不会被释放掉的!foo();// 手动释放闭包占用的内存!foo = null;缓存介绍缓存:暂存数据方便后续计算中使用。缓存中存储的数据简单来说就是:键值对工作中,缓存是经常被使用的手段。目的:提高程序运行的效率我们只要是使用缓存,就完全信赖缓存中的数据。所以, 我们可以通过闭包来保护缓存。对于缓存来说,我们既要存储值,又要取值!存储的目的是为了将来取出来,在js中可以使用对象或者数组来充当缓存。如果是需要保持顺序的,那么就用数组,否则就用对象!// 创建一个缓存:var cache = {};// 往缓存中存数据:cache.name = ‘xiaoming’;cache[’name1’] = ‘xiaohua’;// 取值console.log(cache.name);console.log(cache[’name1’]);计算机中的缓存就是数据交换的缓冲区(称作Cache),当某一硬件要读取数据时,会首先从缓存中查找需要的数据,如果找到了则直接执行,找不到的话则从内存中找。由于缓存的运行速度比内存快得多,故缓存的作用就是帮助硬件更快地运行。缓存使用步骤首先查看缓存中有没有该数据,如果有,直接从缓存中取出来;如果没有就递归计算,并将结果放到缓存中递归计算斐波那契数列存在的问题前面在学习递归的时候,我们举了一个斐波那契数列的例子,但是当时说存在性能问题,我们重新看下这个问题。// 使用递归计算 菲波那契数列// 数列:1 1 2 3 5 8 13 21 34 55 89 。。。// 索引:0 1 2 3 4 5 6 7 8 9 10 。。。var count = 0;var fib = function (num) { count++; if (num === 0 || num == 1) { return 1; } return fib(num - 1) + fib(num - 2);};// 计算索引号为10的值, 一共计算了: 177 次// 计算索引号为11的值, 一共计算了: 287 次// 计算索引号为12的值, 一共计算了: 465 次// ….// 计算索引号为20的值, 一共计算了: 21891 次// 计算索引号为21的值, 一共计算了: 35421 次// …// 计算索引号为30的值, 一共计算了: 2692537 次// 计算索引号为31的值, 一共计算了: 4356617 次fib(31);console.log(count); // 4356617注意上面代码,count是用来记录程序运行时执行的次数,不明白的小伙伴可以返回递归那一章节,我专门画了一张图,可以理解下这个次数是怎么计算的。我们看下上面的代码的注释,求第20项跟21项的时候,虽然只相差一项,但是却多运算了一万多次,试想一下这里面存在的效率问题是多么的可怕。闭包和缓存解决计算斐波那契数列存在的问题其实主要的问题就是,数据重复运算。比如计算第五项的时候,他计算的是第三项跟第四项的和,这时的第三项跟第四项都是从一开始重新计算的,假如吧计算过得值保存下来,就不需要再重复的运算。运用缓存:将计算的值存储下来,减少运算次数,提高效率;使用闭包:从来保护缓存。// 记录计算的次数var count = 0;function fn() { // 缓存对象 var cache = {}; // 这个返回函数才是 递归函数! return function( num ) { count++; // 1 首先查看缓存中有没有 num 对应的数据 if(cache[num]) { // 说明缓存中有我们需要的数据 return cache[num]; } // 2 如果缓存中没有, 就先计算, 并且将计算的结果存储到缓存中 if(num === 0 || num === 1) { // 存储到缓存中 cache[num] = 1; return 1; } var temp = arguments.callee(num - 1) + arguments.callee(num - 2); cache[num] = temp; return temp; };}var fib = fn();var ret = fib(20);console.log(ret); // 10946console.log(‘计算了:’ , count, ‘次’); // 计算了: 39 次我们可以跟上面没有使用缓存,求斐波那契数列的比较一下,此时求第20项的时候,仅仅运算了39次,但是在之前却运行了21891次。上面的方法存在着一些的问题,每次在执行的时候,函数fn都要先被调用一次(var fib = fn();),下面进行优化:将fn转换成自执行函数(沙箱模式,下一章会讲),自执行函数的返回函数就是递归函数;判断缓存是否存在的条件进行优化,之前是通过判断缓存的值是否存在,来进行存、取值的,但是假如一个缓存的值是false的时候呢?岂不是if(false){}了,明明有值的时候,却不能取值了,所以玩我们只需要判断缓存里是否存在某个键就行。var fib = (function () { // 缓存对象 var cache = {}; // 这个返回函数才是 递归函数! return function (num) { // 1 首先查看缓存中有没有 num 对应的数据 /** if(cache[num]) { return cache[num]; } / // 只要缓存对象中存在 num 这个key, 那么结果就应该是 true if (num in cache) { // 说明缓存中有我们需要的数据 return cache[num]; } // 2 如果缓存中没有, 就先计算, 并且将计算的结果存储到缓存中 if (num === 0 || num === 1) { // 存储到缓存中 // cache[num] = 1 是一个赋值表达式, 赋值表达式的结果为: 等号右边的值! return (cache[num] = 1); } // arguments.callee 表示当前函数的引用 return (cache[num] = arguments.callee(num - 1) + arguments.callee(num - 2)); };})();var ret = fib(10)console.log(ret);什么是 arguments.callee?返回正被执行的function对象,也就是所指定的function对象的正文。callee属性是arguments 对象的一个成员,它表示对函数对象本身的引用,这有利于匿名函数的递归或者保证函数的封装性。function fn(a, b) { console.log(arguments);}fn(1, 2);我们可以看到,打印的arguments属性里面有哪些参数:前面几项是函数调用后传进来的实参;callee:f,它其实就是函数fn的引用,你可以理解为:arguments.callee()相当于fn();length就是实参的长度。再去看上面斐波那契的案例,它的递归函数是一个匿名函数,所以在这个函数里面自己调用自己的时候,就是使用的arguments.callee去引用的。14. 沙箱模式沙箱模式又称:沙盒模式、隔离模式。沙箱(sandbox)介绍:用于为一些来源不可信、具备破坏力或无法判定程序意图的程序提供试验环境。然而,沙盒中的所有改动对操作系统不会造成任何损失。14.1 沙箱模式的作用作用:对变量进行隔离问题:在js中如何实现隔离?ES6之前, JavaScritp中只有函数能限定作用域,所以,只有使用函数才能实现隔离。本质上还是对函数作用域的应用。14.2 沙箱模式模型使用自调用函数实现沙箱模式函数形成独立的作用域;函数只有被调用,内部代码才会执行;将全局污染降到最低。(function() { // … // 代码 // …})(); 14.3 沙箱模式应用最佳实践:在函数内定义变量的时候,将 变量定义 提到最前面。// 1 减少了window变量作用域的查找// 2 有利于代码压缩(function( window ) { var fn = function( selector ) { this.selector = selector; }; fn.prototype = { constructor: fn, addClass: function() {}, removeClass: function() {} }; // 给window添加了一个 $属性,值为: fn // 暴露数据的方式: window.$ = fn;})( window );14.4 沙箱模式的说明将代码放到一个立即执行的函数表达式(IIFE)中,这样就能实现代码的隔离;使用IIFE:减少一个函数名称的污染,将全局变量污染降到最低;代码在函数内部执行,函数内部声明的变量不会影响到函数外部;如果外部需要,则可以返回数据或把要返回的数据交给window。IIFE: Immediately Invoke Function Expression立即执行的函数表达式15. 工厂模式工厂模式是一种设计模式,作用是:隐藏创建对象的细节,省略了使用new创建对象。构造函数:构造函数创建之后,我们实例化一个对象的时候都是直接通过new创建出来的。function Person(name, age) { this.name = name; this.age = age;}var p1 = new Person(‘Levi’, 18); 工厂函数:工厂函数的核心就是隐藏这个new创建对象的细节。function Person(name, age) { this.name = name; this.age = age;}function createPerson(name, age) { return new Person(name, age);}var p2 = createPerson(‘Ryan’, 19);两段代码比较下来,我们可以看到,实例出来的p2对象没有直接使用new创建,而是通过一个函数的返回值创建出来的,这就是工厂模式。使用场合:jQuery中,我们用的“$”或者jQuery函数,就是一个工厂函数。/ Jquery 中的部分源码 */// jQuery 实际上是一个 工厂函数,省略了 new 创建对象的操作jQuery = function( selector, context ) { // jQuery.fn.init 才是jQuery中真正的构造函数 return new jQuery.fn.init( selector, context );}(本篇完) ...

December 27, 2018 · 19 min · jiezi

javascript面向对象之继承(上)

我们之前介绍了javascript面向对象的封装的相关内容,还介绍了js的call方法,今天开始讨论js的继承这篇文章参考了《javascript高级程序设计》(第三版),但内容不局限于,网上很多关于js继承的相关内容都是来自于这本书,有兴趣的同学可以翻阅查看原型链继承我们先通过一个栗子,了解一下原型链继承。留意代码注释内容//创建自定义构造函数function Hqg() { this.name = ‘洪七公’;}//在当前构造函数的原型链上添加属性skillHqg.prototype.skill = ‘打狗棒’//通过自定义构造函数Hqg实例化一个对象gjconst gj = new Hqg()console.log(gj.skill);//=>打狗棒//通过自定义构造函数Hqg实例化一个对象hrconst hr = new Hqg()console.log(hr.skill);//=>打狗棒一个简单的栗子,郭靖和黄蓉都从洪七公那里继承到了skill 打狗棒,貌似没什么问题,我们继续看demo//上面代码省略const gj = new Hqg()//通过自定义构造函数Hqg实例化一个对象gjgj.skill = “降龙十八掌”//重新对gj的skill赋值console.log(gj.skill);//=>降龙十八掌//通过自定义构造函数Hqg实例化一个对象hrconst hr = new Hqg()console.log(hr.skill);//=>打狗棒对郭靖的skill重新赋值,也没有影响黄蓉的skill,我们打印一下gjgj的skill屏蔽掉了原型链上的skill,所以gj的skill是降龙十八掌,而hr的skill依然是打狗棒问题即将暴露function Hqg() { this.name = ‘洪七公’;}Hqg.prototype.skill = [‘打狗棒’]const gj = new Hqg()gj.skill.push (“降龙十八掌”)//找到了原型链中的skill,并对其执行push操作,从而改变了构造函数中的skill属性console.log(gj.skill); //=>[“打狗棒”, “降龙十八掌”]const hr = new Hqg()//构造函数中的skill已经被改变console.log(hr.skill); //=>[“打狗棒”, “降龙十八掌”]总结一下,gj和hr都是Hqg的实例,继承Hqg的属性和方法,当Hqg的属性或者方被改变了,后面的实例也会受影响,有时候这并不是我们希望的结果借用构造函数借用?就是使用call或者apply改变一下this指向,就是子类的构造函数内部通过call或者apply调用父类的构造函数,如果对call方法有不了解的地方,可以翻看昨天的文章 举一个栗子//创建一个构造函数,并添加一些属性function Hqg() { this.name = ‘洪七公’; this.job = ‘帮主’; this.skill = [‘降龙十八掌’, ‘打狗棒’]}//创建一个构造函数,并借用了Hqg的构造函数function Hr() { Hqg.call(this) this.name = ‘黄蓉’; this.job = [‘相夫’, ‘教子’]}//创建一个构造函数,并借用了Hqg的构造函数function Gj() { Hqg.call(this) this.name = ‘郭靖’; this.job = [‘吃饭’, ‘睡觉’]}const hr = new Hr();console.log(hr);const gj = new Gj();console.log(gj);输出这样就避免了原型链继承中,构造函数中的属性或者方法被其他实例所改变的问题⚠️:这里要注意call方法的执行顺序://部分代码省略function Hr() { this.name = ‘黄蓉’; this.job = [‘相夫’, ‘教子’] Hqg.call(this)}function Gj() { this.name = ‘郭靖’; this.job = [‘吃饭’, ‘睡觉’] Hqg.call(this)}//部分代码省略如果call在之后执行就会导致一个问题值会被覆盖,这个要注意!借用构造函数进行传参这个算是一个升级的玩法吧function Hqg(name,job,skill) { this.name = name; this.job = job; this.skill = skill}function Hr() { Hqg.call(this,‘黄蓉’,[‘相夫’, ‘教子’],[‘打狗棒’])}function Gj() { Hqg.call(this,‘郭靖’,[‘吃饭’, ‘睡觉’],[‘降龙十八掌’])}const hr = new Hr();console.log(hr);const gj = new Gj();console.log(gj);输出组合继承将原型链和借用构造函数技术组合到一起。使用原型链实现对原型属性和方法的继承,用借用构造函数模式实现对实例属性的继承。这样既通过在原型上定义方法实现了函数复用,又能保证每个实例都有自己的属性一个栗子function Hqg(name) { this.name = name this.skill = [“降龙十八掌”,“打狗棒”]}Hqg.prototype.sayName = function () { console.log(this.name);}function Hero(name, job) { Hqg.call(this, name); this.job = job}Hero.prototype = new Hqg();Hero.prototype.constructor = Hero;Hero.prototype.sayJob = function () { console.log(this.job)}var gj = new Hero(‘郭靖’, ‘吃饭睡觉’);gj.skill.push(“九阴真经”);console.log(gj);var hr = new Hero(‘黄蓉’, ‘相夫教子’);console.log(hr);先看下输出我们把这个组合继承和之前的两个原型链继承和借用构造函数继承进行比较不难发现组合继承融合了他们的优点,成为javascript中最常用的继承模式今天就讨论前三个,还有三个明天继续,不见不散参考链接你们真的了解JS的继承嘛? ...

December 19, 2018 · 1 min · jiezi

javascript面向对象与原型

昨天我们讲了在面向对象中创建对象的几种方式工厂模式构造函数模式工厂模式创建的对象,像工厂一样来创建对象,创建的每一个对象都是通过new Object()来创建的,原型直指Object()构造函数似乎不错,但有的时候我们需要对属性和方法进行修改,属性vue的同学应该都遇到过这种情况,我们需要声明一些全局变量,我们一般这么做//封装全局的ajax请求 Vue.prototype.$http = function (url, options) { //部分代码省略 return fetch(url, options) } // 注入 全局的wxSDK Vue.prototype.$wx = Wx这时候就用到了原型我之前就用了大量篇幅讲过javascript的原型,这次遇到了面向对象,换个角度再次讨论原型模式创建对象我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。也就是说,不用再构造函数中定义对象的实例信息,而是将这些属性和方法添加到原型对象中一个????:function Hero() {}Hero.prototype.name = “欧阳锋"Hero.prototype.nickname = “西毒"Hero.prototype.doSth = function () { console.log(‘学习九阴真经’);}const hero1 = new Hero()console.log(hero1.name); //=>欧阳锋console.log(hero1.nickname); //=>西毒hero1.doSth() //=>学习九阴真经const hero2 = new Hero()console.log(hero2.name); //=>欧阳锋console.log(hero2.nickname); //=>西毒hero2.doSth() //=>学习九阴真经我们看下hero1console.log(hero1);我们可以通过对象实例访问保存在原型中的值,但是我们不能通过对象实例重写原型中的值,强制的重写也可以function Hero() {}Hero.prototype.name = “欧阳锋"Hero.prototype.nickname = “西毒"Hero.prototype.doSth = function () { console.log(‘学习九阴真经’);}const hero1 = new Hero()hero1.proto.name=‘黄药师’console.log(hero1.name);//=>黄药师const hero2 = new Hero()console.log(hero2.name);//=>黄药师这么做就是修改了原型链,通过原型创建的其他实例也会同步更改,这样不是我们所希望的结果怎么办?实例属性屏蔽同名的原型属性在实例中添加属性,使这个属性和原型中的属性同名,这个属性会屏蔽掉(也可以理解为覆盖掉)原型中的那个属性继续????的????function Hero() {}Hero.prototype.name = “欧阳锋"Hero.prototype.nickname = “西毒"Hero.prototype.doSth = function () { console.log(‘学习九阴真经’);}const hero1 = new Hero()const hero2 = new Hero()hero1.name=“黄药师"console.log(hero1.name);//=>黄药师console.log(hero2.name);//=>欧阳锋我们看下hero1console.log(hero1);黄药师在实例中,欧阳锋在原型中,我们访问hero1.name,会先在实例上去搜索name属性,实例上存在name属性,就返回实例上的name属性,停止搜索。我们再去访问hero2.name,同样的会在实例上进行搜索name属性,但是没搜到,继续顺着原型链进行搜索,搜到了,返回原型中的name属性。我们在原型中添加一个属性,这个属性会屏蔽掉原型中的同名属性,也就是说会阻止我们访问原型中的同名属性,但是不会修改,如果我们把这个属性设置为null,会怎么样呢?//部分代码省略const hero1 = new Hero()const hero2 = new Hero()hero1.name=nullconsole.log(hero1.name);//=>null(来自实例)console.log(hero2.name);//=>欧阳锋(来自原型)所以说,就算设置为null,结论是和上面是相同的,只要在实例上存在这个要访问的属性,就会停止搜索实例中的属性我不想要了怎么办,设置null也无效,使用delete删除//部分代码省略const hero1 = new Hero()const hero2 = new Hero()hero1.name=nulldelete(hero1.name)//删除实例中的属性console.log(hero1.name);//=>欧阳锋(来自原型)console.log(hero2.name);//=>欧阳锋(来自原型)判断属性属于实例还是原型有个内置方法hasOwnProperty()用来判断属性是否来自于实例,属于实例则返回true,否则false//部分代码省略const hero1 = new Hero()const hero2 = new Hero()hero1.name=‘黄药师’// console.log(hero1.name);// console.log(hero2.name);console.log(hero1.hasOwnProperty(’name’));//=>trueconsole.log(hero2.hasOwnProperty(’name’));//=>false还有一个in操作符,如果对象能访问到指定属性就返回true//部分代码省略const hero1 = new Hero()const hero2 = new Hero()hero1.name=‘黄药师’console.log(’name’ in hero1);//=>trueconsole.log(’name’ in hero2);//=>true我们将in和hasOwnProperty()结合在一起,封装一个方法用来判断属性来自原型还是实例const hero1 = new Hero()const hero2 = new Hero()hero1.name = ‘黄药师’//true=>来自原型//false=>来自实例function attrFromProto(obj, attr) { return !obj.hasOwnProperty(attr) && (attr in obj)}console.log(attrFromProto(hero1, ’name’));//=>false(来自实例)console.log(attrFromProto(hero2, ’name’));//=>true(来自原型)原型模式创建对象的简化版????我们创建对象是这样的function Hero() {}Hero.prototype.name = “欧阳锋"Hero.prototype.nickname = “西毒"Hero.prototype.doSth = function () { console.log(‘学习九阴真经’);}我能重复的写prototype,我们结合对象字面量简化一下function Hero() {}Hero.prototype = { name: “欧阳锋”, nickname: “西毒”, doSth: function () { console.log(‘学习九阴真经’); }}通过原型模式创建对象注意事项在原型中添加公有方法是值得鼓励的,类似于我们在vue中添加全局ajax,添加一下基本类型的变量也可以,但是当添加引用类型的属性时候问题就出现了function Hero() {}Hero.prototype.name = “欧阳锋"Hero.prototype.nickname = “西毒"Hero.prototype.skill = [“灵蛇拳”, “神驼雪山掌”]Hero.prototype.doSth = function () { console.log(‘学习九阴真经’);}const hero1 = new Hero()const hero2 = new Hero()hero1.skill.push(‘灵蛇杖法’)console.log(hero1.skill);//=>[“灵蛇拳”, “神驼雪山掌”, “灵蛇杖法”]console.log(hero2.skill);//=>[“灵蛇拳”, “神驼雪山掌”, “灵蛇杖法”]我只在hero1实例中push了“灵蛇杖法”,结果影响到了hero2,当在实例对象上添加引用类型时要格外小心今天就到这里,明天不见不散收集整理了一些电子书,有需要的,在公众号后台回复“电子书”即可领取原文链接 ...

December 17, 2018 · 1 min · jiezi

谈一谈javascript面向对象

从今天起我们开始讨论javascript的面向对象面向对象概念理解面向对象语言有个标志=>它们都具有类的概念,通过类可以创建任意多个具有相同属性和方法的对象。面向对象有三大特性封装继承多态但JS中对象与纯面向对象语言中的对象是不同的JS中的对象:无序属性的集合,其属性可以包含基本值、对象或者函数。可以简单理解为JS的对象是一组无序的值,其中的属性或方法都有一个名字,根据这个名字可以访问相映射的值(值可以是基本值/对象/方法)。创建对象的基本方法我们前面在讲原型链的时候说过,两种创建对象的方法对象字面量(对象直接量)这是最快的一个????: const hero = { name:“欧阳锋”, nickname:“西毒”, doSth:function(){ console.log(‘灵蛇杖法’); }⚠️创建对象的属性名并不强制使用引号包裹,除了以下几种情况属性名中包含空格属性名中包含连字符(中划线)属性名中包含保留字const obj={ “go home”:“包含了空格”, “go-home”:“包含了连字符”, “for”:“这是保留字”}new 实例化一个对象通过new运算符创建并实例化一个新对象,new后面是一个构造函数const hero = new Object()hero.name = “欧阳锋"hero.nickname = “西毒"hero.doSth = function () { console.log(‘灵蛇杖法’);}两种创建方法是一样的创建对象通过以上两种方式似乎足够了,但是当场景稍微复杂一点,问题就显现出来了当我门创建很多结构相同的对象时,会产生大量的重复代码,为了解决这个问题,出现了一个解决方案工厂模式工厂模式抽象了创建具体对象的过程,因为javascript无法创建类,开发人员就发明了一种函数,用函数来封装以特定接口创建对象一个????:function createHero(name,nickname,doSth) { const obj = new Object() obj.name=name obj.nickname=nickname obj.doSth = function () { console.log(doSth); } return obj}const hero1 = createHero(“欧阳锋”,“西毒”,“灵蛇杖法”)const hero2 = createHero(“黄药师”,“东邪”,“碧海潮生曲”)console.log(hero1)看下输出:hero1和hero2都直接继承自Object实例,工厂模式就是像工厂一样来创建对象,创建的每一个对象都是通过new Object()来创建的后来,开发人员有发现了更好的模式构造函数模式我们之前讨论过,通过使用自定义构造函数来实例化对象function Hero(name, nickname, doSth) { this.name = name this.nickname = nickname this.doSth = function () { console.log(doSth); }}const hero3 = new Hero(“欧阳锋”,“西毒”,“灵蛇杖法”)const hero4 = new Hero(“黄药师”,“东邪”,“碧海潮生曲”)console.log(hero3);注意⚠️:创建自定义构造函数,函数名首字母大写,用来和非构造函数进行区分我们继续看下输出:hero3是通过Hero实例化出来的,所以hero3先继承自Hero要创建Hero的新实例,必须使用new操作符,以这种方式调用构造函数实际上会经历以下四个步骤,创建一个新对象将构造函数的作用域赋给新对象(因此this就指向了这个新对象)执行构造函数中的代码(为这个新对象添加属性)返回新对象hero3和hero4都是Hero的实例,同时也是Object的实例instanceof用于判断一个变量是否某个对象的实例console.log(hero3 instanceof Hero);//=>trueconsole.log(hero3 instanceof Object);//=>trueconsole.log(hero4 instanceof Hero);//=>trueconsole.log(hero4 instanceof Object);//=>true属性和方法(公有&私有)????的????中,我们将属性和方法绑定在了构造函数Hero中的this上,hero3和hero4都可以访问这些属性绑定在this上的属性我们称之为公有属性绑定在this上的方法我们称之为公有方法也就是说通过构造函数Hero实例化出来的对象是可以方位公有属性和公有方法的既然有公有属性和公有方法,就一定会有私有属性和私有方法我们做一下调整function Hero(name, nickname, doSth) { let test = “私有属性” function method(){console.log(“私有方法”);} this.name = name this.nickname = nickname this.doSth = function () { console.log(doSth); }}const hero3 = new Hero(“欧阳锋”,“西毒”,“灵蛇杖法”)const hero4 = new Hero(“黄药师”,“东邪”,“碧海潮生曲”)console.log(hero3);看下输出:在hero3中是不存在构造函数的私有属性和私有方法的如果我们这创建完构造函数后,追加一下属性和方法,会怎么样呢?试试看 function Hero(name, nickname, doSth) { let test = “私有属性” function method(){console.log(“私有方法”);} this.name = name this.nickname = nickname this.doSth = function () { console.log(doSth); } } Hero.localAttr=“测试属性” Hero.localMethod=function(){ console.log(‘测试方法’); } Hero.prototype.proAttr=“原型属性” Hero.prototype.proMethod=function(){ console.log(‘原型方法’); } const hero3 = new Hero(“欧阳锋”,“西毒”,“灵蛇杖法”) const hero4 = new Hero(“黄药师”,“东邪”,“碧海潮生曲”) console.log(hero3); console.log(’localAttr测试属性:’,hero3.localAttr); console.log(“localMethod测试方法:",hero3.localMethod); console.log(“proAttr原型属性:",hero3.proAttr); console.log(“proMethod原型方法:",hero3.proMethod);看输出:创建完实例对象后,通过.运算符添加的属性是类静态公有属性(实例化的对象无法访问)通过.运算符添加的方法是类静态公有方法(实例化的对象无法访问)通过原型链添加的属性是公有属性(实例化的对象可以访问)通过原型链添加的方法是公有方法(实例化的对象可以访问)今天就到这里,明天不见不散收集整理了一套js进阶教程,公众号后台回复“js进阶”即可领取参考文献:《javascript高级程序设计》(第三版)《javascript设计模式》《javascript语言精粹》(修订版)原文链接 ...

December 14, 2018 · 1 min · jiezi

如何学习一门计算机编程语言

序言计算机编程是一个实践性很强的“游戏”,对于新入门者,好多人都在想,哪一门编程语言最好,我该从哪开始呢?我的回答是:语言不重要,理解编程思想才是最关键的!所有编程语言都支持的那一部分语言特性(核心子集)才是最核心的部分。所以从实际情况出发,选一门你看着顺眼,目前比较贴近你要做的工作或学习计划的计算机语言,开始你的编程之旅吧。观点阐述语言的核心子集包括哪些部分基本数据类型及运算符,这包括常量、变量、数组(所有的语言都支持一种基本数据结构)的定义与使用;数学运算符与逻辑运行符等知识。分支与循环,这是一门语言中的流程控制部分。基本库函数的使用,编程不可能从零开始,每门语言都有一个基本函数库,帮我们处理基本输入输出、文件读写等能用操作。业界有一个二八规律,其实编程也一样,大家回头看看,我们写了那么多代码,是不是大部分都属于这门语言的核心子集部分?也就是说,我们只要掌握了一门语言的核心子集,就可以开始工作了。常用编程范式面向过程编程(最早的范式,即命令式)面向对象编程(设计模式的概念是从它的实践活动中总结出来的)函数式编程(以纯函数基础,可以任意组合函数,实现集合到集合的流式数据处理)声明式编程(以数据结构的形式来表达程序执行的逻辑)事件驱动编程(其分布式异步事件模式,常用来设计大规模并发应用程序)面向切面编程(避免重复,分离关注点)我们要尽量多的了解不同的编程范式,这样能拓展我们的思路。学习语言的时候,有时可以同时学时两门编程语言,对比学习两门语言的同一概念,让我们能够更容易且深入的理解它。我学习javascript的闭包时,开始怎么也理解不了;我就找了本python书,对比着学,才慢慢的理解了。编程语言分类编译型语言 VS 解释型语言编译型:C、C++、Pascal、Object-C、swift解释型:JavaScript、Python、Erlang、PHP、Perl、Ruby混合型:java、C#,C#,javascript(基于V8)动态结构语言 VS 静态结构语言动态语言:Python、Ruby、Erlang、JavaScript、swift、PHP、SQL、Perl静态语言:C、C++、C#、Java、Object-C强类型语言 VS 弱类型语言强类型:Java、C#、Python、Object-C、Ruby弱类型:JavaScript、PHP、C、C++(有争议,介于强弱之间)各种类型的语言,我们都要有所了解,这样才能够全面的理解编程语言中的各种特性,在面对特定的问题时,才能做出正确的选择。通过实际项目来学习语言(以Typescript为例)项目需求:统一处理不同图形(圆形、长方形、矩形等)的面积计算。面向对象三大原则1.Circle类讲解数据封装概念,将半径与名称封装在类内部,并提供访问方法export default class Circle { private r: number private name: string constructor(r: number) { this.r = r this.name = ‘Circle’ } getName(): string { return this.name } area(): number { return Math.pow(this.r, 2) * PI }}2.长方形与矩形讲解继承概念//rectangle.tsexport default class Rectangle { private a: number private b: number private name: string constructor(a: number, b: number, name?: string) { this.a = a this.b = b if (name === undefined) this.name = ‘Rectangle’ else this.name = name } getName(): string { return this.name } area(): number { return this.a * this.b }}//square.tsexport default class Square extends Rectangle { constructor(a: number) { super(a, a, ‘Square’) }}3.实例统一处理不同的形状一起计算面积,讲解多态概念let shapes = Array<any>()shapes.push(new Circle(2))shapes.push(new Rectangle(5, 4))shapes.push(new Square(3))shapes.forEach((element) => { console.log(shape name: ${element.getName()}; shape area: ${element.area()})})接口概念阐述加入接口,规范形状对外部分操作要求,让错误提早到编译阶段被发现export default interface IShape { getName(): string; area(): number}函数式编程讲解用实例来说明怎样理解函数是一等公民,去掉我们习以为常的函数外层包裹let printData = function(err: any, data: string): void { if (err) console.log(err) else console.log(data)}let doAjax = function (data: string, callback: Function): void { callback(null, data)}doAjax(‘hello’, printData)异步处理中的经验分享在实践过程,处理异步调用容易误解的一个重要概念,异步函数执行的具体流程是什么样的?let pf = function(data: string, n: number, callback: Function) { console.log(begin run ${data}) setTimeout(() => { console.log(end run ${data}) callback(null, data) }, n)}let p = Promise.promisify(pf);(async () => { let ps = Array<any>() ps.push(p(‘1111’, 2000)) ps.push(p(‘2222’, 1000)) ps.push(p(‘3333’, 3000)) await Promise.all(ps)})()视频课程地址以上是《运用typescript进行node.js后端开发精要》视频课程的概要,有兴趣的童鞋可以去观看视频。传送门: 快来学习Typescript,加入会编程、能编程、乐编程的行列吧!资源地址https://github.com/zhoutk/sifou ...

November 9, 2018 · 1 min · jiezi