js面向对象编程

学习目标: 

 理解面向对象开发思想 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 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` 对象,因此就提高了运行效率。 #### 构造函数、实例、原型三者之间的关系 ![img](./media/构造函数-实例-原型之间的关系.png) 任何函数都具有一个 `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 ``` 

`__proto__` 是非标准属性。 

实例对象可以直接访问原型对象成员。 ```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 属性对象。 

 

 

posted @ 2017-12-07 10:50  前端大佬李嘉诚  阅读(435)  评论(0编辑  收藏  举报