学习目标:正则表达式
在 JavaScript 中,全部数据类型均可以视为对象,固然也能够自定义对象。
自定义的对象数据类型就是面向对象中的类( Class )的概念。
咱们以一个例子来讲明面向过程和面向对象在程序流程上的不一样之处。
假设咱们要处理学生的成绩表,为了表示一个学生的成绩,面向过程的程序能够用一个对象表示:数组
var std1 = { name: 'Michael', score: 98 } var std2 = { name: 'Bob', score: 81 }
而处理学生成绩能够经过函数实现,好比打印学生的成绩:闭包
function printScore (student) { console.log('姓名:' + student.name + ' ' + '成绩:' + student.score) }
若是采用面向对象的程序设计思想,咱们首选思考的不是程序的执行流程,app
而是 Student 这种数据类型应该被视为一个对象,这个对象拥有 name 和 score 这两个属性(Property)。函数
若是要打印一个学生的成绩,首先必须建立出这个学生对应的对象,而后,给对象发一个 printScore 消息,让对象本身把本身的数据打印出来。学习
抽象数据行为模板(Class):this
function Student (name, score) { this.name = name this.score = score } Student.prototype.printScore = function () { console.log('姓名:' + this.name + ' ' + '成绩:' + this.score) }
根据模板建立具体实例对象(Instance):spa
var std1 = new Student('Michael', 98) var std2 = new Student('Bob', 81)
实例对象具备本身的具体行为(给对象发消息):prototype
std1.printScore() // => 姓名:Michael 成绩:98 std2.printScore() // => 姓名:Bob 成绩 81
面向对象的设计思想是从天然界中来的,由于在天然界中,类(Class)和实例(Instance)的概念是很天然的。设计
Class 是一种抽象概念,好比咱们定义的 Class——Student ,是指学生这个概念,
而实例(Instance)则是一个个具体的 Student ,好比, Michael 和 Bob 是两个具体的 Student 。
因此,面向对象的设计思想是:
面向对象的抽象程度又比函数要高,由于一个 Class 既包含数据,又包含操做数据的方法。
咱们能够直接经过 new Object() 建立:
var person = new Object() person.name = 'Jack' person.age = 18 person.sayName = function () { console.log(this.name) }
var person = { name: 'Jack', age: 18, sayName: function () { console.log(this.name) } }
对于上面的写法当然没有问题,可是假如咱们要生成两个 person 实例对象呢?
咱们能够写一个函数,解决代码重复问题:
function createPerson (name, age) { return { name: name, age: age, sayName: function () { console.log(this.name) } } }
而后生成实例对象:
var p1 = createPerson('Jack', 18) var p2 = createPerson('Mike', 18)
这样封装确实爽多了,经过工厂模式咱们解决了建立多个类似对象代码冗余的问题,
但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
内容引导:
构造函数和实例对象的关系
构造函数的静态成员和实例成员
一种更优雅的工厂函数就是下面这样,构造函数:
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() 有如下几点不一样之处:
而要建立 Person 实例,则必须使用 new 操做符。
以这种方式调用构造函数会经历如下 4 个步骤:
下面是具体的伪代码:
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 }
使用构造函数的好处不只仅在于代码的简洁性,更重要的是咱们能够识别对象的具体类型了。
在每个实例对象中的_proto_中同时有一个 constructor 属性,该属性指向建立该实例的构造函数:
console.log(p1.constructor === Person) // => true console.log(p2.constructor === Person) // => true console.log(p1.constructor === p2.constructor) // => true
对象的 constructor 属性最初是用来标识对象类型的,
可是,若是要检测对象的类型,仍是使用 instanceof 操做符更可靠一些:
console.log(p1 instanceof Person) // => true console.log(p2 instanceof Person) // => true
使用构造函数带来的最大的好处就是建立对象更方便了,可是其自己也存在一个浪费内存的问题:
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('lpz', 18) var p2 = new Person('Jack', 16)
在该示例中,从表面上好像没什么问题,可是实际上这样作,有一个很大的弊端。那就是对于每个实例对象,type 和 sayHello 都是如出一辙的内容,每一次生成一个实例,都必须为重复的内容,多占用一些内存,若是实例对象不少,会形成极大的内存浪费。
console.log(p1.sayHello === p2.sayHello) // => false
对于这种问题咱们能够把须要共享的函数定义到构造函数外部:
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('lpz', 18) var p2 = new Person('Jack', 16) console.log(p1.sayHello === p2.sayHello) // => true
这样确实能够了,可是若是有多个须要共享的函数的话就会形成全局命名空间冲突的问题。
你确定想到了能够把多个函数放到一个对象中用来避免全局命名空间冲突的问题:
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
至此,咱们利用本身的方式基本上解决了构造函数的内存浪费问题。
小结
构造函数和实例对象的关系
内容引导:
原生对象的原型
Javascript 规定,每个构造函数都有一个 prototype 属性,指向另外一个对象。
这个对象的全部属性和方法,都会被构造函数的实例继承。
这也就意味着,咱们能够把全部对象实例须要共享的属性和方法直接定义在 prototype 对象上。
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 属性,该属性是一个对象。
function F () {} console.log(F.prototype) // => object F.prototype.sayHi = function () { console.log('hi!') }
构造函数的 prototype 对象默认都有一个 constructor 属性,指向 prototype 对象所在函数。
console.log(F.constructor === F) // => true
经过构造函数获得的实例对象内部会包含一个指向构造函数的 prototype 对象的指针 __proto__。
var instance = new F() console.log(instance.__proto__ === F.prototype) // => true
<p class="tip">
proto 是非标准属性。
</p>
实例对象能够直接访问原型对象成员。
instance.sayHi() // => hi!
总结:
了解了 构造函数-实例-原型对象 三者之间的关系后,接下来咱们来解释一下为何实例对象能够访问原型对象中的成员。
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具备给定名字的属性
也就是说,在咱们调用 person1.sayName() 的时候,会前后执行两次搜索:
而这正是多个对象实例共享原型所保存的属性和方法的基本原理。
总结:
咱们注意到,前面例子中每添加一个属性和方法就要敲一遍 Person.prototype 。
为减小没必要要的输入,更常见的作法是用一个包含全部属性和方法的对象字面量来重写整个原型对象:
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 的指向正确,建议的写法是:
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 + '岁了') } }
<p class="tip">
全部函数都有 prototype 属性对象。
</p>
练习:为数组对象和字符串对象扩展原型方法
function Person (name, age) { this.type = 'human' this.name = name this.age = age } function Student (name, age) { // 借用构造函数继承属性成员 Person.call(this, name, age) } var s1 = Student('张三', 18) console.log(s1.type, s1.name, s1.age) // => human 张三 18
function Person (name, age) { this.type = 'human' this.name = name this.age = age } Person.prototype.sayName = function () { console.log('hello ' + this.name) } function Student (name, age) { Person.call(this, name, age) } // 原型对象拷贝继承原型对象成员 for(var key in Person.prototype) { Student.prototype[key] = Person.prototype[key] } var s1 = Student('张三', 18) s1.sayName() // => hello 张三
function Person (name, age) { this.type = 'human' this.name = name this.age = age } Person.prototype.sayName = function () { console.log('hello ' + this.name) } function Student (name, age) { Person.call(this, name, age) } // 利用原型的特性实现继承 Student.prototype = new Person() var s1 = Student('张三', 18) console.log(s1.type) // => human s1.sayName() // => hello 张三
函数的调用方式决定了 this 指向的不一样:
这就是对函数内部 this 指向的基本整理,写代码写多了天然而然就熟悉了。
函数也是对象
call、apply、bind
那了解了函数 this 指向的不一样场景以后,咱们知道有些状况下咱们为了使用某种特定环境的 this 引用,
这时候时候咱们就须要采用一些特殊手段来处理了,例如咱们常常在定时器外部备份 this 引用,而后在定时器函数内部使用外部 this 的引用。
然而实际上对于这种作法咱们的 JavaScript 为咱们专门提供了一些函数方法用来帮咱们更优雅的处理函数内部 this 指向问题。
这就是接下来咱们要学习的 call、apply、bind 三个函数方法。
call
call() 方法调用一个函数, 其具备一个指定的 this 值和分别地提供的参数(参数的列表)。
<p class="danger">
注意:该方法的做用和 apply() 方法相似,只有一个区别,就是 call() 方法接受的是若干个参数的列表,而 apply() 方法接受的是一个包含多个参数的数组。
</p>
语法:
fun.call(thisArg[, arg1[, arg2[, ...]]])
参数:
thisArg
arg1, arg2, ...
apply
apply() 方法调用一个函数, 其具备一个指定的 this 值,以及做为一个数组(或相似数组的对象)提供的参数。
<p class="danger">
注意:该方法的做用和 call() 方法相似,只有一个区别,就是 call() 方法接受的是若干个参数的列表,而 apply() 方法接受的是一个包含多个参数的数组。
</p>
语法:
fun.apply(thisArg, [argsArray])
参数:
apply() 与 call() 很是类似,不一样之处在于提供参数的方式。
apply() 使用参数数组而不是一组参数列表。例如:
fun.apply(this, ['eat', 'bananas'])
bind
bind() 函数会建立一个新函数(称为绑定函数),新函数与被调函数(绑定函数的目标函数)具备相同的函数体(在 ECMAScript 5 规范中内置的call属性)。
当目标函数被调用时 this 值绑定到 bind() 的第一个参数,该参数不能被重写。绑定函数被调用时,bind() 也接受预设的参数提供给原函数。
一个绑定函数也能使用new操做符建立对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。
语法:
fun.bind(thisArg[, arg1[, arg2[, ...]]])
参数:
thisArg
arg1, arg2, ...
返回值:
返回由指定的this值和初始化参数改造的原函数拷贝。
小结
call 和 apply 特性同样
bind
bind 支持传递参数,它的传参方式比较特殊,一共有两个位置能够传递
arguments
caller
length
name
function fn(x, y, z) { console.log(fn.length) // => 形参的个数 console.log(arguments) // 伪数组实参参数集合 console.log(arguments.callee === fn) // 函数自己 console.log(fn.caller) // 函数的调用者 console.log(fn.name) // => 函数的名字 } function f() { fn(10, 20, 30) } f()
闭包就是可以读取其余函数内部变量的函数,
因为在 Javascript 语言中,只有函数内部的子函数才能读取局部变量,
所以能够把闭包简单理解成 “定义在一个函数内部的函数”。
因此,在本质上,闭包就是将函数内部和函数外部链接起来的一座桥梁。
闭包的用途:
一些关于闭包的例子
示例1:
var arr = [10, 20, 30] for(var i = 0; i < arr.length; i++) { arr[i] = function () { console.log(i) } }
示例2:
console.log(111) for(var i = 0; i < 3; i++) { setTimeout(function () { console.log(i) }, 0) } console.log(222)
正则表达式是对字符串操做的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。
方式1:
var reg = new Regex('\d', 'i'); var reg = new Regex('\d', 'gi');
方式2:
var reg = /\d/i; var reg = /\d/gi;
// 1. 提取工资 var str = "张三:1000,李四:5000,王五:8000。"; var array = str.match(/\d+/g); console.log(array); // 2. 提取email地址 var str = "123123@xx.com,fangfang@valuedopinions.cn 286669312@qq.com 二、emailenglish@emailenglish.englishtown.com 286669312@qq.com..."; var array = str.match(/\w+@\w+\.\w+(\.\w+)?/g); console.log(array); // 3. 分组提取 // 3. 提取日期中的年部分 2015-5-10var dateStr = '2016-1-5'; // 正则表达式中的()做为分组来使用,获取分组匹配到的结果用Regex.$1 $2 $3....来获取 var reg = /(\d{4})-\d{1,2}-\d{1,2}/; if (reg.test(dateStr)) { console.log(RegExp.$1);} // 4. 提取邮件中的每一部分 var reg = /(\w+)@(\w+)\.(\w+)(\.\w+)?/; var str = "123123@xx.com"; if (reg.test(str)) { console.log(RegExp.$1); console.log(RegExp.$2); console.log(RegExp.$3);}
// 1. 替换全部空白 var str = " 123AD asadf asadfasf adf "; str = str.replace(/\s/g,"xx"); console.log(str); // 2. 替换全部,|, var str = "abc,efg,123,abc,123,a"; str = str.replace(/,|,/g, "."); console.log(str);
QQ号:<input type="text" id="txtQQ"><span></span><br> 邮箱:<input type="text" id="txtEMail"><span></span><br> 手机:<input type="text" id="txtPhone"><span></span><br> 生日:<input type="text" id="txtBirthday"><span></span><br> 姓名:<input type="text" id="txtName"><span></span><br>
//获取文本框 var txtQQ = document.getElementById("txtQQ"); var txtEMail = document.getElementById("txtEMail"); var txtPhone = document.getElementById("txtPhone"); var txtBirthday = document.getElementById("txtBirthday"); var txtName = document.getElementById("txtName"); // txtQQ.onblur = function () { //获取当前文本框对应的span var span = this.nextElementSibling; var reg = /^\d{5,12}$/; //判断验证是否成功 if(!reg.test(this.value) ){ //验证不成功 span.innerText = "请输入正确的QQ号"; span.style.color = "red"; }else{ //验证成功 span.innerText = ""; span.style.color = ""; } }; //txtEMail txtEMail.onblur = function () { //获取当前文本框对应的span var span = this.nextElementSibling; var reg = /^\w+@\w+\.\w+(\.\w+)?$/; //判断验证是否成功 if(!reg.test(this.value) ){ //验证不成功 span.innerText = "请输入正确的EMail地址"; span.style.color = "red"; }else{ //验证成功 span.innerText = ""; span.style.color = ""; } };
表单验证部分,封装成函数:
var regBirthday = /^\d{4}-\d{1,2}-\d{1,2}$/; addCheck(txtBirthday, regBirthday, "请输入正确的出生日期"); //给文本框添加验证 function addCheck(element, reg, tip) { element.onblur = function () { //获取当前文本框对应的span var span = this.nextElementSibling; //判断验证是否成功 if(!reg.test(this.value) ){ //验证不成功 span.innerText = tip; span.style.color = "red"; }else{ //验证成功 span.innerText = ""; span.style.color = ""; } }; }
经过给元素增长自定义验证属性对表单进行验证:
<form id="frm"> QQ号:<input type="text" name="txtQQ" data-rule="qq"><span></span><br> 邮箱:<input type="text" name="txtEMail" data-rule="email"><span></span><br> 手机:<input type="text" name="txtPhone" data-rule="phone"><span></span><br> 生日:<input type="text" name="txtBirthday" data-rule="date"><span></span><br> 姓名:<input type="text" name="txtName" data-rule="cn"><span></span><br> </form>
// 全部的验证规则 var rules = [ { name: 'qq', reg: /^\d{5,12}$/, tip: "请输入正确的QQ" }, { name: 'email', reg: /^\w+@\w+\.\w+(\.\w+)?$/, tip: "请输入正确的邮箱地址" }, { name: 'phone', reg: /^\d{11}$/, tip: "请输入正确的手机号码" }, { name: 'date', reg: /^\d{4}-\d{1,2}-\d{1,2}$/, tip: "请输入正确的出生日期" }, { name: 'cn', reg: /^[\u4e00-\u9fa5]{2,4}$/, tip: "请输入正确的姓名" }]; addCheck('frm'); //给文本框添加验证 function addCheck(formId) { var i = 0, len = 0, frm =document.getElementById(formId); len = frm.children.length; for (; i < len; i++) { var element = frm.children[i]; // 表单元素中有name属性的元素添加验证 if (element.name) { element.onblur = function () { // 使用dataset获取data-自定义属性的值 var ruleName = this.dataset.rule; var rule =getRuleByRuleName(rules, ruleName); var span = this.nextElementSibling; //判断验证是否成功 if(!rule.reg.test(this.value) ){ //验证不成功 span.innerText = rule.tip; span.style.color = "red"; }else{ //验证成功 span.innerText = ""; span.style.color = ""; } } } } } // 根据规则的名称获取规则对象 function getRuleByRuleName(rules, ruleName) { var i = 0, len = rules.length; var rule = null; for (; i < len; i++) { if (rules[i].name == ruleName) { rule = rules[i]; break; } } return rule; }