理解面向对象开发思想 JavaScript 是什么 - 解析执行:轻量级解释型的 - 语言特色:动态,头等函数 (First-class Function) + 又称函数是 JavaScript 中的一等公民 - 执行环境:在宿主环境(host environment)下运行,浏览器是最多见的 JavaScript 宿主环境 + 可是在不少非浏览器环境中也使用 JavaScript ,例如 node.js JavaScript 的组成 - ECMAScript - 语法规范 - 变量、数据类型、类型转换、操做符 - 流程控制语句:判断、循环语句 - 数组、函数、做用域、预解析 - 对象、属性、方法、简单类型和复杂类型的区别 - 内置对象:Math、Date、Array,基本包装类型String、Number、Boolean - Web APIs - BOM - onload页面加载事件,window顶级对象 - 定时器 - location、history - DOM - 获取页面元素,注册事件 - 属性操做,样式操做 - 节点属性,节点层级 - 动态建立元素 - 事件:注册事件的方式、事件的三个阶段、事件对象 #### JavaScript 能够作什么 > 阿特伍德定律: > > Any application that can be written in JavaScript, will eventually be written in JavaScript. > > 任何能够用*JavaScript*来写的应用,最终都将用*JavaScript*来写 > > 阿特伍德 stackoverflow的创始人之一 - [知乎 - JavaScript 能作什么,该作什么?] [最流行的编程语言 JavaScript 能作什么?] ### 浏览器是如何工做的 , 用户界面,咱们所看到的浏览器 Browser engine 浏览器引擎,用来查询和操做渲染引擎 *Rendering engine 用来显示请求的内容,负责解析HTML、CSS,并把解析的内容显示出来 Networking 网络,负责发送网络请求 *JavaScript Interpreter(解析者) JavaScript解析器,负责执行JavaScript的代码 UI Backend UI后端,用来绘制相似组合框和弹出窗口 Data Persistence(持久化) 数据持久化,数据存储 cookie、HTML5中的sessionStorage ``` ### JavaScript 执行过程 JavaScript 运行分为两个阶段: - 预解析 + 全局预解析(全部变量和函数声明都会提早;同名的函数和变量函数的优先级高) + 函数内部预解析(全部的变量、函数和形参都会参与预解析) * 函数 * 形参 * 普通变量 - 执行 先预解析全局做用域,而后执行全局做用域中的代码, 在执行全局代码的过程当中遇到函数调用就会先进行函数预解析,而后再执行函数内代码。 --- ## JavaScript 面向对象编程### 面向对象介绍 #### 什么是对象 > Everything is object (万物皆对象) 对象究竟是什么,咱们能够从两次层次来理解。 **(1) 对象是单个事物的抽象。** 一本书、一辆汽车、一我的均可以是对象,一个数据库、一张网页、一个与远程服务器的链接也能够是对象。当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就能够模拟现实状况,针对对象进行编程。 **(2) 对象是一个容器,封装了属性(property)和方法(method)。** 属性是对象的状态,方法是对象的行为(完成某种任务)。好比,咱们能够把动物抽象为animal对象,使用“属性”记录具体是那一种动物,使用“方法”表示动物的某种行为(奔跑、捕猎、休息等等)。 在实际开发中,对象是一个抽象的概念,能够将其简单理解为:**数据集或功能集**。 ECMAScript-262 把对象定义为:**无序属性的集合,其属性能够包含基本值、对象或者函数**。 严格来说,这就至关于说对象是一组没有特定顺序的值。对象的每一个属性或方法都有一个名字,而每一个名字都映射到一个值。 提示:每一个对象都是基于一个引用类型建立的,这些类型能够是系统内置的原生类型,也能够是开发人员自定义的类型。 #### 什么是面向对象 > 面向对象不是新的东西,它只是过程式代码的一种高度封装,目的在于提升代码的开发效率和可维 护性 。面向对象编程 —— Object Oriented Programming,简称 OOP ,是一种编程开发思想。 它将真实世界各类复杂的关系,抽象为一个个对象,而后由对象之间的分工与合做,完成对真实世界的模拟。 在面向对象程序开发思想中,每个对象都是功能中心,具备明确分工,能够完成接受信息、处理数据、发出信息等任务。 所以,面向对象编程具备灵活、代码可复用、高度模块化等特色,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程,更适合多人合做的大型软件项目。 面向对象与面向过程: - 面向过程就是亲力亲为,事无巨细,面面俱到,步步紧跟,有条不紊 - 面向对象就是找一个对象,指挥得结果 - 面向对象将执行者转变成指挥者 - 面向对象不是面向过程的替代,而是面向过程的封装 面向对象的特性: - 封装性 - 继承性 - [多态性]抽象 扩展阅读: - [维基百科 - 面向对象程序设计] [知乎:如何用一句话说明什么是面向对象思想?] [知乎:什么是面向对象编程思想?#### 程序中面向对象的基本体现 在 JavaScript 中,全部数据类型均可以视为对象,固然也能够自定义对象。 自定义的对象数据类型就是面向对象中的类( Class )的概念。 javascript
咱们以一个例子来讲明面向过程和面向对象在程序流程上的不一样之处。 假设咱们要处理学生的成绩表,为了表示一个学生的成绩,面向过程的程序能够用一个对象示: ```javascript var std1 = { name: 'Michael', score: 98 } var std2 = { name: 'Bob', score: 81 } ``` 而处理学生成绩能够经过函数实现,好比打印学生的成绩: ```javascript function printScore (student) { console.log('姓名:' + student.name + ' ' + '成绩:' + student.score) } ``` 若是采用面向对象的程序设计思想,咱们首选思考的不是程序的执行流程, 而是 `Student` 这种数据类型应该被视为一个对象,这个对象拥有 `name` 和 `score` 这两个属性(Property)。 若是要打印一个学生的成绩,首先必须建立出这个学生对应的对象,而后,给对象发一个 `printScore` 消息,让对象本身把本身的数据打印出来。 抽象数据行为模板(Class): ```javascript function Student(name, score) { this.name = name; this.score = score; this.printScore = function() { console.log('姓名:' + this.name + ' ' + '成绩:' + this.score); } } ``` 根据模板建立具体实例对象(Instance): ```javascript var std1 = new Student('Michael', 98) var std2 = new Student('Bob', 81) ``` 实例对象具备本身的具体行为(给对象发消息): ```javascript std1.printScore() // => 姓名:Michael 成绩:98 std2.printScore() // => 姓名:Bob 成绩 81 ``` 面向对象的设计思想是从天然界中来的,由于在天然界中,类(Class)和实例(Instance)的概念是很天然的。 Class 是一种抽象概念,好比咱们定义的 Class——Student ,是指学生这个概念, 而实例(Instance)则是一个个具体的 Student ,好比, Michael 和 Bob 是两个具体的 Student 。 因此,面向对象的设计思想是: - 抽象出 Class(构造函数) - 根据 Class(构造函数) 建立 Instance - 指挥 Instance 得结果 面向对象的抽象程度又比函数要高,由于一个 Class 既包含数据,又包含操做数据的方法。 ### 建立对象 #### 简单方式 咱们能够直接经过 `new Object()` 建立: ```javascript var person = new Object() person.name = 'Jack' person.age = 18 person.sayName = function () { console.log(this.name) } ``` 每次建立经过 `new Object()` 比较麻烦,因此能够经过它的简写形式对象字面量来建立: ```javascript var person = { name: 'Jack', age: 18, sayName: function () { console.log(this.name) } } ``` 对于上面的写法当然没有问题,可是假如咱们要生成两个 `person` 实例对象呢? ```javascript var person1 = { name: 'Jack', age: 18, sayName: function () { console.log(this.name) } } var person2 = { name: 'Mike', age: 16, sayName: function () { console.log(this.name) } } ``` 经过上面的代码咱们不难看出,这样写的代码太过冗余,重复性过高。 #### 简单方式的改进:工厂函数 咱们能够写一个函数,解决代码重复问题: ```javascript function createPerson (name, age) { return { name: name, age: age, sayName: function () { console.log(this.name) } } } ``` 而后生成实例对象: ```javascript var p1 = createPerson('Jack', 18) var p2 = createPerson('Mike', 18) ``` 这样封装确实爽多了,经过工厂模式咱们解决了建立多个类似对象代码冗余的问题, 但却没有解决对象识别的问题(即怎样知道一个对象的类型)工厂函数没法经过 typeof 判断对象的数据类型 经过实例判断,例如:arr instanceof Array。 ### 构造函数 内容引导: - 构造函数语法 - 分析构造函数 - 构造函数和实例对象的关系 + 实例的 constructor 属性 + instanceof 操做符 - 普通函数调用和构造函数调用的区别 - 构造函数的返回值 - 构造函数的问题 #### 更优雅的工厂函数:构造函数 一种更优雅的工厂函数就是下面这样,构造函数: ```javascript function Person (name, age) { this.name = name this.age = age this.sayName = function () { console.log(this.name) } } var p1 = new Person('Jack', 18) p1.sayName() // => Jack var p2 = new Person('Mike', 23) p2.sayName() // => Mike ``` #### 解析构造函数代码的执行 在上面的示例中,`Person()` 函数取代了 `createPerson()` 函数,可是实现效果是同样的。 这是为何呢? 咱们注意到,`Person()` 中的代码与 `createPerson()` 有如下几点不一样之处: - 没有显示的建立对象 - 直接将属性和方法赋给了 `this` 对象 - 没有 `return` 语句 - 函数名使用的是大写的 `Person` 而要建立 `Person` 实例,则必须使用 `new` 操做符。 以这种方式调用构造函数会经历如下 4 个步骤: 1. 建立一个新对象 2. 将构造函数的做用域赋给新对象(所以 this 就指向了这个新对象) 3. 执行构造函数中的代码 4. 返回新对象 下面是具体的伪代码: ```javascript function Person (name, age) { // 当使用 new 操做符调用 Person() 的时候,实际上这里会先建立一个对象 // var instance = {} // 而后让内部的 this 指向 instance 对象 // this = instance // 接下来全部针对 this 的操做实际上操做的就是 instance this.name = name this.age = age this.sayName = function () { console.log(this.name) } // 在函数的结尾处会将 this 返回,也就是 instance // return this } ``` #### 构造函数和实例对象的关系 使用构造函数的好处不只仅在于代码的简洁性,更重要的是咱们能够识别对象的具体类型了。 在每个实例对象中同时有一个 `constructor` 属性,该属性指向建立该实例的构造函数: ```javascript console.log(p1.constructor === Person) // => true console.log(p2.constructor === Person) // => true console.log(p1.constructor === p2.constructor) // => true ``` 对象的 `constructor` 属性最初是用来标识对象类型的, 可是,若是要检测对象的类型,仍是使用 `instanceof` 操做符更可靠一些: ```javascript console.log(p1 instanceof Person) // => true console.log(p2 instanceof Person) // => true ``` 总结: - 构造函数是根据具体的事物抽象出来的抽象模板 - 实例对象是根据抽象的构造函数模板获得的具体实例对象 - 每个实例对象都具备一个 `constructor` 属性,指向建立该实例的构造函数 + 注意: `constructor` 是实例的属性的说法不严谨,具体后面的原型会讲到 - 能够经过实例的 `constructor` 属性判断实例和构造函数之间的关系 + 注意:这种方式不严谨,推荐使用 `instanceof` 操做符,后面学原型会解释为何 #### 构造函数的问题 使用构造函数带来的最大的好处就是建立对象更方便了,可是其自己也存在一个浪费内存的题: ```javascript function Person (name, age) { this.name = name this.age = age this.type = 'human' this.sayHello = function () { console.log('hello ' + this.name) } } var p1 = new Person('Tom', 18) var p2 = new Person('Jack', 16) ``` 在该示例中,从表面上好像没什么问题,可是实际上这样作,有一个很大的弊端。 那就是对于每个实例对象,`type` 和 `sayHello` 都是如出一辙的内容, 每一次生成一个实例,都必须为重复的内容,多占用一些内存,若是实例对象不少,会形成极大的内存浪费。 ```javascript console.log(p1.sayHello === p2.sayHello) // => false ``` 对于这种问题咱们能够把须要共享的函数定义到构造函数外部: ```javascript function sayHello = function () { console.log('hello ' + this.name) } function Person (name, age) { this.name = name this.age = age this.type = 'human' this.sayHello = sayHello } var p1 = new Person('Top', 18) var p2 = new Person('Jack', 16) console.log(p1.sayHello === p2.sayHello) // => true ``` 这样确实能够了,可是若是有多个须要共享的函数的话就会形成全局命名空间冲突的问题。 你确定想到了能够把多个函数放到一个对象中用来避免全局命名空间冲突的问题: ```javascript var fns = { sayHello: function () { console.log('hello ' + this.name) }, sayAge: function () { console.log(this.age) } } function Person (name, age) { this.name = name this.age = age this.type = 'human' this.sayHello = fns.sayHello this.sayAge = fns.sayAge } var p1 = new Person('lpz', 18) var p2 = new Person('Jack', 16) console.log(p1.sayHello === p2.sayHello) // => true console.log(p1.sayAge === p2.sayAge) // => true ``` 至此,咱们利用本身的方式基本上解决了构造函数的内存浪费问题。 可是代码看起来仍是那么的格格不入,那有没有更好的方式呢? #### 小结 - 构造函数语法 - 分析构造函数 - 构造函数和实例对象的关系 + 实例的 constructor 属性 + instanceof 操做符 - 构造函数的问题 ### 原型 内容引导: - 使用 prototype 原型对象解决构造函数的问题 - 分析 构造函数、prototype 原型对象、实例对象 三者之间的关系 - 属性成员搜索原则:原型链 - 实例对象读写原型对象中的成员 - 原型对象的简写形式 - 原生对象的原型 + Object + Array + String + ... - 原型对象的问题 - 构造的函数和原型对象使用建议 #### 更好的解决方案: `prototype` JavaScript 规定,每个构造函数都有一个 `prototype` 属性,指向另外一个对象。 这个对象的全部属性和方法,都会被构造函数的所拥有。 这也就意味着,咱们能够把全部对象实例须要共享的属性和方法直接定义在 `prototype` 对象上。 ```javascript function Person (name, age) { this.name = name this.age = age } console.log(Person.prototype) Person.prototype.type = 'human' Person.prototype.sayName = function () { console.log(this.name) } var p1 = new Person(...) var p2 = new Person(...) console.log(p1.sayName === p2.sayName) // => true ``` 这时全部实例的 `type` 属性和 `sayName()` 方法, 其实都是同一个内存地址,指向 `prototype` 对象,所以就提升了运行效率。 #### 构造函数、实例、原型三者之间的关系  任何函数都具备一个 `prototype` 属性,该属性是一个对象。 ```javascript function F () {} console.log(F.prototype) // => object F.prototype.sayHi = function () { console.log('hi!') } ``` 构造函数的 `prototype` 对象默认都有一个 `constructor` 属性,指向 `prototype` 对象所在函数。 ```javascript console.log(F.prototype.constructor === F) // => true ``` 经过构造函数获得的实例对象内部会包含一个指向构造函数的 `prototype` 对象的指针 `__proto__`。 ```javascript var instance = new F() console.log(instance.__proto__ === F.prototype) // => true ``` java
`__proto__` 是非标准属性。 node
实例对象能够直接访问原型对象成员。 ```javascript instance.sayHi() // => hi! ``` 总结: - 任何函数都具备一个 `prototype` 属性,该属性是一个对象 - 构造函数的 `prototype` 对象默认都有一个 `constructor` 属性,指向 `prototype` 对象所在函数 - 经过构造函数获得的实例对象内部会包含一个指向构造函数的 `prototype` 对象的指针 `__proto__` - 全部实例都直接或间接继承了原型对象的成员 #### 属性成员的搜索原则:原型链 了解了 **构造函数-实例-原型对象** 三者之间的关系后,接下来咱们来解释一下为何实例对象能够访问原型对象中的成员。 每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具备给定名字的属性 - 搜索首先从对象实例自己开始 - 若是在实例中找到了具备给定名字的属性,则返回该属性的值 - 若是没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具备给定名字的属性 - 若是在原型对象中找到了这个属性,则返回该属性的值 也就是说,在咱们调用 `person1.sayName()` 的时候,会前后执行两次搜索: - 首先,解析器会问:“实例 person1 有 sayName 属性吗?”答:“没有。 - ”而后,它继续搜索,再问:“ person1 的原型有 sayName 属性吗?”答:“有。 - ”因而,它就读取那个保存在原型对象中的函数。 - 当咱们调用 person2.sayName() 时,将会重现相同的搜索过程,获得相同的结果。 而这正是多个对象实例共享原型所保存的属性和方法的基本原理。 总结: - 先在本身身上找,找到即返回 - 本身身上找不到,则沿着原型链向上查找,找到即返回 - 若是一直到原型链的末端尚未找到,则返回 `undefined` #### 实例对象读写原型对象成员 读取: - 先在本身身上找,找到即返回 - 本身身上找不到,则沿着原型链向上查找,找到即返回 - 若是一直到原型链的末端尚未找到,则返回 `undefined` 值类型成员写入(`实例对象.值类型成员 = xx`): - 当实例指望重写原型对象中的某个普通数据成员时实际上会把该成员添加到本身身上 - 也就是说该行为实际上会屏蔽掉对原型对象成员的访问 引用类型成员写入(`实例对象.引用类型成员 = xx`): - 同上 复杂类型修改(`实例对象.成员.xx = xx`): - 一样会先在本身身上找该成员,若是本身身上找到则直接修改 - 若是本身身上找不到,则沿着原型链继续查找,若是找到则修改 - 若是一直到原型链的末端尚未找到该成员,则报错(`实例对象.undefined.xx = xx`) #### 更简单的原型语法 咱们注意到,前面例子中每添加一个属性和方法就要敲一遍 `Person.prototype` 。 为减小没必要要的输入,更常见的作法是用一个包含全部属性和方法的对象字面量来重写整个原型对象: ```javascript function Person (name, age) { this.name = name this.age = age } Person.prototype = { type: 'human', sayHello: function () { console.log('我叫' + this.name + ',我今年' + this.age + '岁了') } } ``` 在该示例中,咱们将 `Person.prototype` 重置到了一个新的对象。 这样作的好处就是为 `Person.prototype` 添加成员简单了,可是也会带来一个问题,那就是原型对象丢失了 `constructor` 成员。 因此,咱们为了保持 `constructor` 的指向正确,建议的写法是: ```javascript function Person (name, age) { this.name = name this.age = age } Person.prototype = { constructor: Person, // => 手动将 constructor 指向正确的构造函数 type: 'human', sayHello: function () { console.log('我叫' + this.name + ',我今年' + this.age + '岁了') } } ``` #### 原生对象的原型 数据库
全部函数都有 prototype 属性对象。 编程