JavaScript基础
什么是JavaScript?
JavaScript是一种基于对象和事件驱动的、并具有安全性能的脚本语言
JavaScript特点
- 是一种解释性脚本语言(代码不进行预编译)。
- 主要用来向HTML(标准通用标记语言下的一个应用)页面添加交互行为。
- 可以直接嵌入HTML页面,但写成单独的js文件有利于结构和行为的分离。
- 跨平台特性,在绝大多数浏览器的支持下,可以在多种平台下运行(如Windows、Linux、Mac、Android、iOS等)。
JavaScript核心语法
变量
什么是变量
变量是计算机内存中存储数据的标识符,根据变量名称可以获取到内存中存储的数据
如何使用变量
var声明变量 var age;
变量的赋值 var age; age = 18;
同时声明多个变量 var age, name, sex; age = 10; name = 'zs';
同时声明多个变量并赋值 var age = 10, name = 'zs';
变量命名
规则 - 必须遵守的,不遵守会报错
- 由字母、数字、下划线、$符号组成,不能以数字开头
- 不能是关键字和保留字,例如:for、while。
- 区分大小写
规范 - 建议遵守的,不遵守不会报错
- 变量名必须有意义
- 遵守驼峰命名法。首字母小写,后面单词的首字母需要大写。例如:userName、userPassword
数据类型
基本数据类型(值类型)
1. number类型
-
数字型进制
最常见的:二进制,八进制,十进制,十六进制
-
数字型范围
最大值
console.log(Number.MAX_VALUE);最小值
console.log(Number.MIN_VALUE); -
数字型的三个特殊值
Infinity, 代表无穷大,大于任何数值
console.log(Number.MIN_VALUE*2);-Infinity,代表无穷小,小于任何数值
console.log(-Number.MIN_VALUE*2);NaN,Not number,代表一个非数值(
isNaN()这个方法判断非数字,并且返回一个值)
2. string类型
2.1、字符方法
chart();
charCodeAt();
//这两个方法都接收一个参数,即基于0的字符位置
2.2、字符串操作方法
1.concat()//用于将一或多个字符串拼接起来,返回拼接得到的新字符串。
concat()//方法可以接受任意多个参数,也就是说可以通过它拼接任意多个字符串。
2.ECMAScript还提供了三个基于子字符串创建新字符串的方法:slice()、substr()和substring()
这三个方法都会返回被操作字符串的一个子字符串,而且也都接收一个或两个参数。第一个参数指定子字符串的开始位置,第二个参数表示在哪里结束。具体来说,slice()和substring()的第二个参数指定的是子字符串最后一个字符后面的位置。而substr()的第二个参数指定的则是返回的字符个数。
当参数是负数时,slice()方法会将传入的负值与字符串的长度相加。substr()方法将负的第一个参数加上字符串的长度,而将负的第二个参数转换为0。最后,substring()方法会把所有负值参数都转化为0。
2.3、字符串位置方法
indexOf() //方法从字符串的开头向后搜索子字符串
lastIndexOf()//方法是从字符串的末尾向前搜索子字符串。
trim()方法这个方法会创建一个字符串的副本,删除前置及后缀的所有空格,然后返回结果
2.4、字符串大小写转换方法
toLowerCase();
toLocaleLowerCase();
toUpperCase();
toLocaleUpperCase()
2.5、字符串的模式匹配方法
match() //方法只接受一个参数,要么是一个正则表达式,要么是一个RegExp对象。
search()//方法始终是从字符串开头向后查找模式。
replace()//方法接受两个参数:第一个参数可以是一个RegExp对象或者一个字符串(这个字符串不会被转换成正则表达式),第二个参数可以使一个字符串或者一个函数。如果第一个参数是字符串,那么只会替换第一个子字符串。要想替换所有子字符串,唯一的办法就是提供一个正则表达式
splite()//方法需要注意的是,在最后一次调用split()返回的数组中,第一项和最后一次项是两个空字符串。之所以这样,是因为通过正则表达式指定的分隔符出现在了字符串的开头(即子字符串”red”)和末尾(即子字符串”yellow”)。
2.6、字符编码转换成一个字符串
fromCharCode()方法
3. boolean类型
JavaScript中的一种原始类型,它只有两种值:true和false。
4. null类型
Null类型是第二个只有一个值的数据类型,这个特殊的值是null。从逻辑角度来看,null值表示一个空对象指针,而这也正是使用typeof操作符检测nul1值时会返回"object"的原因
5. undefined类型
- 当声明一个变量但却没有赋值时,这个变量就被默认赋值为undefined.
- 一个值为undefined的变量和没有定义的变量是不同的
- 如果一个函数没有显式返回一个值,那么返回值将是undefined
- Null类型和Undefined类型的区别
- Null类型实际上是Undefined类型的派生类型,所以javascript将这两种类型视为等值。
- undefined:变量声明但没有初始化时的值
- null:表示一个不存在的对象(Object)
- 可以用
if(!oTemp)来判断一个变量的值是否为undefined或null
基本数据类型特点:
- 基本数据类型是按值访问的,就是说我们可以操作保存在变量中的实际的值;
- 基本数据类型的值是不可变的,任何方法都无法改变一个基本数据类型的值;
- 基本数据类型不可以添加属性和方法
- 基本数据类型的赋值是简单的赋值(如果从一个变量向另一个变量赋值基本类型的值,会在变量对象上创建一个新值,然后把该值赋值到位新变量分配的位置上;
- 基本数据类型的比较是值的比较;
- 基本类型的值在内存中占据固定大小的空间,被保存在栈内存中。(从一个变量向另一个变量复制基本类型的值,会创建这个值的一个副本);
获取数据类型的4种常用方式:
- typeof 可以判断js的基本数据类型,无法判断对象的具体类型
- constructor 返回所有js变量的构造函数(除null,undefined类型)
Object.prototype.toString.call()可以判断具体的对象类型,包括正则等,但是无法判断自定义对象类型- instanceof 变量 instanceof 类型,返回值为布尔值
数据类型转换
一、转为字符串:使用 .toString或者String。
1、 .toString()方法:注意,不可以转null和underfined
2、 String()方法:都能转
3、 隐式转换:num + "",当 + 两边一个操作符是字符串类型,一个操作符是其它类型的时候,会先把其它类型转换成字符串再进行字符串拼接,返回字符串
二、转为数值类型
1、Number():Number()可以把任意值转换成数值,如果要转换的字符串中有一个不是数值的字符,返回NaN
2、parseInt():
3、parseFloat(): parseFloat()把字符串转换成浮点数,parseFloat()和parseInt非常相似,不同之处在与parseFloat会解析第一个. 遇到第二个.或者非数字结束如果解析的内容里只有整数,解析成整数。
4、隐式转换
三、转换为Boolean():0 ''(空字符串) null undefined NaN 会转换成false 其它都会转换成true
1、Boolean():
2、{}
3、隐式转换:!!
复杂类型(引用类型)
1. 对象
对象的概念
称为对象,是一组数据和功能(函数)的集合,使用键值对来保存
对象的初始化
对象在内存中其实有两个区:
- 一个是它本身构造函数
- 另一个指向构造函数的原型。
创建对象的几种方式
- 简单对象的创建:使用对象字面量的方式{},创建一个对象
(最简单,好理解,推荐使用) - 用function(函数)来模拟class (无参构造函数)
- 使用工厂方式来创建(Object关键字)
- 使用原型对象的方式 prototype关键字
- 混合模式(原型和构造函数)
对象属性的几种操作
增
删
改
查
值类型和引用类型
- 简单的数据类型(字符串,数值型,布尔型,undefined,null)
- 复杂数据类型(对象) 变量中保存的是引用的地址
- 查看类型
2. 数组
数组的概念
- JavaScript数组是值的有序集合,每个值叫做一个元素或者元
- 每个元素在数组中有一个位置,以数字表示,称为索引或者下标。
- js的数组是无类型的,数组元素可以是任意类型,同一个数组中的不同元素可能是对象或数组,可以如何顺序也可以重复
- 数组元素的索引不一定要连续,可以有空隙。
- 每个数组都具有一个lengrh属性,length属性就是数组元素的个数。
数组的特点
- JavaScript数组中的默认存储值是undefined,其它编程语言数组的默认存储值是0或者是垃圾数据
- 与其它的编程语言不同,JavaScript可以访问数组中不存在的索引,会返回undefined,而其它的编程语言会报错或返回垃圾数据
- JavaScript可以存储不同类型的数据,而其它的编程语言只能存储一种数据类型的数据
- 当JavaScript中数组的存储空间不够用时,它会自动扩容,而其它的语言数组的大小是固定的,一旦定义了,就无法改变
- JavaScript中分配给数组的存储空间是不连续的,而其他编程语言中分配给数组的存储空间是连续的
数组的基本方法
- push() 在数组尾部添加一个或者多个元素,并且返回数组的新长度
- pop() 删除数组尾部的最后一个元素,并且将这个被删除的元素返回
- toString()将数组转化为字符串
- join()也是将数组的每个元素以指定的字符连接形成新字符串返回,将数组合并为字符串返回,默认使用,连接
- shift() 删除数组的第一个元素,并且返回被删除的元素
- unshift()在数组头部添加一个或者多个元素,并且返回数组的新长度
- concat() 数组的合并,合并后会返回一个新数组,原来的两个数组不会变化
- sort()用于对数组的元素进行排序
- slice() 按指定位置截取复制数组的内容,原数组不会发生改变 10.splice()这个方法可以从指定的位置删除给定数量的元素,并且在这个位置插入需要的元素,并且返回被删除的元素组成的新数组 11.indexOf()从数组的开头(位置 0)开始向后查找。 12.lastIndexOf() 从数组的后面开始向前查找 13.forEach()对数组进行遍历循环,对数组中的每一项运行给定函数。这个方法没有返回值。参数都是function类型,默认有传参,参数分别为:遍历的数组内容;第对应的数组索引,数组本身。 14.map()对数组进行遍历循环,这个方法有返回值,会返回一个与原数组长度相等的新数组 15.some()查找数组中是否有满足条件的元素,如果有就返回true,如果没有就返回false 16.every() 判断数组中是否每一个都满足条件,如果有一个不满足条件,直接跳出,否则的话所有都满足时返回为ture 17.filter()是将所有元素进行判断,将满足条件的元素作为一个新的数组返回 18.reduce()是所有元素调用返回函数,返回值为最后结果,传入的值必须是函数类型:从数组的第1位开始遍历,第0位没有遍历,下标从1开始。其中:刚开始value是数组的第0项,后面每次value都是undefined,如果在函数中使用return 就会将返回值在下次数组的遍历中赋予value 19.Array.isArray() 判断一个对象是不是数组,返回的是布尔值,如果是数组返回true否则返回false
创建数组
- 使用new字段
- 通过字面量
数组的长度和下标
数组的下标实际上就是为数组里面的各个元素进行编号,这个编号是顺序递增进行的,我们可以把数组中的元素看成是正在排队。 数组名.lenght();
取值和赋值
<script>
// 数组一般用来保存一堆相关的数据,方便访问和操作,也方便数据的传输
// 1.数组里的空间可以存放任意类型的数据
var a1 = [110, '你好', '你好a', true, false, undefined, null, 1.111];
console.log(a1);
// 2.数组的赋值
var arr = [22, 33, 44, 55] //通过 创建数组 初始化 数据来赋值
console.log(arr); // [22, 33, 44, 55]
arr[0] = 66; // 通过 中括号[下标] 的方式 赋值!
arr[3] = 88; // 通过 中括号[下标] 的方式 赋值!
console.log(arr); // [66, 33, 44, 88]
// 3.数组的取值
var a = arr[1]; // 通过 中括号[小标] 的方式 取值
console.log(a); // 33
// 4.获取数组的长度
console.log(arr.length); // 4
// 数组大总结:
// 1.数组 元素 -> 数组 空间里的值
// 获取 数组中 第 0 个 元素
// 2.数组 下标 -> 数组 中 每个元素 的 序号,从 0 开始!!!
// 3.length -> 数组的 元素的 个数
</script>
追加数据
直接给数组的下标通过赋值来增加(数组的下标起始值是0);
通过 数组名[数组名.length] 来增加;
通过 数组名.push(参数) 来增加从数组最后一个数据开始增加,push可以带多个参,带几个参,数组最后就增加几个数据;
通过 数组名.unshift(参数)来增加从数组第1个数据开始的参数,unshift可以带多个参,带几个参,数组最开始就增加几个数据;
用 数组名.splice(开始插入的下标数,0,需要插入的参数1,需要插入的参数2,需要插入的参数3……)来增加数组数据;
遍历数组
for循环,也是最常见的
for (let i = 0; i < arr.length; i++) {
console.log(arr[i])
}
forEach()遍历普通数组
arr.forEach( function(item){
console.log(item)
} )
forEach()遍历对象类型数组
const arr = [
{id:1,name:'zhangsan'},
{id:2,name:'lisi'},
{id:3,name:'wangwu'}
]
arr.forEach( function(item){
console.log(item.id + '---' + item.name)
})
map()方法
var newArr = arr.map( function(value,index){ console.log(value + '---' + index) return value + 10 })
console.log(newArr)
for......in 遍历数组
for(let item in arr){
console.log(arr[item])
}
for.....in 遍历对象
const obj = {
a:1,
b:2,
c:3
}
for(let key in obj){
console.log(key + '---' + obj[key] )
}
for.......of 方法 (es6支持)
for(let item of arr){
console.log(item)
}
break和continue
- break是直接结束循环;
- continue是迭代循环,继续执行下一次的循环。
冒泡
冒泡排序是吧一组数组的元素两两比较,交换位置,通过多轮比较,实现从大到小或者从小到大的排序。
var arr = [1,0,5,6,3,9,22,49,20,11,78,9];
// 创建一个新数组
for(var i = 0; i <= arr.length-1; i++){
// 外层循环控制比较几轮
for(var j = 0; j <= arr.length-i-1; j++){
// 内层循环控制每轮比较几个元素
if(arr[j] > arr[j+1]){
// 判断每一次比较的时候,两个数字的大小
// arr[j]是第j个元素
// arr[j+1]是第j+1 个元素
//如果j > j + 1, 把j 和j+i交换,也就是把相对大的值往后排序 也就是从小到大排序
//如果j < j + 1, 把相对小的值往后排 也就是从大到小排序
var temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
// 利用第三方变量交换j 和j + 1 的值
}
}
}
console.log(arr);
算法概念
- 时间复杂度指的是一个算法执行所耗费的时间
- 空间复杂度指运行完一个程序所需内存的大小
- 稳定指如果a=b,a在b的前面,排序后a仍然在b的前面
- 不稳定指如果a=b,a在b的前面,排序后可能会交换位置
3. 函数
JS函数三种定义方法
- 函数关键字function
- 函数字面量(Function Literals)
- Function构造函数
函数的声明和调用
js中声明函数的三种方式
- 函数声明方式
- 函数表达式声明方式
- 使用Function构造函数
函数调用4种方法
一般形式的函数调用 作为对象的方法调用 使用 call 和 apply 动态调用 new 命令间接调用
作用域
- 作用域的概念:作用域是可访问变量的集合。
- 局部作用域:变量在函数内声明,变量为局部作用域。局部变量:只能在函数内部访问。
// 此处不能调用 carName 变量
function myFunction() {
var carName = "Volvo";
// 函数内可调用 carName 变量
}
- 全局作用域:变量在函数外定义,即为全局变量。全局变量有全局作用域: 网页中所有脚本和函数均可使用。 JavaScript 变量生命周期:JavaScript 变量生命周期在它声明时初始化。局部变量在函数执行完毕后销毁。 全局变量在页面关闭后销毁。
函数预解析
- 首先是找到标签按照
- 解析执行环境(这个有点难理解,在下面有具体解释)
- 对标识符( var function)进行解析
匿名函数
8种常用的匿名函数调用方法:
1.使用 !开头,结构清晰,不容易混乱,推荐使用;
!function(){
document.write('ni hao');
}()
2.无法表明函数与之后的()的整体性,不推荐使用。
(function(){
document.write('wo hao');
})();
3.能够将匿名函数与调用的()为一个整体,官方推荐使用;
(function(){
document.write('hello');
}());
4.放在中括号内执行
[function(){
document.write('world');
}()];
5.使用 + 运算符
+function(){
document.write('ni hao');
}()
6.使用 - 运算符
-function(){
document.write('ni hao');
}()
7.使用波浪符 ~
~function(){
document.write('ni hao');
}()
8.使用 void
void function(){
document.write('ni hao');
}()
递归函数
一、什么是递归函数?
- 本质-自己调用自己
- 递归要加结束条件 二、递归函数的特性
- 重复执行
- 调用自身
- 【必须】要有条件控制,避免死循环
函数的参数
一、函数参数:
- 概念: 是函数内部跟函数外部沟通的桥梁。
- 形参: 定义函数时的参数为形参。
- 实参: 调用函数时实际传递出去的参数为实参。
- is not defined:为声明是一个错误(既没有var声明也没有赋值)。
- undefined: 表示没有定义完成(变量只有var声明没有赋值)。 二、多个参数要用","隔开
- 定义函数的时候不需要指定形参的类型。
- 调用的时候实参的类型就是形参的类型。
- 实参和形参是一一对应的
- 形参只能在函数内使用
function sayHello1(a,b) {
console.log(a);
console.log(b);
}
//实参和形参是一一对应的
sayHello1(2,1);
//参数为多个只接收前两个
sayHello1(1,2,3,4);
//a的值是1,b的值是undefined
sayHello1(1);
//形参只能在函数内使用
console.log(a);
三、arguments.length: 获取的是实参的个数
function fn(a,b,c) {
console.log(arguments);
//第一个实参
console.log(arguments[0]);
//第二个是参
console.log(arguments[1]);
//没有第三个实参赋值给c所以是undefined。
console.log(arguments[2]);
console.log(a);
arguments[0] = 3;
console.log(a);
for(var i=0;i<arguments.length;i++) {
if(arguments[i]=="1") {
console.log(arguments[i]);
}
}
}
fn("1",2,3);
流程控制
顺序结构
顺序结构表示程序中的各操作是按照它们出现的先后顺序执行的。
分支结构
1.if-else结构
单分支结构
if(判断条件){...
}
双分支结构
if(判断条件){...
}else{...
}
多分支结构
if(判断条件){...
}else if(){...
}else{...
}
2.switch-case结构
switch(判断条件){
case 表达式1:
代码;
break;
case 表达式2:
代码;
break;
case 表达式3:
代码;
break;
.......
default:代码
}
3.三元运算符
- 三元运算符也被称为条件运算符。
- 该运算符有三个操作数,并且需要判断布尔表达式的值。主要是用来决定哪个值应该赋给变量。
- 语法格式:布尔表达式 ? 表达式1:表达式2
- 当布尔表达式的值为true时,则返回表达式1的值;当布尔表达式的值为false时,则返回表达式2的值
循环结构
1、for循环
var sum=0;
for(var i=0;i<=100;i++){
if(i%2==1){
sum+=i;
}
}
document.write(sum);
2、while循环
.赋初值 while(2控制条件){ 循环体4 增量3}
var i =1
var sum = 0
while(i<100){
sum *+ = i
i++
}
3、do while 循环
do{
循环体;
}while(判断条件)先执行后判断 至少执行一次循环体
for循环与while循环之间的区别
- for() 循环次数明确
- while() 循环次数不明确的情况下
- while() { 循环体} 循环体有可能一次都不执行
do while 与 while 之间的区别
do..while先执行再判断,可以说do..while至少执行一次while先判断再执行,若条件不成立的话可能一次都不执行 要点:
- do..while 主要用于人机交互
- 注意do..while的格式,后面一定要有分号
- do..while 并不等价于 while ,当然也不等价于for
循环嵌套结构
一个for循环里套一个for循环
输出一个3行5列的
for(var i=0;i<3;i++){
for(var j=0;j<5;j++){
document.write('* ')
}
document.write("<br/>");
}
输出一个9*9乘法表
for(var i=0;i<=9;i++){
for(var j=1;j<i;j++){
document.write(i+'*'+j+'='+(i*j)+'  ');
}
document.write('<br/>');
}
javascript高级
构造函数和原型对象
面向编程介绍
什么是对象?
- 对象是单个事物的抽象。
- 对象是一个容器,封装了属性(property)和方法(method)。
什么是面向对象?
面向对象编程 —— Object Oriented Programming,简称 OOP ,是一种编程开发思想。 它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。
面向对象与面向过程:
- 面向过程就是亲力亲为,事无巨细,面面俱到,步步紧跟,有条不紊
- 面向对象就是找一个对象,指挥得结果
- 面向对象将执行者转变成指挥者
- 面向对象不是面向过程的替代,而是面向过程的封装
面向对象的设计思想是:
- 抽象出 Class(构造函数)
- 根据 Class(构造函数) 创建 Instance
- 指挥 Instance 得结果
- 面向对象的抽象程度又比函数要高,因为一个 Class 既包含数据,又包含操作数据的方法。
面向对象的三大特征
- 封装
- 我们平时所用的方法和类都是一种封装,当我们在项目开发中,遇到一段功能的代码在好多地方重复使用的时候,我们可以把他单独封装成一个功能的方法,这样在我们需要使用的地方直接调用就可以了。
- 继承
- 继承在我们的项目开发中主要使用为子类继承父类
- 多态
- 方法重载:重载是指不同的函数使用相同的函数名,但是函数的参数个数或类型不同。调用的时候根据函数的参数来区别不同的函数
- 方法重写:重写(也叫覆盖)是指在派生类中重新对基类中的虚函数(注意是虚函数)重新实现。即函数名和参数都一样,只是函数的实现体不一样
三大特征的优点:
- 封装:封装的优势在于定义只可以在类内部进行对属性的操作,外部无法对这些属性指手画脚,要想修改,也只能通过你定义的封装方法;
- 继承:继承减少了代码的冗余,省略了很多重复代码,开发者可以从父类底层定义所有子类必须有的属性和方法,以达到耦合的目的;
- 多态:多态实现了方法的个性化,不同的子类根据具体状况可以实现不同的方法,光有父类定义的方法不够灵活,遇见特殊状况就捉襟见肘了
工厂函数
- 所谓工厂函数,就是指这些内建函数都是类对象,当你调用他们时,实际上是创建了一个类实例”。
- 意思就是当我调用这个函数,实际上是先利用类创建了一个对象,然后返回这个对象。
- 由于 Javascript 本身不是严格的面向对象的语言(不包含类),实际上来说,Javascript 并没有严格的“工厂函数”,但是在 Javascript中,我们能利用函数模拟类。
1.带参数的工厂函数
像所有函数一样,我们可以通过参数来定义我们的工厂函数 (icecream 冰淇淋),这可以用来改变返回对象的模型。
function createIceCream(flavour='Vanilla') {
return {
type: 'icecream',
scoops: 3,
flavour
}
}
2.组合的工厂函数
在一个工厂函数中定义另一个工厂函数,可以帮助我们把复杂的工厂函数拆分成更小的,可重用的碎片。
我们可以创建一个 dessert(甜点)工厂函数,通过前面的 jelly(果冻)和 icecream(冰淇淋)工厂函数来定义。
function createDessert() {
return {
type: 'dessert',
bowl: [
createJelly(),
createIceCream()
]
};
}
3.异步的工厂函数
并非所有工厂都会立即返回数据。例如,有些必须先获取数据。在这些情况下,我们可以返回 Promises 来定义工厂函数。
function getMeal(menuUrl) {
return new Promise((resolve, reject) => {
fetch(menuUrl)
.then(result => {
resolve({
type: 'meal',
courses: result.json()
});
})
.catch(reject);
});
}
这种深度嵌套的缩进会使异步工厂难以阅读和测试。将它们分解成多个不同的工厂通常是有帮助的,可以使用如下编写。
function getMeal(menuUrl) {
return fetch(menuUrl)
.then(result => result.json())
.then(json => createMeal(json));
}
function createMeal(courses=[]) {
return {
type: 'meal',
courses
};
}
当然,我们可以使用回调函数,但是我们已经有了 Promise.all 这样的工具返回 Promises 来定义工厂函数。
function getWeeksMeals() {
const menuUrl = 'jsfood.com/';
return Promise.all([
getMeal(`${menuUrl}/monday`),
getMeal(`${menuUrl}/tuesday`),
getMeal(`${menuUrl}/wednesday`),
getMeal(`${menuUrl}/thursday`),
getMeal(`${menuUrl}/friday`)
]);
}
4.函数和方法
我们可以定义一个新的函数,它接受一个对象作为参数并返回一个修改的版本。
function eatJelly(jelly) {
if(jelly.scoops > 0) {
jelly.scoops -= 1;
}
return jelly;
}
ES6 的 ... 语法
function eat(jelly) {
if(jelly.scoops > 0) {
return { ...jelly, scoops: jelly.scoops - 1 };
} else {
return jelly;
}
}
import { createJelly, eatJelly } from './jelly';
eatJelly(createJelly());
5.高级工厂
将工厂传递给 高阶函数 ,这将给我们带来巨大的控制力。例如,我们可以使用这个概念来创建一个增强的对象。
function giveTimestamp(factory) {
return (...args) => {
const instance = factory(...args);
const time = Date.now();
return { time, instance };
};
}
const createOrder = giveTimestamp(function(ingredients) {
return {
type: 'order',
ingredients
};
});
这个增强的对象采用一个现有工厂,并将其包装以创建返回带有时间戳实例的工厂。或者,如果我们想要确保一个工厂返回不可变的对象,我们可以用 freezer 来增强它。
function freezer(factory) {
return (...args) => Object.freeze(factory(...args)));
}
const createImmutableIceCream = freezer(createIceCream);
createImmutableIceCream('strawberry').flavour = 'mint'; // Error!
Javascript中七种创建对象的方法
1、工厂模式
function creatPerson(name, age){
var obj = {};
obj.name = name;
obj.age = age;
return obj;
}
var person1 = creatPerson("jim", 24);
- 优点:通过工厂模式,我们能快速创建大量相似对象,并且无重复代码。
- 缺点:通过工厂模式创建的对象都是属于Object,不能区分对象类型,这是工厂模式没有大量使用的原因。
2、构造函数模式
function Person(name, age) {
this.name = name;
this.age = age;
}
var person1 = new Person("jim", 24);
- 构造函数和普通函数相同,只是调用方式不用,构造函数通过new关键字调用,且构造函数名首字母通常大小。
- 使用new操作符调用构造函数实际会经历4个步骤
- 创建一个新对象 a = {}
- 将构造函数的作用域赋给新对象,Person.call(a)
- 执行构造函数
- 返回新对象 return newObj;
- 优点:构造函数创建的对象解决了对象的类型识别问题
person1 instanceof Person; //true - 缺点:构造函数内部的方法会在每个实例上重新创建一遍,造成浪费。
3、原型模式
JS中每个函数都有一个prototype属性,指向一个对象,这个对象的作用是该函数的所有实例都能共享这个对象的属性和方法。我们将这个对象称之为原型对象
function Person() {
}
// 为构造函数的原型对象上添加方法,从而实现实例共享
Person.prototype.setName = function(name) {
this.name = name;
}
Person.prototype.name = "jim";
var person1 = new Person("jim", 24);
person1.setName("tom");
- 优点:原型模式能够解决对象方法重复创建问题
- 缺点:原型模式不能接收参数,而且所有属性都是共享的。
4、组合使用原型模式和构造函数模式
function Person(name, age) {
// 在构造函数内部添加属性,实现独立。
this.name = name;
this.age = age;
}
// 为构造函数的原型对象上添加方法,从而实现实例共享
Person.prototype.setName = function(name) {
this.name = name;
}
var person1 = new Person("jim", 24);
person1.setName("tom");
- 优点:能解决属性共享问题
- 缺点:构造函数和原型分开了的,没有很好的形成一个整体
5、动态原型模式
function Person(name, age) {
this.name = name;
this.age = age;
if(typeof this.setName != 'function') {
Person.prototype.setName = function(name) {
this.name = name;
}
}
}
- 优点:解决了封装问题
- 缺点:基本上非常完美了,除了要多写一个if(就算原型有多个属性方法,我们只需要判断一个就能达到目的)
6、寄生构造函数模式
function specialArray() {
var arr = new Array();
arr.push.apply(arr, arguments);
arr.toPipedString = function() {
retrun this.join(" | ");
}
return arr;
}
7、稳妥构造函数模式
function Person(name, age) {
var o = {}
o.getName = function () {
return this.name;
}
return o;
}
var person1 = Person("jim");
person1.getName() //jim
构造函数中的成员
1.实例成员
实例成员就是在构造函数内部添加的成员,只能由实例化的对象来访问
function P(name, age) {
this.name = name;
this.age = age;
this.run = function () {
console.log(`${this.name}在奔跑`);
}
}
var p1 = new P('张三', 20); // 创建实例化对象
console.log(p1.name); // 用实例化对象访问name属性
p1.run(); // 用实例化对象访问run方法
console.log(P.name); // 无法访问
2.静态成员
在构造函数本身上添加的成员,只能由构造函数本身来访问
function P(name, age) {
this.name = name;
this.age = age;
this.run = function () {
console.log(`${this.name}在奔跑`);
}
}
var p2 = new P('李四', 24); // 创建实例化对象
P.sex = '男'; // 创建静态成员
console.log(P.sex); // '男'
console.log(p2.sex); // undefined
原型
原型的概念
- 原型是函数对象的属性,不是所有对象的属性,对象经过构造函数new出来,那么这个new出来的对象的构造函数有一个属性叫原型。
使用原型给对象添加方法和属性
Gadget.prototype.price = 100;
Gadget.prototype.rating = 3;
Gadget.prototype.getInfo = function() {
return 'Rating: ' + this.rating +', price: ' + this.price;
}
使用原型对象的属性和方法
- 我们使用原型的对象和方法不会在直接在构造函数上使用,而是通过构造函数new出一个对象,那么new出来的对象就会有构造函数原型里的属性和方法。
原型的实时性
- 原型是实时的,意思就是原型对象的属性和方法会实时更新。
- 其实很好理解,javascript中对象是通过引用传递的,原型对象只有一份,不是new出一个对象就复制一份,所以我们对原型的操作和更新,会影响到所有的对象。这就是原型对象的实时性。
自身属性与原型属性
- 这里涉及到javascript是如何搜索属性和方法的,javascript会先在对象的自身属性里寻找,如果找到了就输出,如果在自身属性里没有找到,那么接着到构造函数的原型属性里去找,如果找到了就输出,如果没找到,就null。
- 所以,如果碰到了自身属性和原型属性里有同名属性,那么根据javascript寻找属性的过程,显然,如果我们直接访问的话,会得到自身属性里面的值。
isPrototypeOf()
Object的原型里还有这样一个方法isPrototypeOf(),这个方法可以返回一个特定的对象是不是另一个对象的原型,实际这里不准确,因为我们知道只有函数对象有原型属性,普通对象通过构造函数new出来,自动继承了构造的函数原型的属性方法。但这个方法是可以直接判断,而不需要先取出constructor对象再访问prototype。
Javascript 原型链之原型对象、实例和构造函数三者之间的关系
- constructor 和 prototype 以及实例对象三者之间啥关系?
function OMakeNewCar(){
}
console.info(OMakeNewCar.prototype.constructor === OMakeNewCar);
- 如上,当我们创建一个函数,系统就会为这个函数自动分配一个prototype指针,指向它的原型对象。
- 这个原型对象包含两个部分(
constructor和__proto__),其中constructor指向函数自身。这里形成一个小闭环 - 当我们将该函数作为模板创建实例(new方法)的时候,我们发现创建出的实例是一个与构造函数同名的object,这个object是独立的,他只包含一个__proto__指针(实例没有prototype,强行访问则会输出undefined),这个指针指向上面提到的构造函数的prototype原型对象。
三者形成了一个大“闭环”(注意:构造函数和实例之间无法直接访问,需要通过
__proto__指针间接读取。)
function OMakeNewCar() {
}
var car = new OMakeNewCar();
console.info(car.__proto__ === OMakeNewCar.prototype); //输出 -> true
console.info(car.__proto__.constructor === OMakeNewCar); //输出 -> true
构造函数和原型以及实例对象三者之间关系如此下图所示
2. prototype是啥,__proto__又是啥,他们之间啥关系?
JavaScript中大部分类型的值都拥有proto属性,例如:
console.info('str'.__proto__);
//输出 -> String {length: 0, constructor: ƒ, charAt: ƒ, charCodeAt: ƒ, concat: ƒ, …}
console.info(NaN.__proto__);
//输出 -> Number {constructor: ƒ, toExponential: ƒ, toFixed: ƒ, toPrecision: ƒ, toString: ƒ, …}
console.info(true.__proto__);
//输出 -> Boolean {[[PrimitiveValue]]: false, constructor: ƒ, toString: ƒ, valueOf: ƒ}
console.info(OMakeNewCar.__proto__);
//输出 -> ƒ () { [native code] }
不存在proto属性的类型,如下:
console.info(undefined.__proto__); //Cannot read property '__proto__' of undefined
console.info(null.__proto__); //Cannot read property '__proto__' of null
只有function对象才有prototype属性,其他任何类型的值都没有。即使是使用new方法从function构造出的实例对象也没有prototype属性。
var test = {};
test.prototype = function test() {
}
console.info(test.prototype.prototype);
- 如果改变一个 constructor 的 prototype,他的实例会发生什么改变?
var shape = function() {
};
var p = {
a: function() {
return 'aaaa';
}
};
shape.prototype = p; //更改原型,shape创建的对象就可以直接使用p对象里的属性
var circle = new shape();
console.info(circle.a()); //新对象可以直接使用p对象里的属性
高阶函数
map
举例说明,比如我们有一个函数f(x)=x*x,要把这个函数作用在一个数组[1, 2, 3, 4, 5, 6, 7, 8, 9]上,就可以用map实现如下:
- 由于
map()方法定义在JavaScript的Array中,我们调用Array的map()方法,传入我们自己的函数,就得到了一个新的Array作为结果:
function pow(x) {
return x * x;
}
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
var results = arr.map(pow); // [1, 4, 9, 16, 25, 36, 49, 64, 81]
console.log(results);
注意:map()传入的参数是pow,即函数对象本身。
map()作为高阶函数,事实上它把运算规则抽象了,因此,我们不但可以计算简单的f(x)=x2,还可以计算任意复杂的函数,比如,把Array的所有数字转为字符串:
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
arr.map(String); // ['1', '2', '3', '4', '5', '6', '7', '8', '9']
reduce
Array的reduce()把一个函数作用在这个Array的[x1, x2, x3...]上,这个函数必须接收两个参数,reduce()把结果继续和序列的下一个元素做累积计算,其效果就是:
[x1, x2, x3, x4].reduce(f) = f(f(f(x1, x2), x3), x4)
对一个Array求和,就可以用reduce实现:
var arr = [1, 3, 5, 7, 9];
arr.reduce(function (x, y) {
return x + y;
}); // 25
把[1, 3, 5, 7, 9]变换成整数13579
var arr = [1, 3, 5, 7, 9];
arr.reduce(function (x, y) {
return x * 10 + y;
}); // 13579
filter
filter也是一个常用的操作,它用于把Array的某些元素过滤掉,然后返回剩下的元素。
- 和
map()类似,Array的filter()也接收一个函数。 - 和
map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是true还是false决定保留还是丢弃该元素。 在一个Array中,删掉偶数,只保留奇数
var arr = [1, 2, 4, 5, 6, 9, 10, 15];
var r = arr.filter(function (x) {
return x % 2 !== 0;
});
r; // [1, 5, 9, 15]
把一个Array中的空字符串删掉
var arr = ['A', '', 'B', null, undefined, 'C', ' '];
var r = arr.filter(function (a) {
return a && a.trim(); // 注意:IE9以下的版本没有trim()方法
});
r; // ['A', 'B', 'C']
可见用filter()这个高阶函数,关键在于正确实现一个“筛选”函数。
filter()接收的回调函数,其实可以有多个参数。- 通常我们仅使用第一个参数,表示
Array的某个元素。 - 回调函数还可以接收另外两个参数,表示元素的位置和数组本身:
var arr = ['A', 'B', 'C'];
var r = arr.filter(function (element, index, self) {
console.log(element); // 依次打印'A', 'B', 'C'
console.log(index); // 依次打印0, 1, 2
console.log(self); // self就是变量arr
return true;
});
利用filter,可以巧妙地去除Array的重复元素:
var r,
arr = ['apple', 'strawberry', 'banana', 'pear', 'apple', 'orange', 'orange', 'strawberry'];
r = arr.filter(function (element, index, self) {
return self.indexOf(element) === index;
});
console.log(r.toString());
sort 排序算法
- 排序也是在程序中经常用到的算法。
- 无论使用冒泡排序还是快速排序,排序的核心是比较两个元素的大小。
- 如果是数字,我们可以直接比较,但如果是字符串或者两个对象呢?直接比较数学上的大小是没有意义的,因此,比较的过程必须通过函数抽象出来。
- 通常规定,对于两个元素
x和y,如果认为x < y,则返回-1,如果认为x == y,则返回0,如果认为x > y,则返回1 - 这样,排序算法就不用关心具体的比较过程,而是根据比较结果直接排序。
JavaScript的
Array的sort()方法就是用于排序的
// 看上去正常的结果:
['Google', 'Apple', 'Microsoft'].sort(); // ['Apple', 'Google', 'Microsoft'];
// apple排在了最后:
['Google', 'apple', 'Microsoft'].sort(); // ['Google', 'Microsoft", 'apple']
//因为字符串根据ASCII码进行排序,而小写字母`a`的ASCII码在大写字母之后。
// 无法理解的结果:
[10, 20, 1, 2].sort(); // [1, 10, 2, 20]
//因为`Array`的`sort()`方法默认把所有元素先转换为String再排序,结果`'10'`排在了`'2'`的前面,因为字符`'1'`比字符`'2'`的ASCII码小。
sort()方法也是一个高阶函数,它还可以接收一个比较函数来实现自定义的排序。要按数字大小排序,我们可以这么写:
var arr = [10, 20, 1, 2];
arr.sort(function (x, y) {
if (x < y) {
return -1;
}
if (x > y) {
return 1;
}
return 0;
});
console.log(arr); // [1, 2, 10, 20]
如果要倒序排序,我们可以把大的数放前面:
var arr = [10, 20, 1, 2];
arr.sort(function (x, y) {
if (x < y) {
return 1;
}
if (x > y) {
return -1;
}
return 0;
}); // [20, 10, 2, 1]
默认情况下,对字符串排序,是按照ASCII的大小比较的,现在,我们提出排序应该忽略大小写,按照字母序排序。要实现这个算法,不必对现有代码大加改动,只要我们能定义出忽略大小写的比较算法就可以:
var arr = ['Google', 'apple', 'Microsoft'];
arr.sort(function (s1, s2) {
x1 = s1.toUpperCase();
x2 = s2.toUpperCase();
if (x1 < x2) {
return -1;
}
if (x1 > x2) {
return 1;
}
return 0;
}); // ['apple', 'Google', 'Microsoft']
- 忽略大小写来比较两个字符串,实际上就是先把字符串都变成大写(或者都变成小写),再比较。
- 从上述例子可以看出,高阶函数的抽象能力是非常强大的,而且,核心代码可以保持得非常简洁。
最后友情提示,
sort()方法会直接对Array进行修改,它返回的结果仍是当前Array:
var a1 = ['B', 'A', 'C'];
var a2 = a1.sort();
a1; // ['A', 'B', 'C']
a2; // ['A', 'B', 'C']
a1 === a2; // true, a1和a2是同一对象
Array
对于数组,除了map()、reduce、filter()、sort()这些方法可以传入一个函数外,Array对象还提供了很多非常实用的高阶函数。
every
every()方法可以判断数组的所有元素是否满足测试条件。
一个包含若干字符串的数组,判断所有字符串是否满足指定的测试条件:
var arr = ['Apple', 'pear', 'orange'];
console.log(arr.every(function (s) {
return s.length > 0;
})); // true, 因为每个元素都满足s.length>0
console.log(arr.every(function (s) {
return s.toLowerCase() === s;
})); // false, 因为不是每个元素都全部是小写
find
find()方法用于查找符合条件的第一个元素,如果找到了,返回这个元素,否则,返回undefined:
var arr = ['Apple', 'pear', 'orange'];
console.log(arr.find(function (s) {
return s.toLowerCase() === s;
})); // 'pear', 因为pear全部是小写
console.log(arr.find(function (s) {
return s.toUpperCase() === s;
})); // undefined, 因为没有全部是大写的元素
findIndex
findIndex()和find()类似,也是查找符合条件的第一个元素,不同之处在于findIndex()会返回这个元素的索引,如果没有找到,返回-1:
var arr = ['Apple', 'pear', 'orange'];
console.log(arr.findIndex(function (s) {
return s.toLowerCase() === s;
})); // 1, 因为'pear'的索引是1
console.log(arr.findIndex(function (s) {
return s.toUpperCase() === s;
})); // -1
forEach
forEach()和map()类似,它也把每个元素依次作用于传入的函数,但不会返回新的数组。forEach()常用于遍历数组,因此,传入的函数不需要返回值:
var arr = ['Apple', 'pear', 'orange'];
arr.forEach(console.log); // 依次打印每个元素
闭包
闭包的概念:
- 函数a中, 有一个函数b, 而这个函数b中可以访问函数a中的变量,此时就形成了闭包
- 即闭包就是能够读取其他函数内部的变量
- 闭包就是函数内部和函数外部链接起来的桥梁
闭包的作用:
- 缓存数据, 延长作用域链
闭包的优点:
- 缓存数据, 也是缺点
函数作为返回值
高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。
例:实现一个对Array的求和。
function sum(arr){
return arr.reduce(function(x,y){
return x+y;
})
}
alert(sum([1,3,5,7,9]));
如果不需要立刻求和,而是在后面的代码中,根据需要再计算(可以不返回求和的结果,而是返回求和的函数!)
function l_sum(arr){
var sum = function(){
return arr.reduce(function(x,y){
return x + y;
})
}
return sum;
}
var f = l_sum([1, 2, 3, 4, 5]); // function sum()
console.log(l_sum());//当我们调用l_sum()时,返回的并不是求和结果,而是求和函数
console.log(f());//调用函数f()时,才真正计算求和的结果
- 在这个例子中,我们在函数
l_sum中定义了函数sum - 并且,内部函数
sum可以引用外部函数l_sum的参数和局部变量 - 当
l_sum返回函数sum时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)” 当我们调用l_sum()时,每次调用都会返回一个新的函数,即使传入相同的参数:
var f1 = l_sum([1, 2, 3, 4, 5]);
var f2 = l_sum([1, 2, 3, 4, 5]);
console.log(f1 === f2);// false
//`f1()`和`f2()`的调用结果互不影响。
闭包
返回的函数在其定义内部引用了局部变量,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用
例:返回的函数并没有立刻执行,而是直到调用了f()才执行。
function count() {
var arr = [];
for (var i = 1; i <= 3; i++) {
//创建的3个函数都添加到一个`Array`中返回
arr.push(function () {
return i * i;
});
}
return arr;
}
var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];
alert(f1());//16
alert(f2());//16
alert(f3());//16
- 全部都是
16! - 原因就在于返回的函数引用了变量
i,但它并非立刻执行。 - 等到3个函数都返回时,它们所引用的变量
i已经变成了4,因此最终结果为16。 - 返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量。
如果一定要引用循环变量怎么办?
方法:再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变
function count() {
var arr = [];
for (var i=1; i<=3; i++) {
arr.push((function (n) {
return function () {
return n * n;
}
})(i));
}
return arr;
}
var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];
alert(f1());
alert(f2());
alert(f3());
注意这里用了一个“创建一个匿名函数并立刻执行”的语法:
(function (x) {
return x * x;
})(3); // 9
理论上讲,创建一个匿名函数并立刻执行可以这么写:
function (x) { return x * x } (3);
但是由于JavaScript语法解析的问题,会报SyntaxError错误,因此需要用括号把整个函数定义括起来:
(function (x) { return x * x }) (3);
通常,一个立即执行的匿名函数可以把函数体拆开,一般这么写:
(function (x) {
return x * x;
})(3);
借助闭包,封装一个私有变量。我们用JavaScript创建一个计数器:
function counter(x){
var y = x || 0;
return{
inc:function(){
x += 1;
return x;
}
}
}
var c1 = counter(0);
alert(c1.inc());
alert(c1.inc());
alert(c1.inc());
闭包就是携带状态的函数,并且它的状态可以完全对外隐藏起来。
闭包还可以把多参数的函数变成单参数的函数。
例如,要计算xy可以用Math.pow(x, y)函数,不过考虑到经常计算x2或x3,我们可以利用闭包创建新的函数pow2和pow3:
function make_pow(n) {
return function (x) {
return Math.pow(x, n);
}
}
// 创建两个新函数:
var pow2 = make_pow(2);
var pow3 = make_pow(3);
console.log(pow2(2)); // 4
console.log(pow3(3)); // 9
箭头函数
ES6标准新增了一种新的函数:Arrow Function(箭头函数)。
x => x * x
上面的箭头函数相当于:
function (x) {
return x * x;
}
- 箭头函数相当于匿名函数,并且简化了函数定义。箭头函数有两种格式,一种像上面的,只包含一个表达式,连
{ ... }和return都省略掉了。 - 还有一种可以包含多条语句,这时候就不能省略
{ ... }和return:
x => {
if (x > 0) {
return x * x;
}
else {
return - x * x;
}
}
如果参数不是一个,就需要用括号()括起来:
// 两个参数:
(x, y) => x * x + y * y
// 无参数:
() => 3.14
// 可变参数:
(x, y, ...rest) => {
var i, sum = x + y;
for (i=0; i<rest.length; i++) {
sum += rest[i];
}
return sum;
}
如果要返回一个对象,就要注意,如果是单表达式,这么写的话会报错:
// SyntaxError:
x => { foo: x }
因为和函数体的{ ... }有语法冲突,所以要改为:
// ok:
x => ({ foo: x })
js 改变this指向的几种方法
1.call()和apply()
- call():第一个参数表示要把this指向的新目标,第二个之后的参数其实相当于传参,参数以,隔开(性能较apply略好)
- 用法:
a.call(b,1,2);表示要把a函数的this指向修改为b的this指向,并且运行a函数,传进去的参数是(1,2)` - apply(): 第一个参数同上,第二个参数接受一个数组,里面也是传参,只是以数组的方式,相当于arguments
- 用法:
a.apply(b,[1,2]);表示要把a函数的this指向修改为b的this指向,并且运行a函数,传进去的参数是(1,2)注意:即使只有一个参数的话,也要是数组的形式
2.bind()
作用:bind()方法会创建一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以创建它时传入 bind()方法的第一个参数作为 this,传入 bind() 方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。
var foo = {
bar : 1,
eventBind: function(){
$('.someClass').on('click',function(event) {
/* Act on the event */
console.log(this.bar); //1
}.bind(this));//这里的this是eventBind的this,即指向的是foo
}
总结一下
- apply 、 call 、bind 三者都是用来改变函数的this对象的指向的;
- apply 、 call 、bind 三者第一个参数都是this要指向的对象,也就是想指定的上文;
- apply 、 call 两者都可以利用后续参数传参; 但是传参的方式不一样,apply是数组,call是正常传参形式,bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。
3.new
new运行原理是:
new Animal('cat') = {//类似这样
var obj = {};//先定义一个空对象
obj.__proto__ = Animal.prototype;//把 obj 的__proto__ 指向构造函数 Animal 的原型对象 prototype,此时便建立了 obj 对象的原型链:obj->Animal.prototype->Object.prototype->null
var result = Animal.call(obj,"cat");//改变this指向,从Animal改变到obj上
return typeof result === 'object'? result : obj; //返回
}
用法:
function Fn(){
this.user = "追梦子";
}
var a = new Fn();//this指向a
console.log(a.user); //追梦子
4.return
在构造函数的时候,使用return进行返回一个Object的时候,当去new一个实例对象的时候,会将this指向改变为return的Object;
function fn()
{
this.user = '追梦子';
return {
"user" : "111"
};
}
var a = new fn;
console.log(a.user); //111
generator(生成器)
generator(生成器)是ES6标准引入的新的数据类型。一个generator看上去像一个函数,但可以返回多次。
generator和函数不同的是,generator由function*定义(注意多出的*号),并且,除了return语句,还可以用yield返回多次。
function* foo(x) {
yield x + 1;
yield x + 2;
return x + 3;
}
编写一个产生斐波那契数列的函数,可以这么写:
function* fib(max) {
var
t,
a = 0,
b = 1,
n = 0;
while (n < max) {
yield a;
[a, b] = [b, a + b];
n ++;
}
return;
}
alert(fib(5)); // fib {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: Window}
调用generator对象有两个方法
- 不断地调用generator对象的
next()方法:
var f = fib(5);
f.next(); // {value: 0, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 2, done: false}
f.next(); // {value: 3, done: false}
f.next(); // {value: undefined, done: true}
next()方法会执行generator的代码,然后,每次遇到yield x;就返回一个对象{value: x, done: true/false},然后“暂停”。- 返回的
value就是yield的返回值,done表示这个generator是否已经执行结束了。 - 如果
done为true,则value就是return的返回值。 - 当执行到
done为true时,这个generator对象就已经全部执行完毕,不要再继续调用next()了。
- 直接用
for ... of循环迭代generator对象,这种方式不需要我们自己判断done:
function* fib(max) {
var t,
a = 0, b = 1, n = 0;
while (n < max) {
yield a;
[a, b] = [b, a + b];
n ++;
}
return;
}
for (var x of fib(10)) {
console.log(x); // 依次输出0, 1, 1, 2, 3, ...
}
原型继承
继承的本质是扩展一个已有的Class,并生成新的Subclass。
例:Student构造函数:
function Student(props) {
this.name = props.name || 'Unnamed';
}
Student.prototype.hello = function () {
alert('Hello, ' + this.name + '!');
}
现在,我们要基于Student扩展出PrimaryStudent,可以先定义出PrimaryStudent:
function PrimaryStudent(props) {
// 调用Student构造函数,绑定this变量:
Student.call(this, props);
this.grade = props.grade || 1;
}
但是,调用了Student构造函数不等于继承了Student,PrimaryStudent创建的对象的原型是:
new PrimaryStudent() ----> PrimaryStudent.prototype ----> Object.prototype ----> null
必须想办法把原型链修改为:
new PrimaryStudent() ----> PrimaryStudent.prototype ----> Student.prototype ---->Object.prototype ----> null
新的基于PrimaryStudent创建的对象不但能调用PrimaryStudent.prototype定义的方法,也可以调用Student.prototype定义的方法。
// PrimaryStudent构造函数:
function PrimaryStudent(props) {
Student.call(this, props);
this.grade = props.grade || 1;
}
// 空函数F:
function F() {
}
// 把F的原型指向Student.prototype:
F.prototype = Student.prototype;
// 把PrimaryStudent的原型指向一个新的F对象,F对象的原型正好指向Student.prototype:
PrimaryStudent.prototype = new F();
// 把PrimaryStudent原型的构造函数修复为PrimaryStudent:
PrimaryStudent.prototype.constructor = PrimaryStudent;
// 继续在PrimaryStudent原型(就是new F()对象)上定义方法:
PrimaryStudent.prototype.getGrade = function () {
return this.grade;
};
// 创建xiaoming:
var xiaoming = new PrimaryStudent({
name: '小明',
grade: 2
});
xiaoming.name; // '小明'
xiaoming.grade; // 2
// 验证原型:
xiaoming.__proto__ === PrimaryStudent.prototype; // true
xiaoming.__proto__.__proto__ === Student.prototype; // true
// 验证继承关系:
xiaoming instanceof PrimaryStudent; // true
xiaoming instanceof Student; // true
注意,函数
F仅用于桥接,我们仅创建了一个new F()实例,而且,没有改变原有的Student定义的原型链。
如果把继承这个动作用一个inherits()函数封装起来,还可以隐藏F的定义,并简化代码:
function inherits(Child, Parent) {
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
}
这个inherits()函数可以复用:
function Student(props) {
this.name = props.name || 'Unnamed';
}
Student.prototype.hello = function () {
alert('Hello, ' + this.name + '!');
}
function PrimaryStudent(props) {
Student.call(this, props);
this.grade = props.grade || 1;
}
// 实现原型继承链:
inherits(PrimaryStudent, Student);
// 绑定其他方法到PrimaryStudent原型:
PrimaryStudent.prototype.getGrade = function () {
return this.grade;
};
JavaScript的原型继承实现方式就是:
- 定义新的构造函数,并在内部用
call()调用希望“继承”的构造函数,并绑定this; - 借助中间函数
F实现原型链继承,最好通过封装的inherits函数完成; - 继续在新的构造函数的原型上定义新方法。
class继承
关键字class从ES6开始正式被引入到JavaScript中。class的目的就是让定义类更简单。
函数实现Student的方法:
function Student(name) {
this.name = name;
}
Student.prototype.hello = function () {
alert('Hello, ' + this.name + '!');
}
如果用新的class关键字来编写Student,可以这样写:
class Student {
constructor(name) {
this.name = name;
}
hello() {
alert('Hello, ' + this.name + '!');
}
}
比较一下就可以发现,class的定义包含了构造函数constructor和定义在原型对象上的函数hello()(注意没有function关键字),这样就避免了Student.prototype.hello = function () {...}这样分散的代码。
最后,创建一个Student对象代码
var xiaoming = new Student('小明');
xiaoming.hello();