学习目标:
- 理解面向对象开发思想
- 掌握 JavaScript 面向对象开发相关模式
- 掌握在 JavaScript 中使用正则表达式
- typora-copy-images-to: media
JavaScript 高级
课程介绍
课程大纲
在线地址:JavaScript 高级
目标
- 理解面向对象开发思想
- 掌握 JavaScript 面向对象开发相关模式
- 掌握在 JavaScript 中使用正则表达式
案例演示
基本概念复习
由于 JavaScript 高级还是针对 JavaScript 语言本身的一个进阶学习,所以在开始之前我们先对以前所学过的 JavaScript 相关知识点做一个快速复习总结。
重新介绍 JavaScript
JavaScript 是什么
- 解析执行:轻量级解释型的,或是 JIT 编译型的程序设计语言
- 语言特点:动态,头等函数 (First-class Function)
- 又称函数是 JavaScript 中的一等公民
- 执行环境:在宿主环境(host environment)下运行,浏览器是最常见的 JavaScript 宿主环境
- 但是在很多非浏览器环境中也使用 JavaScript ,例如 node.js
- 编程范式:基于原型、多范式的动态脚本语言,并且支持面向对象、命令式和声明式(如:函数式编程)编程风格
JavaScript 与浏览器的关系
JavaScript 的组成
组成部分 | 说明 |
---|---|
Ecmascript | 描述了该语言的语法和基本对象 |
DOM | 描述了处理网页内容的方法和接口 |
BOM | 描述了与浏览器进行交互的方法和接口 |
JavaScript 可以做什么
Any application that can be written in JavaScript, will eventually be written in JavaScript.
凡是能用 JavaScript 写出来的,最终都会用 JavaScript 写出来
JavaScript 发展历史
- JavaScript 的诞生
- JavaScript 与 Ecmascript 的关系
- JavaScript 与 Java 的关系
- JavaScript 的版本
- JavaScript 周边大事记
小结
基本概念
本小节快速过即可,主要是对学过的内容做知识点梳理。
- 语法
- 区分大小写
- 标识符
- 注释
- 严格模式
- 语句
- 关键字和保留字
- 变量
- 数据类型
- typeof 操作符
- Undefined
- Null
- Boolean
- Number
- String
- Object
- 操作符
- 流程控制语句
- 函数
JavaScript 中的数据类型
JavaScript 有 5 种简单数据类型:Undefined、Null、Boolean、Number、String
和 1 种复杂数据类型 Object
。
基本类型(值类型)
- Undefined
- Null
- Boolean
- Number
- String
复杂类型(引用类型)
- Object
- Array
- Date
- RegExp
- Function
- 基本包装类型
- Boolean
- Number
- String
- 单体内置对象
- Global
- Math
类型检测
typeof
instanceof
Object.prototype.toString.call()
值类型和引用类型在内存中的存储方式(画图说明)
- 值类型按值存储
- 引用类型按引用存储
值类型复制和引用类型复制(画图说明)
- 值类型按值复制
- 引用类型按引用复制
值类型和引用类型参数传递(画图说明)
- 值类型按值传递
- 引用类型按引用传递
值类型与引用类型的差别
- 基本类型在内存中占据固定大小的空间,因此被保存在栈内存中
- 从一个变量向另一个变量复制基本类型的值,复制的是值的副本
- 引用类型的值是对象,保存在堆内存
- 包含引用类型值的变量实际上包含的并不是对象本身,而是一个指向该对象的指针
- 从一个变量向另一个变量复制引用类型的值的时候,复制是引用指针,因此两个变量最终都指向同一个对象
小结
- 类型检测方式
- 值类型和引用类型的存储方式
- 值类型复制和引用类型复制
- 方法参数中 值类型数据传递 和 引用类型数据传递
JavaScript 执行过程
JavaScript 运行分为两个阶段:
- 预解析
- 全局预解析(所有变量和函数声明都会提前;同名的函数和变量函数的优先级高)
- 函数内部预解析(所有的变量、函数和形参都会参与预解析)
- 函数
- 形参
- 普通变量
- 执行
先预解析全局作用域,然后执行全局作用域中的代码,
在执行全局代码的过程中遇到函数调用就会先进行函数预解析,然后再执行函数内代码。
JavaScript 面向对象编程
面向对象介绍
什么是对象
Everything is object (万物皆对象)
对象到底是什么,我们可以从两次层次来理解。
(1) 对象是单个事物的抽象。
一本书、一辆汽车、一个人都可以是对象,一个数据库、一张网页、一个与远程服务器的连接也可以是对象。当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。
(2) 对象是一个容器,封装了属性(property)和方法(method)。
属性是对象的状态,方法是对象的行为(完成某种任务)。比如,我们可以把动物抽象为animal对象,使用“属性”记录具体是那一种动物,使用“方法”表示动物的某种行为(奔跑、捕猎、休息等等)。
在实际开发中,对象是一个抽象的概念,可以将其简单理解为:数据集或功能集。
ECMAScript-262 把对象定义为:无序属性的集合,其属性可以包含基本值、对象或者函数。
严格来讲,这就相当于说对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都
映射到一个值。
提示:每个对象都是基于一个引用类型创建的,这些类型可以是系统内置的原生类型,也可以是开发人员自定义的类型。
什么是面向对象
面向对象不是新的东西,它只是过程式代码的一种高度封装,目的在于提高代码的开发效率和可维护性。
面向对象编程 —— Object Oriented Programming,简称 OOP ,是一种编程开发思想。
它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。
在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。
因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目。
面向对象与面向过程:
- 面向过程就是亲力亲为,事无巨细,面面俱到,步步紧跟,有条不紊
- 面向对象就是找一个对象,指挥得结果
- 面向对象将执行者转变成指挥者
- 面向对象不是面向过程的替代,而是面向过程的封装
面向对象的特性:
- 封装性
- 继承性
- [多态性]
扩展阅读:
程序中面向对象的基本体现
在 JavaScript 中,所有数据类型都可以视为对象,当然也可以自定义对象。
自定义的对象数据类型就是面向对象中的类( Class )的概念。
我们以一个例子来说明面向过程和面向对象在程序流程上的不同之处。
假设我们要处理学生的成绩表,为了表示一个学生的成绩,面向过程的程序可以用一个对象表示:
1 | var std1 = { name: 'Michael', score: 98 } |
而处理学生成绩可以通过函数实现,比如打印学生的成绩:
1 | function printScore (student) { |
如果采用面向对象的程序设计思想,我们首选思考的不是程序的执行流程,
而是 Student
这种数据类型应该被视为一个对象,这个对象拥有 name
和 score
这两个属性(Property)。
如果要打印一个学生的成绩,首先必须创建出这个学生对应的对象,然后,给对象发一个 printScore
消息,让对象自己把自己的数据打印出来。
抽象数据行为模板(Class):
1 | function Student (name, score) { |
根据模板创建具体实例对象(Instance):
1 | var std1 = new Student('Michael', 98) |
实例对象具有自己的具体行为(给对象发消息):
1 | std1.printScore() // => 姓名:Michael 成绩:98 |
面向对象的设计思想是从自然界中来的,因为在自然界中,类(Class)和实例(Instance)的概念是很自然的。
Class 是一种抽象概念,比如我们定义的 Class——Student ,是指学生这个概念,
而实例(Instance)则是一个个具体的 Student ,比如, Michael 和 Bob 是两个具体的 Student 。
所以,面向对象的设计思想是:
- 抽象出 Class
- 根据 Class 创建 Instance
- 指挥 Instance 得结果
面向对象的抽象程度又比函数要高,因为一个 Class 既包含数据,又包含操作数据的方法。
创建对象
简单方式
我们可以直接通过 new Object()
创建:
1 | var person = new Object() |
每次创建通过 new Object()
比较麻烦,所以可以通过它的简写形式对象字面量来创建:
1 | var person = { |
对于上面的写法固然没有问题,但是假如我们要生成两个 person
实例对象呢?
1 | var person1 = { |
通过上面的代码我们不难看出,这样写的代码太过冗余,重复性太高。
简单方式的改进:工厂函数
我们可以写一个函数,解决代码重复问题:
1 | function createPerson (name, age) { |
然后生成实例对象:
1 | var p1 = createPerson('Jack', 18) |
这样封装确实爽多了,通过工厂模式我们解决了创建多个相似对象代码冗余的问题,
但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
构造函数
内容引导:
- 构造函数语法
- 分析构造函数
- 构造函数和实例对象的关系
- 实例的 constructor 属性
- instanceof 操作符
- 普通函数调用和构造函数调用的区别
- 构造函数的返回值
- 构造函数的静态成员和实例成员
- 函数也是对象
- 实例成员
- 静态成员
- 构造函数的问题
更优雅的工厂函数:构造函数
一种更优雅的工厂函数就是下面这样,构造函数:
1 | function Person (name, age) { |
解析构造函数代码的执行
在上面的示例中,Person()
函数取代了 createPerson()
函数,但是实现效果是一样的。
这是为什么呢?
我们注意到,Person()
中的代码与 createPerson()
有以下几点不同之处:
- 没有显示的创建对象
- 直接将属性和方法赋给了
this
对象 - 没有
return
语句 - 函数名使用的是大写的
Person
而要创建 Person
实例,则必须使用 new
操作符。
以这种方式调用构造函数会经历以下 4 个步骤:
- 创建一个新对象
- 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象)
- 执行构造函数中的代码
- 返回新对象
下面是具体的伪代码:
1 | function Person (name, age) { |
构造函数和实例对象的关系
使用构造函数的好处不仅仅在于代码的简洁性,更重要的是我们可以识别对象的具体类型了。
在每一个实例对象中的__proto__中同时有一个 constructor
属性,该属性指向创建该实例的构造函数:
1 | console.log(p1.constructor === Person) // => true |
对象的 constructor
属性最初是用来标识对象类型的,
但是,如果要检测对象的类型,还是使用 instanceof
操作符更可靠一些:
1 | console.log(p1 instanceof Person) // => true |
总结:
- 构造函数是根据具体的事物抽象出来的抽象模板
- 实例对象是根据抽象的构造函数模板得到的具体实例对象
- 每一个实例对象都具有一个
constructor
属性,指向创建该实例的构造函数- 注意:
constructor
是实例的属性的说法不严谨,具体后面的原型会讲到
- 注意:
- 可以通过实例的
constructor
属性判断实例和构造函数之间的关系- 注意:这种方式不严谨,推荐使用
instanceof
操作符,后面学原型会解释为什么
- 注意:这种方式不严谨,推荐使用
构造函数的问题
使用构造函数带来的最大的好处就是创建对象更方便了,但是其本身也存在一个浪费内存的问题:
1 | function Person (name, age) { |
在该示例中,从表面上好像没什么问题,但是实际上这样做,有一个很大的弊端。
那就是对于每一个实例对象,type
和 sayHello
都是一模一样的内容,
每一次生成一个实例,都必须为重复的内容,多占用一些内存,如果实例对象很多,会造成极大的内存浪费。
1 | console.log(p1.sayHello === p2.sayHello) // => false |
对于这种问题我们可以把需要共享的函数定义到构造函数外部:
1 | function sayHello = function () { |
这样确实可以了,但是如果有多个需要共享的函数的话就会造成全局命名空间冲突的问题。
你肯定想到了可以把多个函数放到一个对象中用来避免全局命名空间冲突的问题:
1 | var fns = { |
至此,我们利用自己的方式基本上解决了构造函数的内存浪费问题。
但是代码看起来还是那么的格格不入,那有没有更好的方式呢?
小结
- 构造函数语法
- 分析构造函数
- 构造函数和实例对象的关系
- 实例的 constructor 属性
- instanceof 操作符
- 构造函数的问题
原型
内容引导:
- 使用 prototype 原型对象解决构造函数的问题
- 分析 构造函数、prototype 原型对象、实例对象 三者之间的关系
- 属性成员搜索原则:原型链
- 实例对象读写原型对象中的成员
- 原型对象的简写形式
- 原生对象的原型
- Object
- Array
- String
- …
- 原型对象的问题
- 构造的函数和原型对象使用建议
更好的解决方案: prototype
Javascript 规定,每一个构造函数都有一个 prototype
属性,指向另一个对象。
这个对象的所有属性和方法,都会被构造函数的实例继承。
这也就意味着,我们可以把所有对象实例需要共享的属性和方法直接定义在 prototype
对象上。
1 | function Person (name, age) { |
这时所有实例的 type
属性和 sayName()
方法,
其实都是同一个内存地址,指向 prototype
对象,因此就提高了运行效率。
构造函数、实例、原型三者之间的关系
任何函数都具有一个 prototype
属性,该属性是一个对象。
1 | function F () {} |
构造函数的 prototype
对象默认都有一个 constructor
属性,指向 prototype
对象所在函数。
1 | console.log(F.constructor === F) // => true |
通过构造函数得到的实例对象内部会包含一个指向构造函数的 prototype
对象的指针 __proto__
。
1 | var instance = new F() |
__proto__
是非标准属性。
实例对象可以直接访问原型对象成员。
1 | 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
。
为减少不必要的输入,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象:
1 | function Person (name, age) { |
在该示例中,我们将 Person.prototype
重置到了一个新的对象。
这样做的好处就是为 Person.prototype
添加成员简单了,但是也会带来一个问题,那就是原型对象丢失了 constructor
成员。
所以,我们为了保持 constructor
的指向正确,建议的写法是:
1 | function Person (name, age) { |
原生对象的原型
所有函数都有 prototype 属性对象。
- Object.prototype
- Function.prototype
- Array.prototype
- String.prototype
- Number.prototype
- Date.prototype
- …
练习:为数组对象和字符串对象扩展原型方法。
原型对象的问题
- 共享数组
- 共享对象
如果真的希望可以被实例对象之间共享和修改这些共享数据那就不是问题。但是如果不希望实例之间共享和修改这些共享数据则就是问题。
一个更好的建议是,最好不要让实例之间互相共享这些数组或者对象成员,一旦修改的话会导致数据的走向很不明确而且难以维护。
原型对象使用建议
- 私有成员(一般就是非函数成员)放到构造函数中
- 共享成员(一般就是函数)放到原型对象中
- 如果重置了
prototype
记得修正constructor
的指向
案例:随机方块
面向对象游戏案例:贪吃蛇
案例相关源码以上传到 GitHub :https://github.com/lipengzhou/new-snake
案例介绍
游戏演示
在线演示地址:贪吃蛇
案例目标
游戏的目的是用来体会js高级语法的使用 不需要具备抽象对象的能力,使用面向对象的方式分析问题,需要一个漫长的过程。
功能实现
搭建页面
放一个容器盛放游戏场景 div#map,设置样式
1 | #map { |
分析对象
- 游戏对象
- 蛇对象
- 食物对象
创建食物对象
Food
属性
- x
- y
- width
- height
- color
方法
- render 随机创建一个食物对象,并输出到map上
创建Food的构造函数,并设置属性
1 | var position = 'absolute'; |
- 通过原型设置render方法,实现随机产生食物对象,并渲染到map上
1 | Food.prototype.render = function (map) { |
- 通过自调用函数,进行封装,通过window暴露Food对象
1 | window.Food = Food; |
创建蛇对象
Snake
属性
- width 蛇节的宽度 默认20
- height 蛇节的高度 默认20
- body 数组,蛇的头部和身体,第一个位置是蛇头
- direction 蛇运动的方向 默认right 可以是 left top bottom
方法
- render 把蛇渲染到map上
Snake构造函数
1 | var position = 'absolute'; |
- render方法
1 | Snake.prototype.render = function(map) { |
- 在自调用函数中暴露Snake对象
1 | window.Snake = Snake; |
创建游戏对象
游戏对象,用来管理游戏中的所有对象和开始游戏
Game
属性
food
snake
map
方法
- start 开始游戏(绘制所有游戏对象)
- 构造函数
1 | function Game(map) { |
- 开始游戏,渲染食物对象和蛇对象
1 | Game.prototype.start = function () { |
游戏的逻辑
写蛇的move方法
- 在蛇对象(snake.js)中,在Snake的原型上新增move方法
- 让蛇移动起来,把蛇身体的每一部分往前移动一下
- 蛇头部分根据不同的方向决定 往哪里移动
1 | Snake.prototype.move = function (food, map) { |
- 在game中测试
1 | this.snake.move(this.food, this.map); |
让蛇自己动起来
私有方法
1
2
3
4什么是私有方法?
不能被外部访问的方法
如何创建私有方法?
使用自调用函数包裹在game.js中 添加runSnake的私有方法,开启定时器调用蛇的move和render方法,让蛇动起来
- 判断蛇是否撞墙
1 | function runSnake() { |
- 在snake中添加删除蛇的私有方法,在render中调用
1 | function remove() { |
- 在game中通过键盘控制蛇的移动方向
1 | function bindKey() { |
- 在start方法中调用
1 | bindKey(); |
判断蛇是否吃到食物
1 | // 在Snake的move方法中 |
其它处理
把html中的js代码放到index.js中
避免html中出现js代码
自调用函数的参数
1 | (function (window, undefined) { |
- 传入window对象
将来代码压缩的时候,可以吧 function (window) 压缩成 function (w)
- 传入undefined
在将来会看到别人写的代码中会把undefined作为函数的参数(当前案例没有使用)
因为在有的老版本的浏览器中 undefined可以被重新赋值,防止undefined 被重新赋值
整理代码
现在的代码结构清晰,谁出问题就找到对应的js文件即可。
通过自调用函数,已经防止了变量命名污染的问题
但是,由于js文件数较多,需要在页面上引用,会产生文件依赖的问题(先引入那个js,再引入哪个js)
将来通过工具把js文件合并并压缩。现在手工合并js文件演示
- 问题1
1 | // 如果存在多个自调用函数要用分号分割,否则语法错误 |
- 问题2
1 | // 当自调用函数 前面有函数声明时,会把自调用函数作为参数 |
继承
什么是继承
- 现实生活中的继承
- 程序中的继承
构造函数的属性继承:借用构造函数
1 | function Person (name, age) { |
构造函数的原型方法继承:拷贝继承(for-in)
1 | function Person (name, age) { |
另一种继承方式:原型继承
1 | function Person (name, age) { |
函数进阶
函数的定义方式
- 函数声明
- 函数表达式
new Function
函数声明
1 | function foo () { |
函数表达式
1 | var foo = function () { |
函数声明与函数表达式的区别
- 函数声明必须有名字
- 函数声明会函数提升,在预解析阶段就已创建,声明前后都可以调用
- 函数表达式类似于变量赋值
- 函数表达式可以没有名字,例如匿名函数
- 函数表达式没有变量提升,在执行阶段创建,必须在表达式执行之后才可以调用
下面是一个根据条件定义函数的例子:
1 | if (true) { |
以上代码执行结果在不同浏览器中结果不一致。
不过我们可以使用函数表达式解决上面的问题:
1 | var f |
函数的调用方式
- 普通函数
- 构造函数
- 对象方法
函数内 this
指向的不同场景
函数的调用方式决定了 this
指向的不同:
调用方式 | 非严格模式 | 备注 |
---|---|---|
普通函数调用 | window | 严格模式下是 undefined |
构造函数调用 | 实例对象 | 原型方法中 this 也是实例对象 |
对象方法调用 | 该方法所属对象 | 紧挨着的对象 |
事件绑定方法 | 绑定事件对象 | |
定时器函数 | window |
这就是对函数内部 this 指向的基本整理,写代码写多了自然而然就熟悉了。
函数也是对象
- 所有函数都是
Function
的实例
call、apply、bind
那了解了函数 this 指向的不同场景之后,我们知道有些情况下我们为了使用某种特定环境的 this 引用,
这时候时候我们就需要采用一些特殊手段来处理了,例如我们经常在定时器外部备份 this 引用,然后在定时器函数内部使用外部 this 的引用。
然而实际上对于这种做法我们的 JavaScript 为我们专门提供了一些函数方法用来帮我们更优雅的处理函数内部 this 指向问题。
这就是接下来我们要学习的 call、apply、bind 三个函数方法。
call
call()
方法调用一个函数, 其具有一个指定的 this
值和分别地提供的参数(参数的列表)。
注意:该方法的作用和 apply()
方法类似,只有一个区别,就是 call()
方法接受的是若干个参数的列表,而 apply()
方法接受的是一个包含多个参数的数组。
语法:
1 | fun.call(thisArg[, arg1[, arg2[, ...]]]) |
参数:
thisArg
- 在 fun 函数运行时指定的 this 值
- 如果指定了 null 或者 undefined 则内部 this 指向 window
arg1, arg2, ...
- 指定的参数列表
apply
apply()
方法调用一个函数, 其具有一个指定的 this
值,以及作为一个数组(或类似数组的对象)提供的参数。
注意:该方法的作用和 call()
方法类似,只有一个区别,就是 call()
方法接受的是若干个参数的列表,而 apply()
方法接受的是一个包含多个参数的数组。
语法:
1 | fun.apply(thisArg, [argsArray]) |
参数:
thisArg
argsArray
apply()
与 call()
非常相似,不同之处在于提供参数的方式。apply()
使用参数数组而不是一组参数列表。例如:
1 | fun.apply(this, ['eat', 'bananas']) |
bind
bind() 函数会创建一个新函数(称为绑定函数),新函数与被调函数(绑定函数的目标函数)具有相同的函数体(在 ECMAScript 5 规范中内置的call属性)。
当目标函数被调用时 this 值绑定到 bind() 的第一个参数,该参数不能被重写。绑定函数被调用时,bind() 也接受预设的参数提供给原函数。
一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。
语法:
1 | fun.bind(thisArg[, arg1[, arg2[, ...]]]) |
参数:
thisArg
- 当绑定函数被调用时,该参数会作为原函数运行时的 this 指向。当使用new 操作符调用绑定函数时,该参数无效。
arg1, arg2, …
- 当绑定函数被调用时,这些参数将置于实参之前传递给被绑定的方法。
返回值:
返回由指定的this值和初始化参数改造的原函数拷贝。
示例1:
1 | this.x = 9; |
示例2:
1 | function LateBloomer() { |
小结
call 和 apply 特性一样
- 都是用来调用函数,而且是立即调用
- 但是可以在调用函数的同时,通过第一个参数指定函数内部
this
的指向 - call 调用的时候,参数必须以参数列表的形式进行传递,也就是以逗号分隔的方式依次传递即可
- apply 调用的时候,参数必须是一个数组,然后在执行的时候,会将数组内部的元素一个一个拿出来,与形参一一对应进行传递
- 如果第一个参数指定了
null
或者undefined
则内部 this 指向 window
bind
- 可以用来指定内部 this 的指向,然后生成一个改变了 this 指向的新的函数
- 它和 call、apply 最大的区别是:bind 不会调用
- bind 支持传递参数,它的传参方式比较特殊,一共有两个位置可以传递
- 在 bind 的同时,以参数列表的形式进行传递
- 在调用的时候,以参数列表的形式进行传递
- 那到底以谁 bind 的时候传递的参数为准呢还是以调用的时候传递的参数为准
- 两者合并:bind 的时候传递的参数和调用的时候传递的参数会合并到一起,传递到函数内部
函数的其它成员
- arguments
- 实参集合
- caller
- 函数的调用者
- length
- 形参的个数
- name
- 函数的名称
1 | function fn(x, y, z) { |
高阶函数
- 函数可以作为参数
- 函数可以作为返回值
作为参数
1 | function eat (callback) { |
作为返回值
1 | function genFun (type) { |
函数闭包
1 | function fn () { |
作用域、作用域链、预解析
- 全局作用域
- 函数作用域
- 没有块级作用域
1 | { |
作用域链示例代码:
1 | var a = 10 |
- 内层作用域可以访问外层作用域,反之不行
什么是闭包
闭包就是能够读取其他函数内部变量的函数,
由于在 Javascript 语言中,只有函数内部的子函数才能读取局部变量,
因此可以把闭包简单理解成 “定义在一个函数内部的函数”。
所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
闭包的用途:
- 可以在函数外部读取函数内部成员
- 让函数内成员始终存活在内存中
一些关于闭包的例子
示例1:
1 | var arr = [10, 20, 30] |
示例2:
1 | console.log(111) |
示例3:投票
示例4:判断类型
示例5:沙箱模式
闭包的思考题
思考题 1:
1 | var name = "The Window"; |
思考题 2:
1 | var name = "The Window"; |
小结
函数递归
递归执行模型
1 | function fn1 () { |
举个栗子:计算阶乘的递归函数
1 | function factorial (num) { |
递归应用场景
- 深拷贝
- 菜单树
- 遍历 DOM 树
正则表达式
- 了解正则表达式基本语法
- 能够使用JavaScript的正则对象
正则表达式简介
什么是正则表达式
正则表达式:用于匹配规律规则的表达式,正则表达式最初是科学家对人类神经系统的工作原理的早期研究,现在在编程语言中有广泛的应用。正则表通常被用来检索、替换那些符合某个模式(规则)的文本。
正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。
正则表达式的作用
- 给定的字符串是否符合正则表达式的过滤逻辑(匹配)
- 可以通过正则表达式,从字符串中获取我们想要的特定部分(提取)
- 强大的字符串替换能力(替换)
正则表达式的特点
- 灵活性、逻辑性和功能性非常的强
- 可以迅速地用极简单的方式达到字符串的复杂控制
- 对于刚接触的人来说,比较晦涩难懂
正则表达式的测试
- 在线测试正则
- 工具中使用正则表达式
- sublime/vscode/word
- 演示替换所有的数字
正则表达式的组成
- 普通字符
- 特殊字符(元字符):正则表达式中有特殊意义的字符
示例演示:
\d
匹配数字ab\d
匹配 ab1、ab2
元字符串
通过测试工具演示下面元字符的使用
常用元字符串
元字符 | 说明 |
---|---|
\d | 匹配数字 |
\D | 匹配任意非数字的字符 |
\w | 匹配字母或数字或下划线 |
\W | 匹配任意不是字母,数字,下划线 |
\s | 匹配任意的空白符 |
\S | 匹配任意不是空白符的字符 |
. | 匹配除换行符以外的任意单个字符 |
^ | 表示匹配行首的文本(以谁开始) |
$ | 表示匹配行尾的文本(以谁结束) |
限定符
限定符 | 说明 |
---|---|
* | 重复零次或更多次 |
+ | 重复一次或更多次 |
? | 重复零次或一次 |
{n} | 重复n次 |
{n,} | 重复n次或更多次 |
{n,m} | 重复n到m次 |
其它
1 | [] 字符串用中括号括起来,表示匹配其中的任一字符,相当于或的意思 |
案例
验证手机号:
1 | ^\d{11}$ |
验证邮编:
1 | ^\d{6}$ |
验证日期 2012-5-01
1 | ^\d{4}-\d{1,2}-\d{1,2}$ |
验证邮箱 xxx@itcast.cn:
1 | ^\w+@\w+\.\w+$ |
验证IP地址 192.168.1.10
1 | ^\d{1,3}\(.\d{1,3}){3}$ |
JavaScript 中使用正则表达式
创建正则对象
方式1:
1 | var reg = new Regex('\d', 'i'); |
方式2:
1 | var reg = /\d/i; |
参数
标志 | 说明 |
---|---|
i | 忽略大小写 |
g | 全局匹配 |
gi | 全局匹配+忽略大小写 |
正则匹配
1 | // 匹配日期 |
###匹配正则表达式
// console.log(/./.test(“除了回车换行以为的任意字符”));//true
// console.log(/.*/.test(“0个到多个”));//true
// console.log(/.+/.test(“1个到多个”));//true
// console.log(/.?/.test(“哈哈”));//true
// console.log(/[0-9]/.test(“9527”));//true
// console.log(/[a-z]/.test(“what”));//true
// console.log(/[A-Z]/.test(“Are”));//true
// console.log(/[a-zA-Z]/.test(“干啥子”));//false
// console.log(/[0-9a-zA-Z]/.test(“9ebg”));//true
// console.log(/b|(ara)/.test(“abra”));//true
// console.log(/[a-z]{2,3}/.test(“arfsf”));//true
console.log(/\d/.test("998"));//true
console.log(/\d*/.test("998"));//true
console.log(/\d+/.test("998"));//true
console.log(/\d{0,}/.test("998"));//true
console.log(/\d{2,3}/.test("998"));//true
console.log(/\D/.test("eat"));//true
console.log(/\s/.test(" "));//true
console.log(/\S/.test("嘎嘎"));//true
console.log(/\w/.test("_"));//true
console.log(/\W/.test("_"));//true
###正则表达式案例
1.验证密码强弱
2.验证邮箱:[0-9a-zA-Z_.-]+[@][0-9a-zA-Z._-]+([.][a-zA-Z]+){1,2}
3.验证中文名字[\u4e00-\u9fa5]
正则提取
1 | // 1. 提取工资 |
正则替换
1 | // 1. 替换所有空白 |
案例:表单验证
1 | QQ号:<input type="text" id="txtQQ"><span></span><br> |
1 | //获取文本框 |
表单验证部分,封装成函数:
1 | var regBirthday = /^\d{4}-\d{1,2}-\d{1,2}$/; |
通过给元素增加自定义验证属性对表单进行验证:
1 | <form id="frm"> |
1 | // 所有的验证规则 |
补充
伪数组和数组
在JavaScript中,除了5种原始数据类型之外,其他所有的都是对象,包括函数(Function)。
对象与数组的关系
在说区别之前,需要先提到另外一个知识,就是 JavaScript 的原型继承。
所有 JavaScript 的内置构造函数都是继承自 Object.prototype
。
在这个前提下,可以理解为使用 new Array()
或 []
创建出来的数组对象,都会拥有 Object.prototype
的属性值。
1 | var obj = {};// 拥有 Object.prototype 的属性值 |
可以得到对象和数组的第一个区别:对象没有数组 Array.prototype 的属性值。
什么是数组
数组具有一个最基本特征:索引,这是对象所没有的,下面来看一段代码:
1 | var obj = {}; |
- obj[2]输出’a’,是因为对象就是普通的键值对存取数据
- 而arr[2]输出’a’ 则不同,数组是通过索引来存取数据,arr[2]之所以输出’a’,是因为数组arr索引2的位置已经存储了数据
- obj.length并不具有数组的特性,并且obj没有保存属性length,那么自然就会输出undefined
- 而对于数组来说,length是数组的一个内置属性,数组会根据索引长度来更改length的值
- 为什么arr.length输出3,而不是1
- 在给数组添加元素时,并没有按照连续的索引添加,所以导致数组的索引不连续,那么就导致索引长度大于元素个数
什么是伪数组
- 拥有 length 属性,其它属性(索引)为非负整数(对象中的索引会被当做字符串来处理,这里你可以当做是个非负整数串来理解)
- 不具有数组所具有的方法
伪数组,就是像数组一样有 length
属性,也有 0、1、2、3
等属性的对象,看起来就像数组一样,但不是数组,比如:
1 | var fakeArray = { |
常见的伪数组有:
- 函数内部的
arguments
- DOM 对象列表(比如通过
document.getElementsByTags
得到的列表) - jQuery 对象(比如
$("div")
)
伪数组是一个 Object,而真实的数组是一个 Array。
伪数组存在的意义,是可以让普通的对象也能正常使用数组的很多方法,比如:
1 | var arr = Array.prototype.slice.call(arguments); |
以上在借用数组的原型方法的时候都可以通过数组直接量来简化使用:
1 | var obj = { |
小结
- 对象没有数组 Array.prototype 的属性值,类型是 Object ,而数组类型是 Array
- 数组是基于索引的实现, length 会自动更新,而对象是键值对
- 使用对象可以创建伪数组,伪数组可以正常使用数组的大部分方法
JavaScript 垃圾回收机制
JavaScript 运行机制:Event Loop
Object
静态成员
- Object.assign()
- Object.create()
- Object.keys()
- Object.defineProperty()
实例成员
- constructor
- hasOwnProperty()
- isPrototypeOf
- propertyIsEnumerable()
- toString()
- valueOf()
附录
A 代码规范
代码风格
校验工具
B Chrome 开发者工具
C 文档相关工具
- 电子文档制作工具: docute
- 流程图工具:DiagramDesigner