构造函数
小编上篇博客中介绍到的通过关键字class方式定义类,然后根据类再创建对象的方式,是ES6中语法,现在很多浏览器对ES6的支持还不是很好,所以也要学习通过构造函数(构建函数)的方式创建对象
问?既然浏览器对ES6的支持不是很好,是不是编写代码时不要使用ES6语法呢?(看完这篇文章你就有答案了)
1.构造函数和原型
1.1对象的三种创建方式–复习
- 字面量方式
var obj = {};
- new关键字
var obj = new Object();
- 构造函数方式
function Person(name,age){ this.name = name; this.age = age; } var obj = new Person('zs',12);
说明:
构造函数是一种特殊的函数,主要用来初始化对象,即为对象成员的值进行初始化,其与new一起使用用于创建对象
这里所说的构造函数与前面讲的类中的构造函数不太一样,类中的构造函数主要是给类中的属性赋值,这里的构造函数中,既可以包属性,也可以包含方法,与上面所说的类的概念更接近,可以看成是J S 彻底向面向对象转变过程中的一种过度,四不像
所以在编写构造函数时,可以参考类的定义方式,将对象的一些公共的属性和方法抽象出来,然后封装到这个函数中
如下面分别使用ES6中的class和ES5中的构造函数的方式生成实例对象
/* *ES6 中创建对象的方法:先使用class关键字创建类,然后使用 new 类名() 的方式创建类的对象 */ class Person { constructor(name, age) { this.name = name this.age = age } speak(){ console.log('哇哇哇哇哇哇哇') } } // 创建类的对象 var p1 = new Person('李白', 20) // 访问类中的属性 console.log(p1.name) p1.speak() console.log('---------------------') /* *ES5 中创建对象的方法:先创建构造函数,然后使用 new 函数名称() 的方式创建这个函数的对象 */ function Star(name, gender) { this.name = name this.gender = gender this.sing=function(){ console.log('小呀嘛小二郎') } } // 通过构造函数创建对象 var s1 = new Star('杜甫', '男') console.log(s1.gender) s1.sing()
图解
注意两点
1)构造函数首字母大写,就给使用class创建类一样
2)与new关键字一起使用,这更与class一样
3)最为重要的一点是:类的编写方法更像是语法糖,目的是让我们能够快速、舒服、优雅的编写类,但是从本质上来说,类其实就是函数。初学者可能感觉不到 class 方式定义类相较于 构造函数的优势:1)没有体会到通过构造函数的方式实现继承的痛苦,所以无法理解通过 ES6 中 extends 实现继承的优势; 2)没有见识过传统的面向变成语言
console.log(typeof Person); console.log(typeof FPerson);
结果
new的解释
在内存中创建一个新的空对象
让this指向这个对象,所以在代码中使用this,就是使用这个对象,this 跟类没有关系
执行构造函数中的代码,给这个对象添加属性和方法,但是方法中的代码不会执行
1.2静态成员和实例成员
1.2.1实例成员
实例成员就是构造函数内部通过this添加的成员 如下列代码中 name age sing 就是实例成员,实例成员只能通过实例化的对象来访问
/*通过this添加的成员就是实例成员 1)实例成员只能通过对象.成员的方式访问 2)实例成员与每个对象相关,也就是每个对象的成员的值是不一样的 */ function Star(name, gender) { this.name = name this.gender = gender this.sing = function () { console.log('小呀嘛小二郎') } } // 通过构造函数创建对象 var s1 = new Star('杜甫', '男') var s2 = new Star('蔡徐坤', '女') console.log(s1.gender) console.log(s2.gender) // 实例成员不能通过 【构造函数名称.成员】 的方式访问 // console.log(Star.gender) // undefined
这点与ES6中 class 创建的类是一样的
class Star { constructor(uname, age) { this.uname = uname this.age = age } sing() { console.log('我会唱歌') } } var ldh=new Star('刘德华',17) console.log(ldh.uname)
1.2.2静态成员
静态成员 在构造函数本身上添加的成员 如下列代码中 就是静态成员,静态成员只能通过构造函数来访问
这与通过 class 关键字定义类时一样的
总结:
实例成员属于对象,所以两个对象的实例成员的值不一样
静态成员,属于构造函数本身,每个对象都属于这个构造函数,所以多个对象共享一个静态成员
1.3构造函数的问题
构造函数方法很好用,但是存在浪费内存的问题。
1.4构造函数原型prototype
构造函数通过原型分配的函数是所有对象所共享的。
JavaScript 规定,每一个构造函数都有一个prototype 属性,指向另一个对象。注意这个prototype就是一个对象,叫做原型对象
ES6 中的类也是一样,因为我们说过,类从本质上来讲,也是一个函数
function Star(name, gender) { this.name = name this.gender = gender this.sing = function () { console.log('小呀嘛小二郎') } } console.log(Star.prototype)
这个对象的所有属性和方法,都会被构造函数所拥有
function Star(name, gender) { this.name = name this.gender = gender this.sing = function () { console.log('小呀嘛小二郎') } } // 为Star的原型对象添加方法(相当于将方法添加到Star的父类上去) Star.prototype.cry=function(){ console.log('我要cry,cry,cry,cry,cry') } Star.prototype.dance=function(){ console.log('一步一步,似魔鬼的步伐') } // 通过输出发现,原型对象上确实有了 cry 方法 console.log(Star.prototype) // 那么作为原型对象的子类的 Star 构造函数自然就拥有了cry 方法 var s1 = new Star('杜甫', '男') s1.cry() s1.dance() var s2 = new Star('蔡徐坤', '女') s2.cry() s2.dance()
还可以使用对象的方式为原型添加多个方法
我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。
解惑:
1)这个原型,就类似于其他语言中的基类。。。。
2)不仅使我们自己使用构造函数或类定义的对象,JS中的内置对象的方法,其实都定义在这个对象的原型对象上
var arr=[] // 查看对象的原型对象使用 __proto__ 属性 console.log(arr.__proto__) // 查看构造函数或类的原型对象使用 prototype 属性 console.log(Array.prototype) // 总结:对象的__proto__ 属性和 类或者构造函数的 protptype 属性指向的是同一个对象
1.5对象原型
构造函数的prototype 属性获取的是当前构造函数的原型对象 构造函数的实例的__proto__属性获取的是当前对象的对象原型 这两者是一个对象,也就是说构造函数的原型对象与此构造函数的实例的对象原型是一个对象
function Star(name, gender) { this.name = name this.gender = gender this.sing = function () { console.log('小呀嘛小二郎') } } // 为Star的原型对象添加方法(相当于将方法添加到Star的父类上去) Star.prototype.cry = function () { console.log('我要cry,cry,cry,cry,cry') } Star.prototype.dance = function () { console.log('一步一步,似魔鬼的步伐') } // 通过输出发现,原型对象上确实有了 cry 方法 console.log(Star.prototype) // 那么作为原型对象的子类的 Star 构造函数自然就拥有了cry 方法 var s1 = new Star('杜甫', '男') // s1.cry() // s1对象的 __proto__属性获取是的是s1对象的对象原型 console.log(s1.__proto__) // 验证构造函数的原型对象与实力的对象原型是一个对象 console.log(Star.prototype===s1.__proto__)
1.6constructor构造函数
对象原型( proto)和构造函数(prototype)原型对象里面都有一个属性 constructor 属性(因为两个其实是一个东西) ,constructor 我们称为构造函数,因为它指回构造函数本身。
下面通过代码理解
function Star(name, gender) { this.name = name this.gender = gender this.sing = function () { console.log('小呀嘛小二郎') } } var s1 = new Star('杜甫', '男') // 输出构造函数的原型对象 console.log(Star.prototype) // 输出对象的对象原型 console.log(s1.__proto__)
通过上面的代码,我们看到,constructor 属性的值确实是这个对象对应的构造函数
不仅可以通过这个属性,获取原型对象所属的构造函数,constructor属性 还可以让原型对象重新指向原来的构造函数
一般情况下,对象的方法都在构造函数的原型对象中设置。
如下面这样
// 为构造方法的原型对象中添加speak 方法 Person.prototype.speak=function(){ console.log('人类说话') }
但是如果加入多个方法,使用上面的方式就比较麻烦
function Star(name, gender) { this.name = name this.gender = gender } // 像原型添加sing方法 Star.prototype.sing = function () { console.log('红星闪闪放光彩'); } // 向原型添加 dance 方法 Star.prototype.dance = function () { console.lo('魔鬼的步伐') } // 向原型添加fly Star.prototype.fly = function () { console.log('上了飞机就拖鞋') }
像上面这样,如果要添加多个方法,我们可以给原型对象采取对象形式赋值,如下面的代码
Star.prototype = { sing: function () { console.log('红星闪闪放光彩'); }, dance: function () { console.lo('魔鬼的步伐') }, fly: function () { console.log('上了飞机就拖鞋') } }
但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象 constructor 就不再指向当前构造函数了
此时,我们可以在修改后的原型对象中,添加一个 constructor 指向原来的构造函数。
结果
总结:如果我们修改了原来的原型对象,给原型对象赋值的是一个对象,则必须手动的利用constructor指回原来的构造函数如:
完整代码
function Star(name, gender) { this.name = name this.gender = gender } Star.prototype = { construcotr:Star, sing: function () { console.log('红星闪闪放光彩'); }, dance: function () { console.lo('魔鬼的步伐') }, fly: function () { console.log('上了飞机就拖鞋') } } console.log(Star.prototype)
1.7原型链
每一个实例对象又有一个__proto__属性,指向的构造函数的原型对象,构造函数的原型对象也是一个对象,也有__proto__属性,这样一层一层往上找就形成了原型链。
可以类比基类,基类就是Object
其实所有的自定义的或者系统内置的构造函数或者类的最顶级的对象原型都是 Object
1.8构造函数实例和原型对象三角关系
1.构造函数的prototype属性指向了构造函数原型对象,构造函数的原型对象的constructor属性指向了构造函数
function Star(name, age) { this.name = name this.age = age } // 获取输出构造方法的原型对象 console.log(Star.prototype) // 获取并输出原型对象的构造函数 console.log(Star.prototype.constructor) // 证明原型对象的constructor属性确实获取的是对应的构造函数 console.log(Star.prototype.constructor===Star)
2.实例对象是由构造函数创建的,实例对象的__proto__属性指向了构造函数的原型对象
// 实例对象由构造函数创建 var s1=new Star('肖战',18) // 实例对象的__proto__属性指向了对应构造函数的原型对象 console.log(s1.__proto__)
3.构造函数的原型对象的constructor属性指向了构造函数,实例对象的原型就是构造函数的原型对象,此对象中有constructor属性也指向了构造函数
重要说明:上面所说的理论同样适用于ES6中
class Star { constructor(name){ this.name=name } } var s=new Star('yhb') console.log(Star.prototype) console.log(s.__proto__)
1.9原型链和成员的查找机制
任何对象都有原型对象,也就是prototype属性,任何原型对象也是一个对象,该对象就有__proto__属性,这样一层一层往上找,就形成了一条链,我们称此为原型链;
当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。 如果没有就查找它的原型(也就是 __proto__指向的 prototype 原型对象)。 如果还没有就查找原型对象的原型(Object的原型对象)。 依此类推一直找到 Object 为止(null)。 __proto__对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线。
1.10原型对象中this指向
构造函数中的this和原型对象的this,都指向我们new出来的实例对象
1、构造函数中的this指向的就是new出来的对象
function Star(name, age) { this.name = name this.age = age console.log(this) } var s1=new Star('李白',20) var s2=new Star('杜甫',17)
2、构造函数的原型对象上的this指向的也是new出来的对象
function Star(name, age) { this.name = name this.age = age console.log(this) } Star.prototype.sing=function(){ console.log(this) } var s1=new Star('李白',20) var s1=new Star('杜甫',17)
通过下面的代码也可以比较两个到底是不是都只想了 new 出来对象
function Star(name, age) { this.name = name this.age = age } var that = null // 构造函数的原型对象中的this Star.prototype.sing = function () { that = this } var s1 = new Star('李白', 20) // 韩炳旭说:sing方法一定要调用,否则无法执行赋值操作 s1.sing() console.log(that === s1)
1.11通过原型为数组扩展内置方法
查看数组的原型
console.log(Array.prototype);
为数组扩展和一个方法,可以计算数组中元素的和
var numbers = new Array(1, 2, 3, 4, 5) // 传统方法求和 // var sum = 0 // for (var i = 0; i < numbers.length; i++) { // sum += numbers[i] // } // 通过改变原型 Array.prototype.sum = function () { var sum = 0 for (var i = 0; i < this.length; i++) { sum += this[i] } return sum } console.log(numbers.sum()) // 再创建一个数组,使用[]方式创建的也是Array类的实例 var arr=[2,3,4,7] console.log(arr.sum()) console.log(Array.prototype)
2.继承
在ES6之前,没有extends 关键字实现类的继承,需要通过构造函数+原型对象的方式模拟实现继承,这种方式叫做组合继承
2.1call()
- call()可以调用函数
- call()可以修改this的指向,使用call()的时候 参数一是修改后的this指向,参数2,参数3(普通参数)…使用逗号隔开连接
下面代码单纯的调用函数
function f1() { console.log(this) //window } f1.call()
下面代码修改this的指向
function f1() { /* *1)默认情况下,this=window *2)使用call修改后,this=p */ console.log(this) } f1.call() // 输出window // 创建对象p var p={} // 将函数f1的this指向修改为对象p f1.call(p) // 输出p
上面call 方法的第一个参数就是this引用的新对象,后面的参数作为函数 f1 的普通参数
代码演示
2.2子构造函数继承父构造函数中的属性
下面利用上面讲到的知识实现构造函数的继承(所谓继承,就是让一个类拥有另一个类中的属性和方法,换言之,就是将A类中的属性和方法的宿主换成B的对象)
步骤如下
- 先定义一个父构造函数
- 再定义一个子构造函数
- 子构造函数继承父构造函数的属性(使用call方法)
通过调试证明上面的结论
上面的案例只演示了属性的继承,方法的继承是一样的
2.3借用原型对象继承方法
上面的方式,可以继承构造函数(为了简单,以后我们称其为类)中的方法,但前面讲过,实际开发中,我们更喜欢将方法注册到类的原型上
function Father(name, age) { this.name = name this.age = age } Father.prototype.speak = function () { console.log('hello') } function Son(gender) { // 注释 ① Father.call(this, '张三', 20) this.gender = gender } var f=new Father('yhb',30) console.log(f) var s = new Son('女') console.log(s)
输出结果
所以,Son 的对象中就没有 speak 方法
其实原因很简单,在Father的原型对象上添加了speak方法,那么Father会继承这个方法,但当前 Son与Father以及Father的原型并没有任何关系,我们只不过通过 call 方法巧妙的使用了其 name和age 属性而已,所以Son中自然没有speak 方法
如何解决呢?
我们可以修改Son的原型为Father的原型,这样的话Son就拥有了speak 方法
输出结果
我们发现,Son中确实拥有了speak 方法
但是问题也随之出现,因为 Father 与 Son 此时的原型对象是一个,所以此时我们如果想 Son的原型对象上添加一个方法
Son.prototype.run=function(){ console.log('run') }
再分别输出两个对象
这就违反了继承的原则:只能儿子继承父亲的,但现在是父亲也能继承儿子的
解决方案:将下面的代码
Son.prototype=Father.prototype
替换成下面这个
Son.prototype=new Father()
也就是让Son的原型对象指向 Father 的对象实例,这个实例的 proto 属性指向Father的对象原型,所以其中具有speak方法,但这仅仅是一个实例对象,就想从Father类拷贝了一份,修改此对象的原型,不会影响ather类的对象,所以修改此实例,不会影响到Father 的原型对象
稍微有点小问题就是,此时 Son的构造函数指向的是Father的构造函数了
console.log(s.constructor)
使用下面的代码再将其构造函数指回Son的构造函数
Son.prototype.constructor=Son
完整代码
/* 利用call 实现构造函数的继承*/ function Father(name, age) { this.name = name this.age = age } Father.prototype.speak = function () { console.log('hello') } function Son(gender) { // 注释 ① Father.call(this, '张三', 20) this.gender = gender } Son.prototype=new Father() // 修改Son的构造函数为Son Son.prototype.constructor=Son // 像Son的原型对象中添加一个方法 Son.prototype.run=function(){ console.log('run') } //var f=new Father('yhb',30) // console.log(f) var s = new Son('女') console.log(s) console.log(s.constructor)
图解:
3.ES5新增方法
关于 ECMAScript 与 JS 的关系,以及 ES 各个版本,大家可以自行百度,小编在这里不做展开说明
ES6 是在ES5的基础上升级,所以先学习ES5新增的特性,后面再学习ES6新增的特性
3.1数组方法forEach遍历数组
此方法用来便利数组,参数为一个函数
以后再想便利数组就不用自己编写循环了
var numbers = [1, 3, 5, 7, 9] numbers.forEach(function (item, index, ary) { console.log('元素:' + item); console.log('索引:' + index); console.log('数组本身:' + ary); })
案例:数组求和
var sum=0 numbers.forEach(function(item){ sum+=item }) console.log(sum)
3.2数组方法filter过滤数组
此方法有一个返回值,用于接收所有符合过滤条件的元素,返回值的类型为数组
var numbers = [1, 2, 3, 4, 5, 6] /*下面代码从数组numbers中筛选出所有>4的元素,然后放到新数组result中 注意:一定要有return 关键字 */ var result = numbers.filter(function (item, index) { return item > 4 }) console.log(result);
输出结果
3.3数组方法some
some 查找数组中是否有满足条件的元素 var arr = [10, 30, 4]; var flag = arr.some(function(item,index,array) { //参数一是:数组元素 //参数二是:数组元素的索引 //参数三是:当前的数组 return item < 3; }); console.log(flag);//false返回值是布尔值,只要查找到满足条件的一个元素就立马终止循环
**some()**
方法测试数组中是不是至少有1个元素通过了被提供的函数测试。它返回的是一个Boolean类型的值。
如果用一个空数组进行测试,在任何情况下它返回的都是false
。
语法:
arr.some(callback(element[, index[, array]])[, thisArg])
1)返回值为布尔值
2)找到满足条件的元素,就终止循环,不会继续向后查找
可以利用这个函数查询数组中是否存在某个值
var numbers = [1, 2, 3, 4, 5, 6] function checkAvailability(arr, val) { var res = arr.some(function (element, index) { return element == val }) return res } console.log(checkAvailability(numbers, 1))
3.4找水果案例
首先将数据定义在数组中,然后便利数组中的数据渲染到表格中,然后可以根据价格或者名称查询想吃的水果
- 定义数组对象数据
var fruits = [ { id: 001, title: '苹果', price: 5 }, { id: 001, title: '苹果', price: 5 }, { id: 002, title: '梨', price: 3 }, { id: 003, title: '香蕉', price: 6 }, { id: 004, title: '砂糖橘', price: 2 } ]
- 使用forEach遍历数据并渲染到页面中
var tbody = document.querySelector('tbody') fruits.forEach(function (item) { var tr = ` <tr> <td>${item.id}</td> <td>${item.title}</td> <td>${item.price}</td> </tr>` tbody.insertAdjacentHTML('beforeend',tr) })
- 根据价格筛选数据将筛选出的数据重新渲染到表中
function setData(ary) { // 先删除以前的数据 tbody.innerHTML = '' ary.forEach(function (item) { var tr = ` <tr> <td>${item.id}</td> <td>${item.title}</td> <td>${item.price}</td> </tr>` tbody.insertAdjacentHTML('beforeend', tr) }) } // 根据价格过滤商品 var btnPrice = document.querySelector('#btnPrice') var min_price = document.querySelector('#min_price') var max_price = document.querySelector('#max_price') btnPrice.addEventListener('click', function () { var result = fruits.filter(function (item) { return item.price >= min_price.value && item.price <= max_price.value }) // 重新渲染数据 setData(result) })
- 根据名称搜索数据
// 根据名称搜索 var btnTitle = document.querySelector('#btnTitle') var title = document.querySelector('#title') btnTitle.addEventListener('click', function () { var result = fruits.filter(function (item) { return item.title == title.value }) // 重新渲染表格 setData(result) })
上面数组中,水果名称是唯一的
使用filter会便利每一个对象,然后比较,查询效率相对较低
find 方法只要查询到一个符合条件的就会停止,所以效率相对较高
btnTitle.addEventListener('click', function () { // find 方法返回的不是数组,而是对象,所以要使用foreach,需要将 // 返回的对象放到数组中 var result = fruits.find(function (item) { console.log(item) return item.title == title.value }) var arr = [] arr.push(result) // 重新渲染表格 setData(arr) })
完整代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> * { margin: 0; padding: 0; } .box { width: 1000px; margin: 50px auto; } table, th, td { border: 1px solid #ccc; } table { width: 800px; border-collapse: collapse; text-align: center; margin-top: 20px; } th, td { padding: 5px 10px; } </style> </head> <body> <div class="box"> <label for="">价格</label> <input type="text" id="min_price"> <span>-</span> <input type="text" id="max_price"> <button id="btnPrice">搜索</button> <label for="">名称</label> <input type="text" id="title"> <button id="btnTitle">搜索</button> <table> <thead> <tr> <th>编号</th> <th>名称</th> <th>价格</th> </tr> </thead> <tbody> </tbody> </table> </div> <script> var fruits = [ { id: 001, title: '苹果', price: 5 }, { id: 002, title: '桃子', price: 10 }, { id: 003, title: '梨', price: 3 }, { id: 004, title: '香蕉', price: 6 }, { id: 005, title: '砂糖橘', price: 2 } ] // 获取tbody var tbody = document.querySelector('tbody') setData(fruits) // 将数组中的数据渲染到表格中 function setData(ary) { // 先删除以前的数据 tbody.innerHTML = '' ary.forEach(function (item) { var tr = ` <tr> <td>${item.id}</td> <td>${item.title}</td> <td>${item.price}</td> </tr>` tbody.insertAdjacentHTML('beforeend', tr) }) } // 根据价格过滤商品 var btnPrice = document.querySelector('#btnPrice') var min_price = document.querySelector('#min_price') var max_price = document.querySelector('#max_price') btnPrice.addEventListener('click', function () { var result = fruits.filter(function (item) { return item.price >= min_price.value && item.price <= max_price.value }) // 重新渲染数据 setData(result) }) // 根据名称搜索 var btnTitle = document.querySelector('#btnTitle') var title = document.querySelector('#title') btnTitle.addEventListener('click', function () { // find 方法返回的不是数组,而是对象,所以要使用foreach,需要将 // 返回的对象放到数组中 var result = fruits.find(function (item) { console.log(item) return item.title == title.value }) var arr = [] arr.push(result) // 重新渲染表格 setData(arr) }) </script> </body> </html>
3.5some和forEach区别
- 如果查询数组中唯一的元素, 用some方法更合适,在some 里面 遇到 return true 就是终止遍历 迭代效率更高
- 在forEach和filter 里面 return 不会终止迭代
3.6 trim方法去除字符串两端的空格
var str = ' hello ' console.log(str.trim()) //hello 去除两端空格 var str1 = ' he l l o ' console.log(str.trim()) //he l l o 去除两端空格
3.7获取对象的属性名
Object.keys(对象) 获取到当前对象中的属性名 ,返回值是一个数组
var obj = { id: 1, pname: '小米', price: 1999, num: 2000 }; var result = Object.keys(obj) console.log(result)//[id,pname,price,num]
3.8Object.defineProperty
Object.defineProperty设置或修改对象中的属性
Object.defineProperty(对象,修改或新增的属性名,{ value:修改或新增的属性的值, writable:true/false,//如果值为false 不允许修改这个属性值 enumerable: false,//enumerable 如果值为false 则不允许遍历 configurable: false //configurable 如果为false 则不允许删除这个属性 属性是否可以被删除或是否可以再次修改特性 })
利用上面的方法,我们可以在不为文本框添加事件的同时,将文本框的值与对象中的某个属性关联起来
var username=document.querySelector('#username') var data = {} Object.defineProperty(data, "title", { get: function () { return username.value }, set: function (newValue) { username.value=newValue }, enumerable: true, configurable: true })