在js的面向对象中类(class)是在ES6之后出现的,在ES6之前通过构造函数和原型来模拟类的实现机制。
◆构造函数和原型
1.1概述
在典型的OOP的语言中(如Java) , 都存在类的概念,类就是对象的模板,对象就是类的实例,但在ES6之前,JS中并没用引入类的概念。
ES6,全称ECMAScript6.0 , 2015.06发版。但是目前浏览器的JavaScript是ES5版本,大多数高版本的浏览器也支持ES6 ,不过只实现了ES6的部分特性和功能。在ES6之前, 对象不是基于类创建的,而是用一种称为构建函数的特殊函数来定义对象和它们的特征。
创建对象可以通过以下三种方式:
1.对象字面量
2.new Object()
3.自定义构造函数
1.2 构造函数
构造函数是一种特殊的函数 ,主要用来初始化对象,即为对象成员变量赋初始值,它总与new一起使用。我们可以把对象中一些公共的属性和方法抽取出来, 然后封装到这个函数里面。
new在执行时会做四件事情:
①在内存中创建一个新的空对象。
②让this指向这个新的对象。
③执行构造函数里面的代码,给这个新对象添加属性和方法。
④返回这个新对象(所以构造函数里面不需要(return )。
JavaScript的构造函数中可以添加一些成员,可以在构造函数本身上添加,也可以在构造函数内部的this.上添加。通过这两种方式添加的成员,就分别称为静态成员和实例成员。
- 静态成员:在构造函数本上添加的成员称为静态成员,只能由构造函数本身来访问
1 | // 静态成员 在构造函数本身上添加的成员 sex 就是静态成员 |
- 实例成员:在构造函数内部创建的对象成员称为实例成员,只能由实例化的对象来访问.uname 、age、sing都是实例成员
1 | function Star(uname, age) { |
1.3 构造函数的问题
构造函数方法很好用,但是存在浪费内存的问题。每实例化一个对象,都会为复杂类型(函数)开辟一片新的内存空间。而且这每个方法都是同一个方法。

1.4 构造函数原型prototype
构造函数通过 原型分配的 函数 是所有对象所共享的。
JavaScript规定,每一个构造函数都有一个prototype属性,指向另一个对象。 注意这个prototype就是一个对象,这个对象的所有属性和方法,都会被构造函数所拥有。
我们可以把那些不变的方法,直接定义在protptype对象上,这样所有对象的实例就可以共享这些方法。
1 | Star.prototype.sing = function() { |
1.5对象原型__proto__
实例化之后的对象都会有一个属性__proto__
指向构造函数的 prototype原型对象,之所以我们对象可以使用构造函数prototype原型对象的属性和方法,就是因为对象有__proto__
原型的存在。
_ proto_ 对象原型和原型对象prototype是等价的(
ldh.__proto__ === Star.prototype
)_ proto_ 对象原型的意义就在于为对象的查找机制提供一个方向,或者说一 条路线,但是它是一 个非标准属性,因此实际开发中,不可以使用这个属性,它只是内部指向原型对象prototype
系统给对象添加一个__proto__
指向我们构造函数的原型对象 prototype。
方法的查找规则(以上面的ldh.sing();为例): 首先先看ldh 对象身上是否有 sing 方法,如果有就执行这个对象上的sing;如果没有sing 这个方法,因为有proto 的存在,就去构造函数原型对象prototype身上去查找sing这个方法

1.6 constructor构造函数
对象原型(__proto__
) 和构造函数( prototype )原型对象里面都有一个属性 constructor属性, constructor我们称为构造函数,因为它指回构造函数本身。
constructor主要用于记录该对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。
如果我们修改了原来的原型对象,给原型对象赋值的是一个对象,则必须手动的利用constructor指回原来的构造函数
1 | Star.prototype = { |
1.7 构造函数、实例、原型对象三者之间的关系
Star构造函数通过prototype属性指向Star原型对象prototype,反过来constructor又指向了原来的构造函数。ldh是一个实例化对象,通过__proto__
属性指向了Star原型对象prototype,constructor也是指向了原来的构造函数Start。

1.8 原型链
ldh实例化对象通过__proto__
属性指向了Star原型对象prototype,对象都有一个__proto__
属性,Star原型对象prototype的__proto__
指向的则是Object的原型对象prototype,Object的原型对象的__proto__
指向的则是null。这也为我们实例化对象查找成员提供了一条线路,比如下面查找ldh的成员变量就按照箭头方向,最后没有就返回null。

1.9 JavaScript的成员查找机制(规则)
①当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
②如果没有就查找它的原型(也就是_ proto_ 指向的 prototype原型对象)。
③如果还没有就查找原型对象的原型( Object的原型对象)。
④依此类推一直找到Object为止 ( null)。
⑤proto_ 对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线。
当然,如果某个对象自己本身和它的原型上面都有这个属性,按照就近原则。感觉这个java的继承是一样的。所有的对象都继承了Object,Object里面的变量和方法,子类都可以拿来直接使用,甚至修改覆盖。
1 | console.log(ldh.toString());//这里ldh这个对象本身是没有toString()方法,但是Object的原型对象上面有。 |
1.10 原型对象中this指向
在构造函数中,里面this指向的是对象实例。
原型对象函数里面的this 指向的也是实例对象。
1.11 利用原形对象扩展内置对象
可以通过原型对象,对原来的内置对象进行扩展自定义的方法。比如给数组增加自定义求偶数和的功能。
注意:数组和字符串内置对象不能给原型对象覆盖操作Array.prototype= {}
(即使在原型对象中添加 constructor: Array,也不行),只能是Array.prototype.xxx = function({})
的方式。
1 | Array.prototype.sum = function() { |
◆继承
ES6之前并没有给我们提供extends继承。我们可以通过构造函数+原型对象模拟实现继承,被称为组合继承。
2.1 call()
调用这个函数并且修改函数运行时的this指向
1 | fun.call(thisArg, arg1, arg2, ...) |
- thisArg :当前调用函数this的指向对象
- arg1 , arg2 :传递的其他参数
1 | function fn(x, y) { |
2.2 借用构造函数继承父类型属性
核心原理:通过call()把父类型的this指向子类型的this , 这样就可以实现子类型继承父类型的属性。
1 | // 1. 父构造函数 |
2.3 借用原型对象继承父类型方法
1 | Son.prototype = Father.prototype; //这样直接赋值会有问题,如果修改了子原型对象,父原型对象也会跟着一起变化 |

因此我们可以让son的原形对象指向父亲的实例化对象,父亲的实例化对象可以同——proto——
指向fuqin的原形对象,父亲原型对象里面的方法,儿子就可以放心使用。但是此时儿子的原型对象指向的是父亲对象,因此需要修改。
1 | Son.prototype = new Father(); |

2.4 ES6 class本质
1.class本质还是function, 我们也可以简单的认为类就是构造函数的另外一种写法。
2.类的所有方法都定义在类的prototype属性上
3.类创建的实例,里面也有__ proto__
指向类的prototype原型对象
4.所以ES6的类的绝大部分功能, ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
所以ES6的类其实就是语法糖。
语法糖:语法糖就是一种便捷写法.简单理解,有两种方法可以实现同样的功能,但是一种写法更加清晰、 方便,那么这个方法就是语法糖。就比如 i++; 就是 i = i + 1 ;的语法糖。
构造函数的特点:
(1) 构造函数有原型对象prototype
(2) 构造函数原型对象prototype 里面有constructor 指向构造函数本身
(3) 构造函数可以通过原型对象添加方法
(4) 构造函数创建的实例对象有__proto__
原型指向 构造函数的原型对象
◆ES5中的新增方法
3.1 数组方法
迭代(遍历)方法: forEach()、map()、filter()、 some()、 every();
*1) forEach() *
1 | array.forEach(function(currentvalue, index, arr) ) |
- currentValue :数组当前项的值
- index :数组当前项的索引
- arr :数组对象本身
- 如果修改数组里面得知,会改变原数组
2) filter()
filter()方法创建一个新的数组 ,新数组中的元素是通过检查指定数组中符合条件的所有元素,主要用于筛选数组,注意它直接返回一个新数组
1 | array.filter(function(currentValue, index, arr)) |
currentValue: 数组当前项的值
index :数组当前项的索引
arr :数组对象本身
参数不用可以省略。
1 | var arr = [12, 66, 4, 88, 3, 7]; |
3)some() 方法
1 | array.some(function (currentValue,index,arr)); |
some()方法用于检测数组中的元素是否满足指定条件,通俗点查找数组中是否有满足条件的元素,注意它返回值是布尔值,如果查找到这个元素,就返回true,如果查找不到就返回false.
如果找到第一个满足条件的元素,则终止循环不在继续查找.
1 | var arr1 = ['red', 'pink', 'blue']; |
- filter 也是查找满足条件的元素 ,返回的是一个数组 ,而且是把所有满足条件的元素返回回来。
- some 也是查找满足条件的元素是否存在 , 返回的是一个布尔值, 如果查找到第一个满足条件的元素就终止循环。
4)reduce() 方法
**reduce()**
方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。
1 | const array1 = [1, 2, 3, 4]; |
reducer 函数接收4个参数:
Accumulator (acc) (累计器)
CurrentValue (cur) (当前值)
Current ndex (idx) (当前索引)
SourceArray (src) (源数组)
reducer 函数的返回值分配给累计器,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值。
5)其他方法
every() 方法测试一个数组内的所有元素是否都能通过某个指定函数的测试。它返回一个布尔值。
map()
方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。返回一个新数组。他与foreach的区别在于不会改变原来的那个数组,会生成一个新数组。
可以用在对网络请求之后的数据进行处理。
1 | var numbers = [1, 4, 9]; |
forEach() 和 some()的区别
在forEach 里面 return 不会终止迭代, filter 里面 return 不会终止迭代,在some 里面 遇到 return true 就是终止遍历 ,迭代效率更高。
查询商品案例
1.把数据渲染到页面中
2.根据价格显示数据
3.根据商品名称显示数据
3.2 字符串方法
trim()方法会从一一个字符串的两端删除空白字符。
1 | str.trim() //只去除字符串两端的空格,不管字符串中间的空格 |
trim()方法并不影响原字符串本身,它返回的是一个新的字符串。
trim()方法的应用场景 —— 表单取值
以前在表单事件中,我们通过判断表单里面的值为“”就判定为用户没有输入内容,需要给出提示。但是当用户点击了空格之后,表单就有内容,但是这并不是我们想要的,依然需要提示用户。还有一点就是我们取得的数据两端包含的有空格,这样不利于遍历数据,对数据的处理。因此,以后在获取表单时的时候,我们首先需要去除数据两端的空格。
1 | var str = input.value.trim(); |
3.3对象方法
1)Object.keys() 用于获取对象自身所有的属性
1 | object.keys(obj) |
效果类似for..in
返回一个由属性名组成的数组,只返回属性名,不返回属性值。
1 | var obj = { |
2)Object.defineProperty() 定义对象中新属性或修改原有的属性。增加,修改属性。
1 | object.defineProperty (obj, prop, descriptor) |
obj:必需。目标对象
prop :必需。需定义或修改的属性的名字
descriptor :必需。目标属性所拥有的特性
Object.defineProperty()的第三个参数descriptor说明:
value:设置属性的值默认为undefined
writable:值是否可以重写。true | false默认为false 就是是否可以再次修改,如果设置为false,再次修改的值无效。比如id这类属性我们希望他是不会发生变化的。
enumerable:目标属性是否可以被枚举。true | false默认为false。就是是否可以被遍历,比如某个address属性很隐私,我们不希望它遍历出来。
configurable:目标属性是否可以被删除或是否可以再次修改特性true| false 默认为false。比如下面设置了
configurable: false
,当第二次调用defineProperty
方法就会报错。
1 |
|
总结
1 | function Star() { |

prototype
每个函数都有一个 prototype 属性,函数的 prototype 属性指向了一个对象,这个对象正是调用该构造函数而创建的实例的原型,也就是这个例子中的 ldh 和 zxy 的原型。
那什么是原型呢?你可以这样理解:每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型”继承”属性。
构造函数和实例原型之间的关系
实例对象和实例原型之间关系 —— __proto__
这是每一个JavaScript对象(除了 null )都具有的一个属性,叫__proto__
,这个属性会指向该对象的原型。

还是上面的例子,看看
1 | console.log(ldh.__proto__ === Star.prototype);//true |
constructor 每个原型都有一个 constructor 属性指向关联的构造函数。
还是上面的例子
1 | console.log(ldh.__proto__.constructor === Star);//true |

原型的原型
当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。蓝色的部分是原型链。
