# 一、JavaScript基础
# 1、基本数据类型介绍
所有的编程语言都有数据类型的概念。
在JavaScript
中,数据类型可以分为基本数据类型和引用数据类型。其中基本数据类型包括Undefined
,Null
,Boolean
,Number
,String
5种类型。在ES6
中新增了一种基本的数据类型Symbol
.
引用类型有Object
,Function
,Array
,Date
等。
问题:两种类型有什么区别?
存储位置不同
区别 | 基本数据类型 | 引用数据类型 |
---|---|---|
存储位置 | 栈(stack) | 堆(heap) |
占据空间 | 小,大小固定 | 大,大小不固定 |
引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
下面我们先来回顾基本数据类型的内容,后面再复习引用类型的内容,以及看一下对应的常见的面试题。
# 1.1 Undefined类型
Undefined
类型只有一个唯一的字面值undefined
,表示的含义是一个变量不存在。
问题:哪些场景中会出现undefined
?
第一:使用只声明而未初始化的变量时,会返回undefined
var a
console.log(a) //undefined
第二:获取一个对象的某个不存在的属性时,会返回undefined
var obj={
userName:'zhangsan'
}
console.log(obj.age)//undefined
第三:函数没有明确的返回值,却对函数的调用结果进行打印
function fn(){}
console.log(fn()) //undefined
第四:函数定义的时候,使用了多个形参,但是在调用的时候传递的参数的数量少于形参数量,那么没有匹配上的参数就为undefined
function fn(p1,p2,p3){
console.log(p3) //undefined
}
fn(1,2)
# 1.2 Null类型
Null
类型只有一个唯一的字面值null
,表示一个空指针的对象,这也是在使用typeof
运行符检测null
值时会返回object
的原因。
问题:哪些场景中会出现null
?
第一:一般情况下,如果声明的变量是为了以后保存某个值,则应该在声明时就将其赋值为null
var obj=null
function foo(){
return {
userName:'zhangsan'
}
}
obj=foo();
第二:JavaScript
在获取DOM
元素时,如果没有获取到指定的元素对象,就会返回null
document.querySelector('#id') //null
第三:在使用正则表达式进行匹配的时候,如果没有匹配的结果,就会返回null
'test'.match(/a/);// null
# 1.3 Undefined与null比较
Undefined
和Null
虽然是两种不同的基本数据类型,但是在某些情况也存在相同之处,下面看一下它们两者相同点和不同点。
(1)相同点
第一:Undefined
和Null
两种数据类型都只有一个字面值,分别是undefined
和null
.
第二:Undefined
和Null
类型在转换为Boolean
类型的值时,都会转换为false
.
第三:在需要将两者转换成对象的时候,都会抛出一个TypeError
的异常。
var a;
var b=null;
cosnole.log(a.name);//Cannot read property 'name' of undefined
cosnole.log(b.name);//Cannot read property 'name' of undefined
第四:Undefined
类型派生自Null
类型,所以在非严格相等的比较下,两者是相等的。如下面代码所示:
null==undefined //true
(2)不同点
第一:null
是JavaScript
的关键字,而undefined
是JavaScript
的一个全局变量,也就是挂载在window
对象上的一个变量,并不是关键字。
第二:在使用typeof
运算符进行检测时,Undefined
类型的值会返回undefined
.而Null
类型的值返回为object
typeof undefined ;//undefined
typeof null ;//object
第三:在需要进行字符串类型的转换时,null
会转换成字符串null
,而undefined
会转换字符串undefined
.
undefined+" abc" //"undefined abc"
null+" abc" //"null abc"
第四:在进行数值类型的转换时,undefined
会转换为NaN
,无法参与计算,而null
会转换为0
,可以参与计算。
undefined +0;// NaN
null+0 ;// 0
第五:建议:无论在什么情况下都没有必要将一个变量显示的赋值为undefined
。如果需要定义某个变量来保存将来要使用的对象,应该将其初始化为null
.
# 1.4 Boolean类型
Boolean
类型(布尔类型)的字面量只有两个,分别是true
和false
,它们是区分大小写的。
Boolean
类型使用最多的场景就是用于if
语句的判断。在JavaScript
中,if
语句可以接受任何类型的表达式,即if(a)
语句中的a
,可以是Boolean
,Number
,String
,Object
,Null
,Undefined
等类型。
如果a
不是Boolean
类型的值,那么JavaScript
解析器会自动调用Boolean( )
函数对a
进行类型的转换,返回最终符合if
语句判断的true
或者是false
值。
不同类型与Boolean
类型的值的转换是Boolean
类型的重点。
第一:String
类型转换为Boolean
类型
空字符都会转换成false
,而任何非空字符串都会转换为true
第二:Number
类型转换为Boolean
类型
0
和NaN
都会转换为false
.而除了0
和NaN
以外都会转换true
.
第三:Object
类型转换Boolean
类型
如果object
为null
时,会转换为false
,如果object
不为null
,则都会转换成true
.
var obj={}
Boolean(obj) //true
var obj=null
Boolean(obj)//false
第四:Function
类型转换Boolean
类型
任何Function
类型都会转换为true
var fn=function(){
}
Boolean(fn)//true
第五:Null
类型转换为Boolean
类型,我们知道Null
类型只有一个null
值,会转换为false
.
第六:Undefined
类型转换Boolean
类型,我们知道Undefined
类型只有一个undefined
值,会转换为false
.
# 1.5 Number类型
在JavaScript
中,Number
类型的数据包括了整型数据,也包括了浮点型数据。
我们先来看一下整型的处理。整型可以是十进制,也可以通过八进制或者是十六进制来表示。
第一:八进制:如果想要用八进制来表示一个数值,那么首位必须是0,其它位必须是0--7的数字,如果后面的数字大于7,则破坏了八进制的规则,这时会被当作十进制数来处理。
var num1=024
console.log(num1) //20
var num2=079
console.log(num2) //79
num1
第一位是0
表示八进制,后面每位数字都是在0--7
之间的,所以符合八进制规则,最终转换为十进制为20
num2
的第一位也是0,但是最后一位已经超过了7,所以不属于八进制,这里直接作为十进制来处理,最终输出的结果为79.
第二:十六进制:
如果想用十六进制表示一个数值,那么前面两位必须是0x
,其它的位必须是(0--9,a--f
或者A--F
).如果超出了这个范围,则会抛出异常。
var num1=0x5f //95
var num2=Ox5h //Uncaught SyntaxError: Invalid or unexpected token
与Boolean
类型一样,当其它类型在与Number
类型进行数据转换时,也会遵守一定的规则。
# 1.5.1 Number类型转换
在实际开发中,我们经常会遇到将其他类型的值转换为Number
类型的情况。在JavaScript
中,一共有3个函数可以完成这种转换,分别是Number()
函数,parseInt( )
函数,parseFloat( )
函数。下面我们看一下这些函数需要注意的事项。
Number( )函数
Number( )
函数可以用于将任何类型转换为Number
类型,它在转换时遵循如下规则:
第一:如果是数字,会按照对应的进制数据格式,统一转换为十进制返回。
Number(10) //10
Number(010) // 8, 010是八进制的数据,转换成十进制是8
Number(0x10) // 16,0x10是十六进制的数据,转换成十进制是16
第二:如果是Boolean
类型的值,true
返回1,false
返回是的0
Number(true) //1
Number(false) //0
第三:如果值为null
,则返回0
Number(null) //0
第四:如果值为undefined
,则返回NaN
Number(undefined) //NaN
第五:如果值为字符串类型,需要遵循如下规则
(1)如果该字符串只包含了数字,则会直接转换成十进制数;如果数字前面有0,则会直接忽略掉这个0。
Number('21') //21
Number('012') //12
(2) 如果字符串是有效的浮点数形式,则会直接转成对应的浮点数,前置的多个重复的0会被删除,只保留一个。
Number('0.12') //0.12
Number('00.12') //0.12
(3)如果字符串是有效的十六进制形式,则会转换为对应的十进制数值
Number('0x12') //18
(4) 如果字符串是有效的八进制,则不会按照八进制转换,而是直接按照十进制转换并输出,因为前置的0会被直接忽略掉。
Number('010') //10
Number('0020') //20
(5)如果字符串为空,即字符串不包含任何字符,或为连续多个空格,则会转换为0.
Number('') //0
Number(' ')//0
(6)如果字符串中包含了任何不适以上5种情况的其它格式内容,则会返回NaN
Number('123a') //NaN
Number('abc') //NaN
第六:如果是对象类型,则会调用对象的valueOf( )
函数获取返回值,并且判断返回值能否转换为Number
类型,如果不能,会调用对象的toString( )
函数获取返回值,并且判断是否能够转换为Number
类型。如果也不满足,则返回NaN
.
以下是通过valueOf( )
函数将对象转换成Number
类型。
var obj={
age:'12',
valueOf:function(){
return this.age
},
}
Number(obj) //12
以下是通过toString( )
函数将对象转换成Number
类型。
var obj={
age:'21',
toString:function(){
return this.age
}
}
Number(obj)
parseInt( )函数
parseInt()
函数用于解析一个字符串,并返回指定的基数对应的整数值。
语法格式:
parseInt(string,radix)
其中string
参数表示要被解析的值,如果该参数不是一个字符串,那么会使用toString( )
函数将其转换成字符串。并且字符串前面的空白符会被忽略。
radix
表示的是进制转换的基数,可以是二进制,十进制,八进制和十六进制。默认值为10.
因为对相同的数采用不同进制进行处理时可能会得到不同的结果,所以在任何情况下使用parseInt
函数时,建议都手动补充第二个参数。
parseInt( )
函数会返回字符串解析后的整数值,如果该字符串无法转换成Number
类型,则会返回NaN
.
parseInt('aaa')//NaN
在使用parseInt
函数将字符串转换成整数时,需要注意的问题:
第一:如果遇到传入的参数是非字符串类型的情况,则需要将其优先转换成字符串类型。即使传入的是整型数据。
第二:parseInt( )
函数在做转换时,对于传入的字符串会采用前置匹配的原则。
parseInt("fg123",16)
对于字符串fg123
,首先从第一个字符开始,f
是满足十六进制的数据的,因为十六进制数据的范围是0--9
,a--f
,所以保留f
,然后是第二个字符g
,它不满足十六进制数据范围,因此从第二个字符都最后一个字符全部舍弃,最终字符串只保留了字符f
,然后将字符f
转换成十六进制的数据,为15,因此最终返回的结果为15
.
还要注意的一点就是,如果传入的字符串中涉及到了算术运算,则不会执行,算术符号会被当作字符处理。
parseInt('16*2')// 16,这里直接当作字符串处理,并不会进行乘法的运算
parseInt(16*2) // 32
第三:对浮点数的处理
如果传入的值是浮点数,则会忽略小数点以及后面的数,直接取整。
parseInt(12.98) //12
第四:map( )
函数与parseInt( )
函数的问题
我们这里假设有一个场景,存在一个数组,数组中的每个元素都是数字字符串,['1','2','3','4'],如果将这个数组中的元素全部转换成整数,应该怎样处理呢?
这里我们可能会想到使用map( )
函数,然后在该函数中调用parseInt( )
函数来完成转换。所以代码如下:
<script>
var arr = ["1", "2", "3", "4"];
var result = arr.map(parseInt);
console.log(result);
</script>
执行上面程序得到的结果是:[1,NaN,NaN,NaN]
为什么会出现这样的问题呢?
上面的代码等效如下的代码
var arr = ["1", "2", "3", "4"];
// var result = arr.map(parseInt);
var result = arr.map(function (val, index) {
return parseInt(val, index);
});
console.log(result);
通过以上的代码,可以发现,parseInt
函数第二个参数实际上就是数组的索引值。所以,整体的形式如下所示:
parseInt('1',0) // 任何整数以0为基数取整时,都会返回本身,所以这里返回的是1
parseInt('2',1) //注意parseInt第二个参数的取值范围为2--36,所以不满足条件,这里只能返回NaN
parseInt('3',2) // 表示将3作为二进制来进行处理,但是二进制只有0和1,所以3超出了范围,无法转换,返回`NaN`
parseInt('4',3) //将4作为三进制来处理,但是4无法用三进制的数据表示,返回NaN
所以当我们在map( )
函数中使用parseInt( )
函数时,不能直接将parseInt( )
函数作为map( )
函数的参数,而是需要在map( )
函数的回调函数中使用,并尽量指定基数。代码如下所示:
var arr = ["1", "2", "3", "4"];
var result = arr.map(function (val) {
return parseInt(val, 10);
});
console.log(result);
parseFloat( )函数
parseFloat
函数用于解析一个字符串,返回对应的浮点数,如果给定值不能转换为数值,则返回NaN
与parseInt( )
函数相比,parseFloat( )
函数没有进制的概念。
注意:
第一:如果字符串前面有空白符,则会直接忽略掉,如果第一个字符就无法解析,则会直接返回NaN
parseFloat(' 2.6')// 2.6
parseFloat('f2.6') //NaN
第二:对于小数点,只能正确匹配第一个,第二个小数点是无效的,它后面的字符也都将被忽略。
parseFloat('12.23')// 12.23
parseFloat('12.23.39')//12.23
总结:
虽然Number( )
,parseInt( )
,parseFloat( )
函数都能勇于Number
类型的转换,但是他们之间还是有一定的差异
第一:Number( )
函数转换的是传入的整个值,并不是像parseInt( )
函数和parseFloat( )
函数一样会从首位开始匹配符合条件的值。如果整个值不能被完整转换,则会返回NaN
第二:parseFloat( )
返回对应的浮点数,parseInt( )
返回整数,并且parseFloat( )
函数在解析时没有进制的概念,而parseInt()
函数在解析时会依赖于出入的第二个参数来做值的转换。
# 1.5.2 isNaN( )函数与Number.isNaN( )函数对比
Number
类型数据中存在一个比较特殊的值NaN
(Not a Number
),它表示应该返回数值却并未返回数值的情况。
NaN
存在的目的是在某些异常情况下保证程序的正常执行。例如0/0
,在其他的语言中,程序会直接抛出异常,而在JavaScript
中会返回NaN
,程序可以正常运行。
NaN
有两个很明显的特点,第一个是任何涉及NaN
的操作都会返回NaN
,第二个是NaN
与任何值都不相等,即使是与NaN
本身相比。
NaN==NaN //false
在判断NaN
时,ES5
提供了isNaN
函数,ES6
为Number
类型增加了静态函数isNaN( ).
问题:既然在ES5
中提供了isNaN
函数,为什么要在ES6
中专门增加Number.isNaN( )
函数呢?两者在使用上有什么区别?
我们先来看一下isNaN( )
函数
isNaN( )
函数的作用是用来确定一个变量是不是NaN
,NaN
是一个Number
类型的数值,只不过这个值无法用真实的数字表示。
isNaN
检测的机制:它在处理的时候会去判断传入的变量值能否转为数字,如果能转换成数字则会返回false
,如果无法转换则会返回true
.
isNaN(NaN)//true
isNaN(undefined) //true
isNaN({})//true
isNaN(true)// false ,Number(true)会转换成数字1
isNaN(false)// false,Number(false)会转换成数字0
isNaN(null) // false,Number(null)会转换成数字0
isNaN(1) //false
isNaN('aaa') //true 字符串aaa无法转换成数字
isNaN('1') //false 字符串“1”可以转换成数字1.
**Number.isNaN( )
**函数
既然在全局的环境中有了isNaN( )
函数,为什么在ES6
中会专门针对Number
类型增加一个isNaN
函数呢?
这是因为全局的isNaN
函数本身存在误导性,而ES6
中的Number.isNaN( )
函数会在真正意义上去判断变量是否为NaN
,不会做数据类型转换。只有在传入的值为NaN
,才会返回true
,传入其它类型的值时会返回false
.
Number.isNaN(NaN)// true
Number.isNaN(1) //false
Number.isNaN(null) //false
Number.isNaN(undefined) //false
如果在非ES6
环境中想用ES6
中的isNaN( )
函数,怎样处理呢?
if(!Number.isNaN){
Number.isNaN=function(n){
return n!==n
}
}
在所有类型的数据中,如果一个变量和自身进行比较,只有在变量为NaN
时才会返回false
,其它情况都是返回的true
.
所以n!==n
返回true
,也只有在n
的值为NaN
的时候才会成立。
总结:
isNaN( )
函数与Number.isNaN( )
函数的区别如下:
第一:isNaN( )
函数在判断是否为NaN
时,需要进行数据类型转换,只有在无法转换为数字时才会返回true
第二:Number.isNaN( )
函数在判断是否为NaN
时,只需要判断传入的值是否为NaN
,并不会进行数据类型转换。
# 1.6 String类型
在JavaScript
中的String
类型可以通过双引号表示,也可以通过单引号表示,并且这两种方式是完全等效的。
# 1.6.1 String类型定义
在JavaScript
中有3种方式来创建字符串,分别是字符串字面量,直接调用String( )
函数,还有就是通过new String( )
构造函数的方式。
字面量
字符串字面量就是直接通过单引号或者是双引号定义字符串的方式。
注意:单引号和双引号是等价的。
var str='hello'
var str2="JavaScript"
直接调用String( )
函数
直接调用String( )
函数,会将传入的任何类型的值转换成字符串类型。在转换的时候,需要遵循如下的规则:
第一:如果是Number
类型的值,则直接转换成对应的字符串。
String(123) // '123'
String(123.56) // "123.56"
第二:如果是Boolean
类型的值,则直接转换成字符串的"true"
或者是"false"
String(true)// "true"
String(false) // "false"
第三:如果值为null
,直接转换成字符串的"null"
String(null) // "null"
第四:如果值为undefined
,则转换成字符串的undefined
String(undefined) //"undefined"
new String( )构造函数
这种方式是使用new
运算符来创建一个String
的实例。转换的规则和String( )
函数是一样的,最后返回的是一个String
类型的对象实例。
new String(678) //返回的对象中有length属性,并且可以通过下标获取对应的值。
三种创建方式的区别
使用字符串字面量方式和直接调用String( )
函数的方式得到的字符串都是基本字符串,而通过new String( )
方式生成的字符串是字符串对象。
基本字符串在比较的时候,只需要比较字符串的值即可,而在比较字符串对象时,比较的是对象所在的地址。
var str='hello'
var str2=String('hello')
str===str2 //true
var str3=new String('hello')
var str4=new String('hello')
str3===str4 //false
对于str
与str2
都是基本字符串,只是比较字符串的值就可以了,所以两者是相等的。
而对于str3
与str4
都是通过String
类型的实例,所以在比较的时候需要判断变量是否指向了同一个对象,也就是内存地址是否相同,很明显,str3
与str4
都是在内存中新生成的地址,彼此各不相同。
函数调用
在String
对象的原型链有一系列的函数,例如indexOf( )
,substring()
等等。
通过String
对象的实例可以调用这些函数做字符串的处理。
但是,我们发现了一个问题,就是采用字面量方式定义的字符串也能够直接调用原型链上的这些函数。
'hello'.indexOf('o') //4
这是为什么呢?
实际上基本字符串本身是没有字符串对象上的这些函数的,而在基本字符串调用字符串对象才有的函数时,JavaScript
会自动将基本字符串转换为字符串对象,形成一种包装的类型,这样基本字符串就可以正常调用字符串对象的方法了。
# 1.6.2 字符串常见算法
我们来看一下常见的String
类型中的算法,这些在面试的时候也是经常被问到的。
第一:字符串逆序输出
字符串逆序输出就是将一个字符串以相反的顺序进行输出。
例如abcdef
输出的结果是fedcba
第一种算法
这里我们是借助与数组的reverse()
函数来实现。
function reverseString(str) {
return str.split("").reverse().join("");
}
console.log(reverseString("abcdef"));
第二种算法:
var arr=Array.from('abcdef') //转换成数组,这里比第一种方式简单
console.log(arr.reverse().join(""))
第三种算法:
这里可以通过字符串本身提供的chartAt
函数来完成。
function reverseString2(str) {
var result = "";
for (var i = str.length - 1; i >= 0; i--) {
result += str.charAt(i);
}
return result;
}
console.log(reverseString2("abcdef"));
统计字符串中出现次数最多的字符及出现的次数
假如有一个字符串javascriptjavaabc
,其中出现最多的字符是a
,出现了5次。
算法1
思想:通过key-value
形式的对象存储字符串以及字符串出现的次数,然后逐个判断出现次数最大的值,同时获取对应的字符。
<script>
function getMaxCount(str) {
var json = {}; //表示key-value结构的对象
//遍历str的每一个字符得到key-value形式的对象
for (var i = 0; i < str.length; i++) {
//判断json对象中是否有当前从str字符串中取出来的某个字符。
if (!json[str.charAt(i)]) {
//如果不存在,把当前字符作为key添加到json对象中,值为1
json[str.charAt(i)] = 1;
} else {
//如果存在,则让value值加1
json[str.charAt(i)]++;
}
}
//存储出现次数最多的字符
var maxCountChar = "";
//存储出现最多的次数
var maxCount = 0;
//遍历json对象,找出出现次数最大的值
for (var key in json) {
if (json[key] > maxCount) {
maxCount = json[key];
maxCountChar = key;
}
}
return (
"出现最多的字符是" + maxCountChar + ",共出现了" + maxCount + "次"
);
}
var str = "javascriptjavaabc";
console.log(getMaxCount(str));
</script>
算法2
思路:这里主要是对字符串进行排序,然后通过lastIndexOf()
函数获取索引值后,判断索引值的大小以获取出现的最大次数。
function getMaxCount(str) {
//定义两个变量,分别表示出现最大次数和对应的字符。
var maxCount = 0,
maxCountChar = "";
//处理成数组,调用sort()函数排序,再处理成字符串
str = str.split("").sort().join("");
for (var i = 0, j = str.length; i < j; i++) {
var char = str[i];
//计算每个字符出现的次数
var charCount = str.lastIndexOf(char) - i + 1;
//与次数最大值进行比较
if (charCount > maxCount) {
//更新maxCount与maxCountChar的值
maxCount = charCount;
maxCountChar = char;
}
//变更索引为字符出现的最后位置
i = str.lastIndexOf(char);
}
return "出现最多的字符是" + maxCountChar + ",出现次数为" + maxCount;
}
console.log(getMaxCount("caa"));
去除字符串中重复的字符
假如存在一个字符串"javascriptjavaabc"
,其中存有重复的字符,现在需要将这些重复的字符去掉,只保留一个。
function removeStringChar(str) {
//结果数组
var result = [];
//key-value形式的对象
var json = {};
for (var i = 0; i < str.length; i++) {
//当前处理的字符
var char = str[i];
//判断是否在对象中
if (!json[char]) {
//将value值设置为true
json[char] = true;
//添加到结果数组中
result.push(char);
}
}
return result.join("");
}
var str = "javascriptjavaabc";
console.log(removeStringChar(str));
算法2
这里可以使用ES6
中的Set
数据结构,可以结构具有自动去重的特性,可以直接将数组元素去重。
下面先来看一下Set
的基本使用方式
const set = new Set([1,2,3,4,4,]);
//console.log(set) // Set(4) {1, 2, 3, 4}
[...set] // [1, 2, 3, 4] 通过扩展运算符将set中的内容转换成数组,同时可以看到已经去重。
基本思路:
(1)将字符串处理成数组,然后作为参数传递给Set
的构造函数,通过new
运算符生成一个Set
实例。
(2) 将Set
通过扩展运算符(...)转换成数组的形式,最终转换成字符串获得需要的结果。
function removeStringChar(str) {
let set = new Set(str.split(""));
return [...set].join("");
}
var str = "javascriptjavaabc";
console.log(removeStringChar(str));
判断一个字符串是否为回文字符串
回文字符串指的是一个字符串正序和倒序是相同的,例如字符串abcdcba
是一个回文字符串,而字符串abcedba
就不是一个回文字符串。
需要注意的是,这里不区分字符的大小写,即a
和A
在判断的时候是相等的。
算法1
主要思想是将字符串按从前往后顺序的字符与按从后往前顺序的字符逐个进行比较,如果遇到不一样的值则直接返回false
,否则返回true
.
function isEequStr(str) {
//空字符串则直接返回true
if (!str.length) {
return true;
}
//统一转换成小写,同时再将其转换成数组
str = str.toLowerCase().split("");
var start = 0,
end = str.length - 1;
//通过while循环,判断正序和倒序的字母
while (start < end) {
// 如果相等则更改比较的索引
if (str[start] === str[end]) {
start++;
end--;
} else {
return false;
}
}
return true;
}
var str = "abcdcba";
算法2
思想:将字符串进行逆序的处理,然后与原来的字符串进行比较,如果相等则表示是回文字符串,否则不是回文字符串。
function isEequStr(str) {
//字符串统一转换成小写的形式
str = str.toLowerCase();
//将字符串转换成数组
var arr = str.split("");
//将数组逆序并转换成字符串
var reverseStr = arr.reverse().join("");
return str === reverseStr;
}
console.log(isEequStr("abccba"));
# 2、运算符
在JavaScript
中的运算符包括:算术运算符,关系运算符,等于运算符,位运算符(与、或、非)等
# 2.1 等于运算符
在JavaScript
中等于分为双等(==)比较,和三等于(===)比较。
# 2.1.1 三等于运算符
(1)如果比较的值类型不相同,则直接返回false
1==='1' //false
true==='true' //false
这里还需要注意的一点就是,基本数据类型存在包装类型,在没有使用new
操作符时,简单类型的比较实际上就是值的比较,而使用了new
操作符以后,实际得到的是引用类型的值,在判断时会因为类型不同而直接返回false
1===Number(1) //true
1===new Number(1) //false
'hello'===String('hello') //true
'hello'===new String('hello') //false
(2) 如果比较的值都是数值类型,则直接比较值的大小,相等则返回true
,否则返回false
,需要注意的是,如果参与比较的值中有任何一方为NaN
,则返回false
26===26 //true
34===NaN //false
(3)如果比较的值是字符串类型,则判断每个字符是否相等,如果全部相等,返回true
,否则返回false
'abc'==='abc' //true
'abc'==='abd' //false
(4)关于null
与undefined
比较
null===null //true
undefined===undefined //true
undefined===null //false
(5)如果比较的值都是引用类型,则比较的是引用类型的地址,当两个引用指向同一个地址时,则返回true
,否则返回false
var a=[]
var b=a
var c=[]
console.log(a===b) //true
console.log(a===c) //false
new String('hello')===new String('hello')//false 两个不同对象,地址不相同
//创建构造函数
function Person(userName) {
this.userName = userName;
}
var p1 = new Person("wangwu");
var p2 = new Person("wangwu");
console.log(p1 === p2);//false 两个不同对象,地址不相同
# 2.1.2 双等于运算符
相比于三等于运算符,双等于运算符在进行相等比较的时候,要复杂一点。因为它不区分数据类型,而且会做隐式类型的转换。
双等于在进行比较的时候要注意的点:
如果比较的值类型不相同,则会按照下面的规则进行转换后再进行比较
(1) 如果比较的一方是null
或者是undefined
,只有在另一方是null
或者是undefined
的情况下才返回true
,否则返回false
null==undefined //true
null==1 //false
undefined==2 //false
(2)如果比较的是字符串和数值类型数据,则会将字符串转换为数值后再进行比较,如果转换后的数值是相等的则返回true
,否则返回false
.
1=='1' //true
'222'==222 //true
(3)如果比较的时候,有一方的类型是boolean
类型,会将boolean
类型进行转换,true
转换为1,false
转换0,然后在进行比较。
'1'==true
'2'==true //false
'0'==false //true
# 2.2 typeof运算符
typeof
运算符用于返回对应的数据类型,
基本的使用方式
typeof operator
typeof (operator)
operator
表示要返回类型的操作数,可以是引用类型,也可以是基本数据类型。
括号有时候是必须的,如果不加上括号将会因为优先级的问题,而得不到我们想要的结果。
下面我们看一下typeof
的使用场景
(1)处理Undefined
类型
我们知道Undefined
类型的值只有一个undefined
,typeof
运算符在处理如下情况的时候,返回的结果都是undefined
处理undefined本身
未声明的变量
已经声明但是没有初始化的变量
typeof undefined //"undefined"
typeof abc //"undefined" ,未声明的变量abc,通过typeof返回的是undefined
var sum
typeof sum //undefined 已经声明但是没有初始化的变量
(2)处理Boolean
类型的值
Boolean
类型的值有两个,分别是true
和false
,typeof
运算符在处理这两个值的时候返回都是boolean
var b=true
typeof b //"boolean"
(3) 处理Number
类型的值
对于Number
类型的数,typeof
运算符在处理时会返回number
typeof 666 //number
typeof 66.66 //number
(4)处理String
类型的值
字符串类型,typeof
返回的是string
,包括空字符串。
typeof 'aaa' //string
typeof '' //string
(5)处理Function
类型的值
函数的定义,包括函数的声明,typeof
返回的值function
function fun(){}
typeof fun // "function"
var fun2=function(){}
typeof fun2 // "function"
关于通过class
关键字定义的类,通过typoef
计算返回的值也是function
class Obj{
}
typeof Obj // "function"
class
是在ES6
中新增的一个关键字,原理依旧是原型继承,也就是说本质上仍然是一个Function
(6) 处理Object
类型的值
对象字面量的形式,返回的是object
var obj={userName:'zhangsan'}
typeof obj //"object"
数组,通过typeof
计算返回的值是object
var arr=[1,2,3]
typeof arr // "object"
var arr2=new Array()
typeof arr2 //"object"
(7) typeof
运算符对null
的处理
typeof
运算符对null
的处理,返回的是object
typeof null //object
注意:在前面我们提到过,在使用typeof
的时候,括号有时候是必须的,如果不加上括号会因为优先级问题,得不到我们想要的结果。
例如如下代码所示:
var num=123
typeof (num + 'hello')// string
typeof num + " hello" //"number hello"
通过上面的代码,我们知道typeof
运算符的优先级要高于字符串的拼接运算符(+)
,但是优先级低于小括号,所以在未使用括号时,会优先处理typeof num
, 返回的是number
,然后与hello
字符串进行拼接,得到的最终的结果就是number hello
下面,我们再来看一段代码
typeof 6/2 // NaN
在上面的代码中,会先执行typeof 6
得到的结果为number
,然后除以2,一个字符串除以2,得到的结果为NaN
typeof (6/2) //"number"
这里会先计算括号中的内容,然后在通过typeof
进行计算。
# 3、常用的判空方法
在JavaScript
中判断一个变量是否为空,我们往往会想到对变量取反,然后判断是否为true
if(!x){ }
这是一个非常简单的判断变量是否为空的方法,但是其实涉及到的场景却很多,这里我们就分情况来看一下。
(1)判断变量为空对象
判断变量为null
或者为undefined
判断一个变量是否为空时,可以直接将变量与null
或者是undefined
进行比较,需要注意的是双等号和三等好直接的区别。
if(obj==null) //可以判断null或者是undefined的情况
if(obj===undefined) //只能判断undefined的情况
判断变量为空对象{ }
判断一个变量是否为空对象时,可以通过for...in
语句遍历变量的属性,然后调用hasOwnProperty( )
函数,判断是否有自身存在的属性,如果存在就不是空对象,如果不存在自身的属性(不包括继承的属性),那么变量为空对象。
function isEmpty(obj) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
return false;
}
}
return true;
}
var obj = {
username: "zhangsan",
};
console.log(isEmpty(obj));// false,表明obj这个对象是有自己的属性,所以不是空对象
var obj = {};
console.log(isEmpty(obj));//true,这里将obj对象的属性去掉了,返回的值为true,表明没有自己的属性,表示空对象
//这里通过构造函数的形式创建对象,并且指定了age属性
function Person() {
this.age = 20;
}
var p = new Person();
console.log(isEmpty(p));//false
下面看一下另外一种情况
function Person() {}
Person.prototype.userName = "zhangsan";
var p = new Person();
console.log(isEmpty(p)); //true
在上面的代码中,变量p
是通过new
操作符得到的Person
对象的实例,所以p
会继承Person
原型链上的userName
属性,但是因为不是自身的属性,所以会被判断为空,所以返回true
.
(2)判断变量为空数组
判断变量是否为空数组时,首先要判断变量是否为数组,然后通过数组的length
属性确定。(instanceof
用于判断一个变量是否某个对象的实例)
var arr=new Array()
arr instanceof Array && arr.length===0
以上两个条件都满足时,变量就是一个空数组。
(3) 判断变量为空字符串
判断变量是否为空字符串时,可以直接将其与空字符串进行比较,或者调用trim()
函数去掉前后的空格以后,在去判断字符串的长度。
str==''||str.trim().length==0
当满足以上两个条件中的任意一个时,变量就是一个空字符串。
(4)判断变量为0或者NaN
当一个变量为Number
类型时,判断变量是否为0或者NaN
,因为NaN
与任何值比较都是false
,所以这里我们通过取非来完成判断。
!(Number(num)&&num)==true
当上述代码返回的结果为true
,表明变量为0或者是NaN
(5)
在最开始的时候,我们提到的
在JavaScript
中判断一个变量是否为空,我们往往会想到对变量取反,然后判断是否为true
if(!x){
}
这种方式会包含多种情况,下面我们总结一下:
变量为null
变量为undefined
变量为空字符串''
变量为数字0
变量为NaN
# 4、流程控制
关于流程控制这块内容,这里我们重点看一下Switch
结构
看一下如下代码执行的结果
<script>
function getStringValue(str) {
switch (str) {
case "1":
console.log("a");
break;
case "2":
console.log("b");
break;
case "3":
console.log("c");
break;
default:
console.log("d");
}
}
getStringValue("2"); //b
getStringValue("5"); //d
</script>
以上的代码非常简单。分别输出的是b
和d
但是,这里我们把对getStringValue
函数的调用修改成如下的形式:
getStringValue(3) //d
这里将参数修改成数字3,得到的结果是d
.原因是:在JavaScript
中的关于case
的比较是采用严格相等的方式(===)。在上面的函数调用中,传递的是数字类型的3,而在case
中比较的是String
字符串的'3',两者按照严格方式进行对比,是不相等的。所以只能执行default
,输出字母d
.
下面,再来看如下的调用
getStringValue(String("3")); //c
上面调用的结果是c
.
在前面的课程中,我们讲解过:字符串的字面量和直接调用String( )
函数生成的字符串都是基本的字符串,它们在本质上都是一样的。
所以在严格模式下进行比较是相等的。
String('3')==='3' //true
下面再来看另外一种调用方式
getStringValue(new String("3")); //d
通过new
关键字创建的是字符串对象,这里采用严格模式进行比较,比较的是字符串对象的内存地址是否相同。而当与字符串的字面量进行比较时,会返回false
.
new String('3')==='3' //false
所以在运行整个getStringValue
整个函数的时候,得到的结果为d
.
# 二、引用数据类型
引用类型有Object
,Function
,Array
,Date
,Math
等。
引用类型与基本数据类型的区别:
(1)引用数据类型的实例需要通过new
关键字创建。
(2)将引用数据类型赋值给变量,实际上赋值的是内存地址
(3)引用数据类型的比较是对内存地址的比较,而基本数据类型的比较是对值的比较。
# 1、Object类型
Object
类型是JavaScript
中使用最多的一个类型。
大部分的引用数据类型都是Object
类型。
由于引用数据类型的实例都是通过new
关键字来创建的,所以我们先来探讨有关new
操作相关的问题。
# 1.1 new 操作符的作用
new
操作符在执行过程中会改变this
的指向,所以下面我们先来看一下this
的用法。
<script>
function Person(userName, age) {
this.userName = userName;
this.age = age;
}
console.log(new Person("zhangsan", 20));
</script>
执行上面的代码,发现输出的是一个Person
对象,包含了userName
和age
的数据。
但是,问题是,在构造函数Person
中,我们没有添加return
,为什么会有返回值呢?
其实就是this
这个关键字起作用。
<script>
function Person(userName, age) {
console.log(this);//输出的是Person{ }对象
this.userName = userName;
this.age = age;
}
new Person("zhangsan", 20);
</script>
执行上面的代码,我们可以看到this
这里就是一个Person
的空对象,后面的两行代码就相当于给Person
对象添加了userName
和age
这两个属性。
下面我们把代码修改成如下的形式:
<script>
function Person(userName, age) {
var Person = {};
Person.userName = userName;
Person.age = age;
}
console.log(new Person("zhangsan", 20));
</script>
以上打印的结果中,输出的是Person{}
,并没有包含userName
和age
,原因是什么呢?
因为在 构造函数中如果没有添加return
,则默认返回的是return this
.
修改后的代码如下:
<script>
function Person(userName, age) {
var Person = {};
Person.userName = userName;
Person.age = age;
return Person;
}
console.log(new Person("zhangsan", 20));
</script>
对this
有了一个简单的了解以后,下面重点看如下代码
var person= new Person("zhangsan", 20)
从上面的代码中,主要的作用就是创建一个Person
对象,然后赋值给了person
这个变量,该变量中包含了Person
对象中的属性和函数。
其实,在new
操作符做了如下3件事情。
var person={};
person.__proto__=Person.prototype;
Person.call(person)
# 1.2 原型对象理解
# 函数对象的 prototype 属性
我们创建的每一个函数都有一个 prototype
属性,这个属性是一个指针,指向一个对象。这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法,简单来说,该函数实例化的所有对象的__proto__
的属性指向这个对象,它是该函数所有实例化对象的原型。
function Person(){
}
// 为原型对象添加方法
Person.prototype.sayName = function(){
alert(this.name);
}
下面我们来看一下它们之间的关系。
简易图
# constructor 属性
当函数创建,prototype
属性指向一个原型对象时,在默认情况下,这个原型对象将会获得一个 constructor 属性,这个属性是一个指针,指向 prototype
所在的函数对象。
拿前面的一个例子来说 Person.prototype.constructor
就指向 Person
函数对象。
console.log(Person.prototype.constructor == Person)
下面我们来更新一下它们之间的关系图。
**简易图**
# 对象的 __proto__
属性
当我们调用构造函数创建一个新实例后,在这个实例的内部将包含一个指针,指向构造函数的原型对象.
根据前面的 Person
构造函数我们新建一个实例
var student = new Person();
console.log(student.__proto__ === Person.prototype); // true
从上面我们可以看出,这个连接是存在与实例与构造函数的原型对象之间的,而不是存在于实例和构造函数之间的。
下面我们来看一下现在这几个对象之间的关系
isPrototypeOf()
方法用于测试一个对象是否存在于另一个对象的原型链上。
console.log(Person.prototype.isPrototypeOf(student)); // true
简易图
# 1.3 原型属性
# 属性访问
每当代码读取对象的某个属性时,首先会在对象本身搜索这个属性,如果找到该属性就返回该属性的值,如果没有找到,则继续搜索该对象对应的原型对象,以此类推下去。
因为这样的搜索过程,因此我们如果在实例中添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性,因为在实例中搜索到该属性后就不会再向后搜索了。
# 属性判断
既然一个属性既可能是实例本身的,也有可能是其原型对象的,那么我们该如何来判断呢?
在属性确认存在的情况下,我们可以使用 hasOwnProperty()
方法来判断一个属性是存在与实例中,还是存在于原型中
function Person() {};
Person.prototype.name = "laker" ;
var student = new Person();
console.log(student.name); // laker
console.log(student.hasOwnProperty("name")); // false
student.name = "xiaoming";
console.log(student.name); //xiaoming 屏蔽了原型对象中的 name 属性
console.log(student.hasOwnProperty("name")); // true
function hasPrototypeProperty(object, name){
return !object.hasOwnProperty(name) && (name in object);
}
# 所有属性获取
function Person() {
this.name = "KXY";
}
Person.prototype = {
job: "student",
};
var kxy = new Person();
Object.defineProperty(kxy, "sex", {
value: "female",
enumerable: false,
});
console.log(Object.keys(kxy)); //["name"] //无法获取不可枚举的属性与原型链上继承的属性
console.log(Object.getOwnPropertyNames(kxy));//["name", "sex"]
//for...in能获取原型链上继承的属性,无法获取不可枚举的属性
for (var pro in kxy) {
console.log("kxy." + pro + " = " + kxy[pro]);// kxy.name = KXY
//kxy.job = student
}
怎样判断属性是否为实例属性并且是否可枚举
如果想判断指定名称的属性是否为实例属性并且是否可枚举的,可以使用propertyIsEnumerable
function Student(userName) {
this.userName = userName;
}
Student.prototype.sayHello = function () {
console.log("hello" + this.userName);
};
var stu = new Student();
console.log(stu.propertyIsEnumerable("userName")); //true:userName为自身定义的实例属性
console.log(stu.propertyIsEnumerable("age")); // false:age属性不存在,返回false
console.log(stu.propertyIsEnumerable("sayHello")); // false :sayHello属于原型上的函数
//将userName属性设置为不可枚举
Object.defineProperty(stu, "userName", {
enumerable: false,
});
console.log(stu.propertyIsEnumerable("userName")); // false: userName设置了不可枚举
# 1.4 Object.create( )
方法
# 基本使用
该函数的主要作用是创建并返回一个指定原型和指定属性的新对象,语法格式如下:
Object.create(prototype,propertyDescriptor)
prototype
属性为对象的原型(必须),可以为null
,如果为null
,则对象的原型为undefined
.
propertyDescriptor
表示的是属性描述符(可选),具体的格式如下:
propertyName:{
value:'',
writable:true,
enumerable:true,
configurable:true
}
基本实现:
<script type="text/javascript">
const person = {
userName: "zhangsan",
sayHello: function () {
console.log("hello " + this.userName);
},
};
const stu = Object.create(person);
stu.userName = "lisi";
stu.sayHello(); //hello lisi 覆盖了person中的userName属性原有的值
</script>
通过以上的代码,可以看到stu
对象的原型是person
.也就是stu.__proto__===person
下面再来看一个案例:
var obj = Object.create(null, {
userName: {
value: "wangwu",
writable: true,
enumerable: true,
configurable: true,
},
age: {
value: 23,
},
});
console.log(obj.userName);
console.log(obj.age);
obj.age = 26;
console.log(obj.age);
for (var o in obj) {
console.log(o);
}
delete obj.userName;
console.log(obj.userName);
delete obj.age;
console.log(obj.age);
# 实现原理
通过如下的伪代码来查看对应的实现原理
Object.create=function(proto,propertiesObject){
//省略了其它判断操作
function F(){}
F.prototype=proto;
if(propertiesObject){ Object.defineProperties(F, propertiesObject)}
return new F()
}
通过以上的代码,我们可以得出如下的结论:
var f=new F()
f.__proto__===F.prototype
下面我们可以通过一个例子来验证一下:
var obj = { x: 12, y: 13 };
var test = Object.create(obj);
console.log(test);
console.log(test.x);
console.log(test.__proto__.x);
最后,这里演示一下Object.defineProperties
方法的基本使用
该方法的主要作用就是添加或修改对象的属性。
如下代码所示:
var person = {};
Object.defineProperties(person, {
userName: {
value: "张三",
enumerable: true,
},
age: {
value: 12,
enumerable: true,
},
});
for (var p in person) {
console.log(p);
}
person.age = 20;
console.log(person.age);
# 应用场景
对于Object.create
方法很重要的一个应用场景是用来实现继承
function Person(name, sex) {
this.name = name;
this.sex = sex;
}
Person.prototype.getInfo = function () {
console.log("getInfo: [name:" + this.name + ", sex:" + this.sex + "]");
};
var a = new Person("jojo", "femal");
var b = Object.create(Person.prototype);
console.log(a.name);
console.log(b.name);
console.log(b.getInfo);
下面看一下怎样实现完整的继承操作。
function Person(name, sex) {
this.name = name;
this.sex = sex;
}
Person.prototype.getInfo = function () {
console.log("getInfo: [name:" + this.name + ", sex:" + this.sex + "]");
};
function Student(name, sex, age) {
Person.call(this, name, sex);
this.age = age;
}
Student.prototype = Object.create(Person.prototype);
var s = new Student("coco", "femal", 25);
s.getInfo();
下面,我们简单的分析一下,上面的代码。
对象s
的__proto__
指向的是s
的构造函数Student
的prototype
s.__proto__===Student.prototype
那么Student.prototype
的__proto__
指向什么呢?
Student.prototype.__proto__===Person.prototype
s.__proto__.__proto__===Person.prototype
而我们知道对象s
是有Student
创建的,所以其构造函数为Student
,所以我们在修改了原型以后,这里应该重新修正构造函数。
function Person(name, sex) {
this.name = name;
this.sex = sex;
}
Person.prototype.getInfo = function () {
console.log("getInfo: [name:" + this.name + ", sex:" + this.sex + "]");
};
function Student(name, sex, age) {
Person.call(this, name, sex);
this.age = age;
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
var s = new Student("coco", "femal", 25);
s.getInfo();
# 1.5 Object.create( )
与new Object()
的区别
# 1.6 模拟new
操作符的实现
在前面我们介绍了new
操作符所做的三件事情,下面我们来模拟实现一下。
function Person(name, age) {
this.name = name;
this.age = age;
}
function New() {
var obj = {};
var res = Person.apply(obj, arguments);
return typeof res === "object" ? res : obj;
}
console.log(New("zhangsan", 19));
# 1.7 原型链理解
下面我们通过一个案例来看一个简单的原型链过程。初步代码如下
var A=function(){ }
var a=new A( );
通过a
实例沿着原型链第一次的追溯,__proto__
属性指向A()
构造函数的原型对象。
a.__proto__===A.prototype
a
实例沿着原型链第二次的追溯,A
原型对象的__proto__
属性指向Object
类型的原型对象.
a.__proto__.__proto__===A.prototype.__proto__
A.prototype.__proto__===Object.prototype
a
实例沿着原型链第三次追溯,Object
类型的原型对象的__proto__
属性为null
a.__proto__.__proto__.__proto__===Object.prototype.__proto__
Object.prototype.__proto__===null
具体的图如下所示:
下面,我们再来看一个案例:
function Super(){
};
function Middle(){
};
function Sub(){
};
Middle.prototype = new Super();
Sub.prototype = new Middle();
var suber = new Sub();
对应的原型链如下图所示:
上面的图其实并不完整,因为漏掉了Object
.所以完整的图如下
通过以上内容的讲解,我们对原型链有了更加深入的理解。
# 1.8 原型链特点
关于原型链的特点,主要有两个
第一个特点:由于原型链的存在,属性查找的过程不再只是查找自身的原型对象,而是会沿着整个原型链一直向上,直到追溯到Object.prototype
.也就是说,当js
引擎在查找对象的属性时,先查找对象本身是否存在该属性,如果不存在,会在原型链上查找,直到Object.prototype
.如果Object.prototype
上也找不到该属性,则返回undefined
,如果期间在对象本身找到了或者是某个原型对象上找到了该属性,则会返回对应的结果。
由于这个特点,我们在自定义的对象中,可以调用某些未在自定义构造函数中定义的函数,例如toString( )
函数。
function Person(){ }
var p = new Person();
p.toString(); // 实际上调用的是Object.prototype.toString( )
第二个特点:由于属性查找会经历整个原型链,因此查找的链路越长,对性能的影响越大。
# 1.9 属性的区分
对象属性的查找往往会涉及到整个原型链,那么应该怎样区分属性是实例自身的还是从原型链中继承的呢?
关于这个问题,前面我们也已经讲解过,是通过hasOwnProperty( )
函数来完成的,这里我们在简单的复习强调一下。
function Person(name, age) {
this.name = name;
}
//在对象的原型上添加age属性
Person.prototype.age = 21;
var p = new Person("zhangsan");
console.log(p.hasOwnProperty("name")); //true
console.log(p.hasOwnProperty("age")); //false
name
属性为实例属性,在调用hasOwnProperty
方法时,会返回true
。age
属性为原型对象上的属性,在调用hasOwnProperty
函数时,会返回false
.
在使用for...in
运算符,遍历对象的属性时,一般可以配合hasOwnProperty
方法一起使用,检测某个属性是否为对象自身的属性,如果是,可以做相应的处理。
for(var p in person){
if(person.hasOwnProperty(p)){
}
}
# 2、Array类型
Array
类型中提供了丰富的函数用于对数组进行处理,例如,过滤,去重,遍历等等操作。
# 2.1 怎样 判断一个变量是数组还是对象
这里,我们可能会想到使用typeof
运算符,因为typeof
运算符是专门用于检测数据类型的,但是typeof
运算符能够满足我们的需求吗?
var a = [1, 2, 3];
console.log(typeof a);
# 2.1.1 instanceof
运算符
instanceof
运算符用于通过查找原型链来检查某个变量是否为某个类型数据的实例,使用instanceof
运算符可以判断一个变量是数组还是对象。
var a = [1, 2, 3];
console.log(a instanceof Array); // true
console.log(a instanceof Object); // true
var userInfo = { userName: "zhangsan" };
console.log(userInfo instanceof Array); // false
console.log(userInfo instanceof Object); // true
这里我们可以封装一个函数,用于判断变量是数组类型还是对象类型。
var a = [1, 2, 3];
function getType(o) {
if (o instanceof Array) {
return "Array";
} else if (o instanceof Object) {
return "Object";
} else {
return "参数类型不是Array也不是Object";
}
}
console.log(getType(a));
# 2.1.2 通过构造函数来判断
判断一个变量是否是数组还是对象,其实就是判断变量的构造函数是Array
类型还是Object
类型。
因为一个对象的实例都是通过构造函数创建的。
var a = [1, 2, 3];
console.log(a.__proto__.constructor === Array);
console.log(a.__proto__.constructor === Object); // false
同样这里,这里我们也可以封装一个函数,来判断变量是数组类型还是对象类型。
function getType(o) {
//获取构造函数
var constructor = o.__proto__.constructor;
if (constructor === Array) {
return "Array";
} else if (constructor === Object) {
return "Object";
} else {
return "参数类型不是Array也不是Object";
}
}
var a = [1, 2, 3];
console.log(getType(a));
# 2.1.3 通过toString( )
函数来判断
我们知道,每种引用类型都会直接或间接继承Object
类型,因此它们都包含toString( )
函数。
不同数据类型的toString( )
函数返回值也不一样,所以通过toString( )
函数就可以判断一个变量是数组还是对象,当然,这里我们需要用到call
方法来调用Object
原型上的toString( )
函数来完成类型的判断。
如下所示:
var arr = [1, 2, 3];
var obj = { userName: "zhangsan" };
console.log(Object.prototype.toString.call(arr)); //[object Array]
console.log(Object.prototype.toString.call(obj)); // [object Object]
console.log(arr.toString()); // 1,2,3
# 2.1.4 通过Array.isArray( )
函数来判断
Array.isArray
方法用来判断变量是否为数组。
var arr = [1, 2, 3];
var obj = { name: "zhangsan" };
console.log(Array.isArray(1)); //false
console.log(Array.isArray(arr)); //true
console.log(Array.isArray(obj)); //false
# 2.2 怎样过滤数组中满足条件的数据
对数组中的数据进行过滤,我们使用比较多的是filter
方法。
<script>
var fn = function (x) {
return x % 2 !== 0;
};
var arr = [1, 2, 5, 6, 78, 9, 10];
var result = arr.filter(fn);
console.log(result);
</script>
下面,我们再来看一下针对复杂类型数组的过滤。
下面案例是查找出年龄大于16的男生的信息。
var arr = [
{ gender: "男", age: 15 },
{ gender: "男", age: 17 },
{ gender: "女", age: 15 },
];
var fn = function (obj) {
return obj.gender === "男" && obj.age > 16;
};
const result = arr.filter(fn);
console.log(result);
# 2.3 怎样对数组元素做累加处理
对数组中的元素做累加的处理,可以通过reduce
函数来完成。
reduce
函数最主要的作用就是做累加的操作,该函数接收一个函数作为累加器,将数组中的每个元素从左到右依次执行累加器,返回最终的处理结果。
reduce
函数的语法如下:
arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
求出数组中所有元素累加的和
var arr = [1, 2, 3, 4, 5, 6];
var sum = arr.reduce(function (accumulator, currentValue) {
return accumulator + currentValue;
}, 0);
console.log(sum);
# 2.4 怎样求数组中的最大值与最小值
关于查询出数组中的最大值与最小值的实现方式有很多种,下面我们来看一下具体的实现。
第一:通过prototype
属性扩展min
函数和max
函数来实现求最小值与最大值
//最小值
Array.prototype.min = function () {
var min = this[0];
var len = this.length;
for (var i = 1; i < len; i++) {
if (this[i] < min) {
min = this[i];
}
}
return min;
};
//最大值
Array.prototype.max = function () {
var max = this[0];
var len = this.length;
for (var i = 1; i < len; i++) {
if (this[i] > max) {
max = this[i];
}
}
return max;
};
var arr = [1, 3, 6, 90, 23];
console.log(arr.min()); // 1
console.log(arr.max()); // 90
第二:通过数组的reduce
函数来完成。
Array.prototype.max = function () {
return this.reduce(function (preValue, currentValue) {
return preValue > currentValue ? preValue : currentValue; //返回最大的值
});
};
Array.prototype.min = function () {
return this.reduce(function (preValue, currentValue) {
return preValue < currentValue ? preValue : currentValue; // 返回最小的值
});
};
var arr = [1, 3, 6, 90, 23];
console.log(arr.min()); //
console.log(arr.max()); //
第三:通过ES6
中的扩展运算符来实现
这里我们可以通过ES6
中的扩展运算符(...)来实现。
var arr = [1, 3, 6, 90, 23];
console.log(Math.min(...arr)); //
console.log(Math.max(...arr));
# 2.5 数组遍历的方式有哪些
数组遍历是我们针对数组最频繁的操作。下面我们看一下常见的数组的遍历方式。
# 通过for循环
这时最基本的实现方式
var arr=[1,2,3]
for(var i=0;i<arr.length;i++){
console.log(arr[i])
}
# 使用forEach( )
函数
forEach
函数也是我们遍历数组用的比较多的方法,forEach( )
函数接收一个回调函数,参数分别表示当前执行的元素的值,当前值的索引和数组本身。
var arr = [1, 3, 6, 90, 23];
arr.forEach(function (element, index, array) {
console.log(index + ":" + element);
});
# 使用map( )
函数
`m
var arr = [1, 3, 6, 90, 23];
var result = arr.map(function (element, index, array) {
console.log(index);
return element * element;
});
console.log("result: ===", result);
在使用map
函数的时候一定要注意:在map( )
函数的回调函数中需要通过return
将处理后的值进行返回,否则会返回undefined
.
如下所示:
var arr = [1, 3, 6, 90, 23];
var result = arr.map(function (element, index, array) {
// console.log(index);
element * element;
});
console.log("result: ===", result);
在上面的计算中,将return
关键字省略了,最终返回的结果是:
[undefined, undefined, undefined, undefined, undefined]
# 使用some( )
函数与every( )
函数
some( )
函数与every( )
函数的相似之处都是在对数组进行遍历的过程中,判断数组中是否有满足条件的元素,如果有满足条件的就返回true
,否则返回false
.
some()
与every()
的区别在于:some( )
函数只要数组中某个元素满足条件就返回true
,不会在对后面的元素进行判断。而every( )
函数是数组中每个元素都要满足条件时才会返回true
.
例如:要判断数组中是否有大于6的元素的时候,可以通过some( )
函数来处理。
而要判断数组中是否所有的元素都大于6,则需要通过every( )
函数来处理。
function fn(element, index, array) {
return element > 6;
}
var result = [1, 2, 3, 4, 5].some(fn); //false
console.log(result);
var result = [1, 2, 3, 4, 5, 7].some(fn);
console.log(result);
下面测试一下every( )
函数
function fn(element, index, array) {
return element > 6;
}
var result = [1, 2, 3, 4, 5, 7].every(fn); //false
console.log(result);
下面修改一下数组中的元素。
function fn(element, index, array) {
return element > 6;
}
var result = [7, 8].every(fn); //true
console.log(result);
现在数组中的元素的值都是大于6,所以返回的结果为true
.
# 使用find( )
函数
find( )
函数用于数组的遍历,当找到第一个满足条件的元素值时,则直接返回该元素值,如果都找不到满足条件的,则返回undefined
.
find( )
方法的参数与forEach
是一样的。
var arr = [1, 3, 6, 90, 23];
const result = arr.find(function (element, index, array) {
return element > 6;
});
console.log(result); // 90
var arr = [1, 3, 6, 90, 23];
const result = arr.find(function (element, index, array) {
return element > 100; //undefined
});
console.log(result);
以上就是我们比较常用的数组遍历的方式。当然还有我们前面讲解过的filter
,reduce
函数。
# 2.6 手动实现find
方法
<script>
Array.prototype.findTest = function (fn) {
for (var i = 0; i < this.length; i++) {
var f = fn(this[i]);//把数组元素传递到函数中
if (f) { //如果函数的返回值为true
return this[i]; //则返回对应的数组元素
}
}
};
var arr = [1, 3, 6, 90, 23];
var result = arr.findTest(function (item) {
return item > 6;
});
console.log(result);
</script>
# 2.7 手动实现filter方法
filter
函数内部需要一个回调函数,数组中的每个元素都会执行该回调函数,在执行回调函数时会将数组中的每个元素传递给回调函数的参数,在回调函数的函数体内进行判断,如果返回的是true
,那么将该元素放到新数组arr
中,如果判断的结果为false
,则数据不会放到新数组arr
中。
//模拟实现filter函数
Array.prototype.filterOne = function (fn) {
var newArray = [];
for (var i = 0; i < this.length; i++) {
var f = fn(this[i]);
if (f) {
newArray.push(this[i]);
}
}
return newArray;
};
var array = [65, 56, 89, 53];
var arr = array.filterOne(function (item) {
return item >= 60;
});
console.log("arr=", arr);
# 2.8 手动实现some函数
some()
方法让数组中的每一个元素执行一次回调函数,在该回调函数中执行一些操作,只要有一个操作结果为真,就会返回true。不会在对后面的元素进行判断,否则返回false。
//手动模式some方法
Array.prototype.someTest = function (fn) {
for (let i = 0; i < this.length; i++) {
let f = fn(this[i]);
if (f) {
return f;
}
}
return false;
};
let array = [1, 3, 5, 7, 90];
let result = array.someTest(function (item) {
return item > 10;
});
console.log("result=", result);
# 2.9 手动实现every函数
该方法与some()
方法不同,some()
方法只要有一个符合条件就返回true,而 every()
方法是数组中所有元素都要符合指定的条件,才会返回true.
//手动模拟实现`every`方法
Array.prototype.everyTest = function (fn) {
let f = true;
for (let i = 0; i < this.length; i++) {
let f = fn(this[i]);
if (!f) {
//只要有一个不符合,就立即返回false.
return false;
}
}
return f;
};
let array = [11, 31, 5, 71, 90];
let result = array.everyTest(function (item) {
return item > 10;
});
console.log("result=", result); //false
# 2.10 手动实现map方法
map( )
函数在对数据进行遍历的时候,会将数组中的每个元素做相应的处理,然后得到新的元素,并返回一个新的数组。
//手动实现map方法
Array.prototype.mapTest = function (fn) {
let newArray = [];
for (let i = 0; i < this.length; i++) {
let f = fn(this[i], i, this);
newArray.push(f);
}
return newArray;
};
var arr = [1, 3, 6, 90, 23];
var result = arr.mapTest(function (element, index, array) {
console.log(index);
return element * element;
});
console.log("result: ===", result);
# 2.11 手动实现reduce方法
Array.prototype.reduceTest = function (fn, initialValue) {
//如果没有传递initialValue,我们将使用数组的第一项作为initialValue的值
let hasInitialValue = initialValue !== undefined;
let value = hasInitialValue ? initialValue : this[0];
//如果没有传递initialValue,则索引从1开始,否则从0开始
for (let i = hasInitialValue ? 0 : 1, len = this.length; i < len; i++) {
value = fn(value, this[i], i, this);
}
return value;
};
var arr = [1, 2, 3, 4, 5, 6];
var sum = arr.reduceTest(function (accumulator, currentValue) {
return accumulator + currentValue;
}, 0);
console.log(sum);
# 2.12 怎样实现数组的去重
数组去重是指当数组中出现重复的元素的时候,通过一定的方式,将重复的元素去掉。
# 利用数组遍历去重
// 数组去重
function fn(array) {
var newArray = [];
for (var i = 0; i < array.length; i++) {
if (newArray.indexOf(array[i]) === -1) {
newArray.push(array[i]);
}
}
return newArray;
}
var arr = [1, 2, 3, 4, 5, 5, 6];
console.log(fn(arr));
# 利用键值对去重
function fn(array) {
var obj = {},
result = [],
val;
for (var i = 0; i < array.length; i++) {
val = array[i];
if (!obj[val]) {//根据key获取obj对象中的值
obj[val] = "ok"; //表示该元素已经出现了
result.push(val);
}
}
return result;
}
var arr = [1, 2, 3, 4, 5, 5, 6];
console.log(fn(arr));
function fn(array) {
var obj = {},
result = [],
val,
type;
for (var i = 0; i < array.length; i++) {
val = array[i];
type = typeof val;
if (!obj[val]) {
obj[val] = [type];
result.push(val);
} else if (obj[val].indexOf(type) < 0) {
obj[val].push(type);
result.push(val);
}
}
return result;
}
var arr = [1, 2, 3, 4, 5, 5, 6, "6"];
console.log(fn(arr));
# 使用Set
数据结构去重
具体的代码如下所示:
function fn(arr) {
return Array.from(new Set(arr));
}
console.log(fn([1, 2, 3, 4, 5, 5, 6, "6"]));
# 2.13 怎样获取数组中最多的元素
# 利用键值对实现
<script>
function fn(arr) {
//如果数组中没有值,直接返回
if (!arr.length) return;
//如果只有一个值,返回1,表示出现了1次
if (arr.length === 1) return 1;
var result = {};
//对数组进行遍历
for (var i = 0; i < arr.length; i++) {
if (!result[arr[i]]) {
result[arr[i]] = 1;
} else {
result[arr[i]]++;
}
}
//遍历result对象
var keys = Object.keys(result);
var maxNum = 0,
maxElement;
for (var i = 0; i < keys.length; i++) {
if (result[keys[i]] > maxNum) {
maxNum = result[keys[i]];
maxElement = keys[i];
}
}
return (
"在数组中出现最多的元素是" + maxElement + ",共出现了" + maxNum + "次"
);
}
var array = [1, 2, 3, 3, 3, 6, 6, 6, 6, 6, 7, 8, 9];
console.log(fn(array));
</script>
# 算法优化
function fn(array) {
var result = {};
var maxNum = 0;
var maxElement = null;
for (var i = 0; i < array.length; i++) {
var val = array[i];
result[val] === undefined ? (result[val] = 1) : result[val]++;
if (result[val] > maxNum) {
maxNum = result[val];
maxElement = val;
}
}
return (
"在数组中出现最多的元素是" + maxElement + ",共出现了" + maxNum + "次"
);
}
var array = [1, 2, 3, 3, 3, 6, 6, 6, 6, 6, 7, 8, 9];
console.log(fn(array));
# 三、函数
# 1、函数定义有哪几种实现方式
在使用函数前,先需要对函数进行定义。关于函数的定义总体上可以分为三类。
第一类是函数声明。
第二类是函数表达式
第三类是通过Function
构造函数来完成函数的定义。
首先来看一下函数的声明。
函数声明是直接通过function
关键字接一个函数名,同时可以接收参数。
function sum(num1, num2){
return num1 + num2
}
函数表达式
函数表达式的形式类似于普通变量的初始化,只不过这个变量初始化的值是一个函数。如下代码所示:
var sum = function (num1,num2){
return num1 + num2
}
这个函数表达式没有名称,属于匿名函数表达式。
Function( )
构造函数
使用new
操作符,调用Function( )
构造函数,传入参数,也可以定义一个函数。
var sum = new Function('num1','num2', 'return a+b ')
其中的参数,除了最后一个参数是要执行的函数体,其它的参数都是函数的形参。
# 2、Function( )构造函数定义函数的问题
但是,我们在实际的应用中很少使用Function( )
构造函数来实现对函数的定义。
原因是:
第一:Function( )
构造函数每次执行时,都会解析函数体,并创建一个新的函数对象,所以当在一个循环或者是一个频繁执行的函数中去使用Function( )
构造函数的时候,相对来说性能是比较低的。
第二:通过Function( )
构造函数创建的函数,并不遵循典型的作用域。
如下代码所示:
var a = "12";
function fun() {
var a = "11";
return new Function("return a");
}
console.log(fun()());
# 3、函数表达式的应用场景
关于函数表达式非常典型的应用就是实现了块级作用域
var person = (function () {
var _name = "";
return {
getName: function () {
return _name;
},
setName: function (userName) {
_name = userName;
},
};
})();
person.setName("zhangsan");
console.log(person.getName());
# 4、函数声明与函数表达式有什么区别
函数声明与函数表达式虽然是两种定义函数的方式,但是两者之间还是有区别的。
第一点就是:函数名称
// 函数声明,函数名称sum是必须的
function sum (num1,num2){
return num1 + num2
}
// 没有函数名称的匿名函数表达式
var sum = function (num1,num2){
return num1 + num2
}
第二点就是关于:函数提升
console.log(add(1, 2)); // 3
console.log(sum(3, 6)); // Uncaught TypeError: sum is not a function
// 函数声明
function add(num1, num2) {
return num1 + num2;
}
// 函数表达式
var sum = function (num1, num2) {
return num1 + num2;
};
# 5、函数常见的调用模式有哪些
函数调用模式
function add(num1, num2) {
return num1 + num2;
}
// 函数表达式
var sum = function (num1, num2) {
return num1 + num2;
};
console.log(add(1, 2));
console.log(sum(3, 6));
方法调用模式
var obj = {
userName: "zhangsan",
getUserName: function () {
return this.userName;
},
};
console.log(obj.getUserName());
var obj = {
userName: "zhangsan",
getUserName: function () {
return this.userName;
},
};
// console.log(obj.getUserName());
console.log(obj["getUserName"]());
var obj = {
userName: "zhangsan",
getUserName: function () {
return this.userName;
},
setUserName: function (name) {
this.userName = name;
return this;
},
};
console.log(obj.setUserName("lisi").getUserName());// lisi
构造器(构造函数)调用模式
//定义构造函数
function Person(name) {
this.userName = name; //定义属性
}
// 在原型上定义函数
Person.prototype.getUserName = function () {
return this.userName;
};
// 通过new来创建实例
var p = new Person("zhangsan");
// 调用原型上的方法
console.log(p.getUserName());
function sum(num1, num2) {
return num1 + num2;
}
//定义一个对象
var obj = {};
//通过call()和apply( )函数调用sum( )函数
console.log(sum.call(obj, 2, 6));
console.log(sum.apply(obj, [3, 6]));
匿名函数调用模式
所谓的匿名函数,就是没有函数名称的函数。匿名函数的调用有两种方式,一种是通过函数表达式定义函数,并赋值给变量,通过变量进行调用。如下所示:
//通过函数表达式定义匿名函数,并赋值给变量sum
var sum =funciton (num1,num2){
return num1 + num2
}
// 通过sum来进行调用
sum(2,6)
另外一种是使用小括号()
将匿名函数括起来,然后在后面使用小括号( )
,传递对应的参数从而完成对应的调用。
(function (num1, num2) {
console.log(num1 + num2);
})(2, 6);
# 6、实参与形参有哪些区别
第一:在函数的调用过程中,数据传递是单向的,也就是只能把实参的值传递给形参,而不能把形参的值反向传递给实参
第二:当实参是基本数据类型的值的时候,在向形参传递的时候,实际上是将实参的值复制一份传递给形参,在函数运行结束以后
形参释放,而实参中的值不会发生变化。当实参是引用类型的值的时候,实际是将实参的内存地址传递给形参,即实参与形参都指向了
相同的内存地址,此时形参可以修改实参的值。
var person = { age: 21 };
function fn(obj) {
obj.age = 22;
}
fn(person);
console.log(person.age);
第三:函数可以不用定义形参,在函数体中可以通过arguments
对象获取传递过来的实参的值,并进行处理。
第四:在函数定义形参时,形参的个数并一定要和实参的个数相同,实参与形参会按照从前向后的顺序进行匹配,没有匹配到的形参被当作undefined
来处理。
第五:实参并不需要与形参的数据类型一致,因为形参的数据类型只能在执行的时候才能够被确定,因为会通过隐式数据类型的转换。
# 7、介绍一下arguments对象
arguments
对象是所有函数都具有的一个内置的局部变量,表示的是函数实际接收到的参数,是一个类似数组的结构。
下面我们说一下arguments
对象都具有哪些性质。
第一:arguments
对象只能在函数内部使用,无法在函数的外部访问到arguments
对象。同时arguments
对象存在于函数级的作用域中。
console.log(arguments); //Uncaught ReferenceError: arguments is not defined
function fn() {
console.log(arguments.length);
}
fn(1, 2, 3);
第二:可以通过索引来访问arguments
对象中的内容,因为arguments
对象类似数组结构。
function fn() {
console.log(arguments[0]); // 1
console.log(arguments[1]); // 2
console.log(arguments[2]); // undefined
}
fn(1, 2);
第三:arguments
对象的值由实参决定,不是有形参决定。
function fn(num1, num2, num3) {
console.log(arguments.length); // 2
}
fn(1, 2);
因为arguments
对象的length
属性是由实际传递的实参的个数决定的,所以这里输出的是2.
function fn(num1, num2, num3) {
arguments[0] = 23;
console.log("num1=", num1); //23
num2 = 33;
console.log(arguments[1]); // 33
}
fn(1, 2);
function fn(num1, num2, num3) {
// arguments[0] = 23;
// console.log("num1=", num1); //23
// num2 = 33;
// console.log(arguments[1]); // 33
arguments[2] = 19;
console.log(num3); //undefined
num3 = 10;
console.log(arguments[2]); // 19
}
fn(1, 2);
function fn(num1, num2, num3) {
// arguments[0] = 23;
// console.log("num1=", num1); //23
// num2 = 33;
// console.log(arguments[1]); // 33
arguments[2] = 19;
console.log(num3); //undefined
num3 = 10;
console.log(arguments[2]); // 19
console.log(arguments.length); // 2 长度还是2
}
fn(1, 2);
# 8、arguments对象有哪些应用场景
第一:进行参数个数的判断。
function fn(num1, num2, num3) {
// 判断传递的参数个数是否正确
if (arguments.length !== 3) {
throw new Error(
"希望传递3个参数,实际传递的参数个数为:" + arguments.length
);
}
}
fn(1, 3);
第二:对任意个数参数的处理,也就是说只会对函数中前几个参数做特定处理,后面的参数不论传递多少个都会统一进行处理,这种情况我们可以使用arguments
对象来完成。
function fn(sep) {
var arr = Array.prototype.slice.call(arguments, 1);
// console.log(arr); // ["a", "b", "c"]
return arr.join(sep);
}
console.log(fn("-", "a", "b", "c"));
第三:模拟函数的重载
什么是函数的重载呢?
函数的重载指的是在函数名称相同的情况下,函数的形参的类型不同或者是个数不同。
但是在JavaScript
中没有函数的重载。
function fn(num1, num2) {
return num1 + num2;
}
function fn(num1, num2, num3) {
return num1 + num2 + num3;
}
console.log(fn(1, 2)); // NaN
console.log(fn(1, 2, 3)); // 6
function fn() {
//将arguments对象转换成数组
var arr = Array.prototype.slice.call(arguments);
// console.log(arr); // [1,2]
//调用数组中的reduce方法完成数据的计算
return arr.reduce(function (pre, currentValue) {
return pre + currentValue;
});
}
console.log(fn(1, 2));
console.log(fn(1, 2, 3));
console.log(fn(1, 2, 3, 4, 5));
# 9、说一下普通函数与构造函数的区别
在JavaScript
的函数中,有一类比较特殊的函数:'构造函数'。当我们创建对象的时候,经常会使用构造函数。
构造函数与普通函数的区别:
第一:构造函数的函数名的第一字母通常会大写。
第二:在构造函数的函数体内可以使用this
关键字,表示创生成的对象实例。
function Person(userName) {
this.userName = userName;
}
var person = new Person("zhangsan");
console.log(person);
第三:在使用构造函数的时候,必须与new
操作符配合使用。
第四:构造函数的执行过程与普通函数也是不一样的。
代码如下:
function Person(userName) {
this.userName = userName;
this.sayHi = function () {
console.log(this.username);
};
}
var p1 = new Person("zhangsan");
var p2 = new Person("lisi");
console.log(p1.sayHi === p2.sayHi); // false
function Person(userName) {
this.userName = userName;
// this.sayHi = function () {
// console.log(this.username);
// };
}
Person.prototype.sayHi = function () {
console.log(this.username);
};
var p1 = new Person("zhangsan");
var p2 = new Person("lisi");
console.log(p1.sayHi === p2.sayHi); // true
# 10、什么是变量提升,什么是函数提升
在javascript
中存在一些比较奇怪的现象。在一个函数体内,变量在定义之前就可以被访问到,而不会抛出异常。
如下所示:
function fn() {
console.log(num); // undefined
var num = 2;
}
fn();
同样函数在定义之前也可以被调用,而不会抛出异常。
如下代码所示:
fn();
function fn() {
console.log("hello");
}
导致出现以上情况的原因是,在javascript
中存在变量提升与函数提升的机制。
在讲解变量提升之前,先来说以作用域的问题。
# 作用域
images/
在JavaScript
中,一个变量的定义与调用都是在一个固定的范围内的,这个范围我们称之为作用域。
作用域可以分为全局的作用域,局部作用域(函数作用域)和块级作用域。
如下程序:
function fn() {
var userName = "zhangsan";
console.log(userName);
}
fn(); //zhangsan
下面,再看如下代码:
var userName = "zhangsan";
function fn() {
console.log(userName);
}
fn(); //zhangsan
综上两个案例,我们可以总结出,作用域本质就是一套规则,用于确定在何处以及如何查找变量的规则。
下面,我们再来看一个比较复杂的结构图,来体验一下作用域
- 作用域链
下面,我们再来看一下前面的代码:
var userName = "zhangsan";
function fn() {
console.log(userName);
}
fn(); //zhangsan
我们在查找userName
这个变量的时候,现在函数的作用域中进行查找,没有找到,再去全局作用域中查找。你会注意到,这是一个往外层查找的过程,即顺着一条链条从下往上查找变量。这个链条,我们就称之为作用域链。
如下图所示:
对应的代码如下:
# 面试中关于作用域与作用域链的问题
第一题:以下代码的执行结果是:
var a = 1;
function fn1() {
function fn2() {
console.log(a);
}
function fn3() {
var a = 4;
fn2();
}
var a = 2;
return fn3;
}
var fn = fn1();
fn(); // 2
第二题:以下代码的执行结果是:
var a = 1;
function fn1() {
function fn3() {
var a = 4;
fn2();
}
var a = 2;
return fn3;
}
function fn2() {
console.log(a);
}
var fn = fn1();
fn(); // 1
第三题:以下代码的输出结果为
var a = 1;
function fn1() {
function fn3() {
function fn2() {
console.log(a);
}
var a;
fn2();
a = 4;
}
var a = 2;
return fn3;
}
var fn = fn1();
fn(); //undefined
第四题:以下代码的输出结果为:
var x = 10;
bar(); //10
function foo() {
console.log(x);
}
function bar() {
var x = 30;
foo();
}
第五题: 以下代码的输出结果为:
var x = 10;
bar(); //30
function bar() {
var x = 30;
function foo() {
console.log(x);
}
foo();
}
第六题:以下代码的输出结果为:
var x = 10;
bar(); //30
function bar() {
var x = 30;
(function () {
console.log(x);
})();
}
# 变量提升
所谓变量提升,是将变量的声明提升到函数顶部的位置,也就是将变量声明提升到变量所在的作用域的顶端,而变量的赋值并不会被提升。
var str = "hello world";
(function () {
console.log(str);
var str = "hello vue";
})(); // undefined
var str = "hello world";
(function () {
var str; //变量的声明得到提升
console.log(str);
str = "hello vue"; // 变量的赋值没有得到提升
})();
如下代码所示:
(function () {
console.log(str);
str = "hello vue";
})(); // str is not defined
以下代码的执行结果是:
function foo() {
var a = 1;
console.log(a); //1
console.log(b); //undefined
var b = 2;
}
foo();
上面的代码等价于
function foo() {
var a;
var b;
a = 1;
console.log(a); // 1
console.log(b); // undefined
b = 2;
}
foo();
# 函数提升
不仅通过var
定义的变量会出现提升的情况,使用函数声明方式定义的函数也会出现提升。
如下代码:
foo(); // 函数提升
function foo() {
console.log("hello");
}
function foo(){
console.log("hello");
}
foo() //'hello'
foo(); // foo is not a function
var foo = function () {
console.log("hello");
};
看一下如下程序的执行结果:
function foo() {
function bar() {
return 3;
}
return bar();
function bar() {
return 9;
}
}
console.log(foo()); // 9
如下程序的执行结果:
var a = true;
foo();
function foo() {
if (a) {
var a = 20;
}
console.log(a); // undefined
}
以上的代码的执行过程如下:
var a;
a = true;
function foo(){
var a;
if(a){
a=20
}
console.log(a)
}
foo()
如下程序的执行结果:
function v() {
var a = 1;
function a() {}
console.log(a);
}
v(); // 1
下面我们再来看一段代码:
function fn() {
console.log(typeof foo); // function
var foo = "hello";
function foo() {
return "abc";
}
console.log(typeof foo); // string
}
fn();
执行上面的代码,首先打印的是function
,然后是string
.
上面的代码实际上可以修改成如下的代码段。
function fn1() {
// 变量提升到函数的顶部
var foo;
// 函数提升,但是优先级低,所以出现在变量声明的后面。
function foo() {
return "abc";
}
console.log(typeof foo); //function
foo = "hello";
console.log(typeof foo); //string
}
下面,我们再来看一段代码,看一下对应的输出结果是:
function foo() {
var a = 1;
function b() {
a = 10;
return;
function a() {}
}
b();
console.log(a);
}
foo(); //1
上面的代码可以修改成如下的代码。
function foo() {
//变量a提升
var a;
//函数声明b的提升
function b() {
//内部的函数声明a的提升
function a() {}
//全局变量
a = 10;
return;
}
a = 1;
b();
console.log(a);//在当前的作用域中,可以找到变量a,不需要获取全局变量a,所以其值为1,所以打印结果为1,
}
foo();
# 11、闭包
在正常的情况下,如果定义了一个函数,就会产生一个函数作用域,在函数体中的局部变量会在这个函数的作用域中使用。
一旦函数执行完毕后,函数所占用的空间就会被回收,存在于函数体中的局部变量同样也会被回收,回收后将不能被访问。
如果我们期望在函数执行完毕以后,函数中的局部变量仍然可以被访问到,应该怎样实现呢?
这里我们可以通过闭包来实现。
在讲解闭包的问题之前,我们先说一个概念,执行上下文环境。
# 执行上下文环境
JavaScript
的每段代码的执行都会存在于一个执行上下文环境中。
执行上下文有且只有三类,全局执行上下文,函数上下文,与eval
上下文;由于eval
一般不会使用,这里不做讨论
function f1() {
f2();
console.log(1);
};
function f2() {
f3();
console.log(2);
};
function f3() {
console.log(3);
};
f1();//3 2 1
为了方便理解,我们假设执行栈是一个数组,在代码执行初期一定会创建全局执行上下文并压入栈,因此过程大致如下:
//代码执行前创建全局执行上下文
ECStack = [globalContext];
// f1调用
ECStack.push('f1 functionContext');
// f1又调用了f2,f2执行完毕之前无法console 1
ECStack.push('f2 functionContext');
// f2又调用了f3,f3执行完毕之前无法console 2
ECStack.push('f3 functionContext');
// f3执行完毕,输出3并出栈
ECStack.pop();
// f2执行完毕,输出2并出栈
ECStack.pop();
// f1执行完毕,输出1并出栈
ECStack.pop();
// 此时执行栈中只剩下一个全局执行上下文
# 什么是闭包
关于闭包的官方概念:一个拥有许多变量和绑定了这些变量执行上下文环境的表达式,通常是一个函数。
简单的理解就是:闭包就是能够读取其它函数内部变量的函数。由于在JavaScript
语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。
function outer () {
...
function inner () {
...
}
}
所以,本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
闭包有两个比较显著的特点:
第一:函数拥有的外部变量的引用,在函数返回时,该变量仍然处于活跃状态。
第二:闭包作为一个函数返回时,其执行上下文环境不会销毁,仍然处于执行上下文环境中。
在JavaScript
中存在一种内部函数,即函数声明和函数表达式可以位于另一个函数的函数体内,在内部函数中可以访问外部函数声明的变量,当这个内部函数在包含它们外部函数之外被调用时,就会形成闭包。
function makeFunc() {
var name = "Mozilla";
function displayName() {
alert(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();
下面,我们再来看另外一段代码:
function fn() {
var max = 10;
return function bar(x) {
if (x > max) {
console.log(x);
}
};
}
var f1 = fn();
f1(11); // 11
# 闭包的应用场景
应用缓存
var cacheApp = (function () {
var cache = {};
return {
getResult: function (id) {
// 如果在内存中,则直接返回
if (id in cache) {
return "得到的结果为:" + cache[id];
}
//经过耗时函数的处理
var result = timeFn(id);
//更新缓存
cache[id] = result;
//返回计算的结果
return "得到的结果为:" + result;
},
};
})();
//耗时函数
function timeFn(id) {
console.log("这是一个非常耗时的任务");
return id;
}
console.log(cacheApp.getResult(23));
console.log(cacheApp.getResult(23));
代码封装
在编程的时候,我们提倡将一定特征的代码封装到一起,只需要对外暴露对应的方法就可以,从而不用关心内部逻辑的实现。
<script>
var stack = (function () {
//使用数组模拟栈
var arr = [];
return {
push: function (value) {
arr.push(value);
},
pop: function () {
return arr.pop();
},
size: function () {
return arr.length;
},
};
})();
stack.push("abc");
stack.push("def");
console.log(stack.size()); // 2
console.log(stack.pop()); // def
console.log(stack.size()); // 1
</script>
# 闭包常见面试题
第一:如下程序执行的结果为:
获取所单击的li
元素的索引值
<ul>
<li>a</li>
<li>b</li>
<li>c</li>
<li>d</li>
<li>e</li>
</ul>
对应的js
代码如下:
// 获取所单击的`li`元素的索引值
var list = document.getElementsByTagName("ul")[0].children;
for (var i = 0; i < list.length; i++) {
list[i].onclick = function () {
console.log(i);
};
}
可以采用闭包解决这个问题:
var list = document.getElementsByTagName("ul")[0].children;
for (var i = 0; i < list.length; i++) {
(function (index) {
list[index].onclick = function () {
console.log(index);
};
})(i);
}
第二:如下程序输出结果是:
var arr = ["a", "b", "c"];
for (var i = 0; i < arr.length; i++) {
setTimeout(function () {
console.log(arr[i]);
}, 1000);
}
代码修改后的内容为:
var arr = ["a", "b", "c"];
for (var i = 0; i < arr.length; i++) {
(function (index) {
setTimeout(function () {
console.log(arr[index]);
}, 1000);
})(i);
}
第三:以下程序打印结果是:
var userName = "zhangsan";
var person = {
userName: "lisi",
method: function () {
return function () {
return this.userName;
};
},
};
console.log(person.method()()); //zhangsan
var userName = "zhangsan";
var person = {
userName: "lisi",
method: function () {
var that = this; //用that保存person的this
return function () {
return that.userName;
};
},
};
console.log(person.method()());
第四:以下程序的输出结果
function create() {
var a = 100;
return function () {
console.log(a);
};
}
var fn = create();
var a = 200;
fn(); // 100
第五:以下程序的输出结果:
function print(fn) {
var a = 200;
fn();
}
var a = 100;
function fn() {
console.log(a); // 100
}
print(fn);
# 闭包优缺点
闭包的优点:
第一:保护函数内变量的安全,实现封装,防止变量流入其它环境发生命名冲突,造成环境污染。
第二:在适当的时候,可以在内存中维护变量并缓存,提高执行效率
闭包的缺点:
消耗内存:通常来说,函数的活动对象会随着执行上下文环境一起被销毁,但是由于闭包引用的是外部函数的活动对象,因此这个活动对象无法被销毁,所以说,闭包比一般的函数需要消耗更多的内存。
# 12、this指向
# 常见面试题
我们知道,当我们创建一个构造函数的实例的时候,需要通过new
操作符来完成创建,当创建完成后,函数体中的this
指向了这个实例。
如下代码所示:
function Person(userName) {
this.userName = userName;
}
var person = new Person("zhangsan");
console.log(person.userName);
如果,我们将上面的Person
函数当作一个普通函数来调用执行,那么对应的this
会指向谁呢?
function Person(userName) {
this.userName = userName;
}
Person("lisi");
console.log(window.userName);
通过上面的程序,我们可以总结出,this
指向的永远是函数的调用者。
第一:如下程序的输出结果:
var a = 10;
var obj = {
a: 120,
method: function () {
var bar = function () {
console.log(this.a); // 10
};
bar();//这里是通过window对象完成bar方法的调用
return this.a;
},
};
console.log(obj.method()); // 120
第二:如下程序的输出结果是:
var num = 10;
function Person() {
//给全局变量重新赋值
num = 20;
// 实例变量
this.num = 30;
}
Person.prototype.getNum = function () {
return this.num;
};
var person = new Person();
console.log(person.getNum()); // 30
第三:如下程序的输出结果是:
function fn() {
console.log(this);
}
let obj = {
fn: fn,
};
fn(); //window
obj.fn(); //obj
第四:如下程序的输出结果是:
var fullName = "language";
var obj = {
fullName: "javascript",
prop: {
getFullName: function () {
return this.fullName;
},
},
};
console.log(obj.prop.getFullName()); // undefined
var test = obj.prop.getFullName; // language
console.log(test());
第五:如下程序的输出结果是:
var val = 1;
var json = {
val: 10,
dbl: function () {
val *= 2; //这里由于前面没有添加this,也就是没有写成this.val,所以这里的val指向了全局变量
},
};
json.dbl();
console.log(json.val + val); // 12
如果将上面的题目修改成如下的形式:
var val = 1
var json = {
val: 10,
dbl: function () {
this.val *= 2 //20
}
}
json.dbl()
console.log(json.val + val)//21 20+1=21
第六,如下程序的输出结果是:
var num = 10;
var obj = { num: 20 };
obj.fn = (function (num) {
this.num = num * 3;
num++;
return function (n) {
this.num += n;
num++;
console.log(num);
};
})(obj.num);
var fn = obj.fn;
fn(5);
obj.fn(10);
console.log(num, obj.num);
第七:this
指向call()
函数,apply()
函数,bind()
函数调用后重新绑定的对象。
我们知道通过call()
函数,apply()
函数,bind()
函数可以改变函数执行的主体,如果函数中存在this
关键字,则this
指向call()
函数,apply()
函数,bind()
函数处理后的对象。
代码如下:
//全局变量
var value = 10;
var obj = {
value: 20,
};
// 全局函数
var method = function () {
console.log(this.value);
};
method(); // 10
method.call(obj); // 20
method.apply(obj); // 20
var newMethod = method.bind(obj);
newMethod(); // 20
下面我们再来看一段代码,看一下对应的执行结果:
<body>
<button id="btn">获取用户信息</button>
<script>
var userInfo = {
data: [
{ userName: "zhangsan", age: 20 },
{ userName: "lisi", age: 21 },
],
getUserInfo: function () {
var index = 1;
console.log(this.data[index].userName + " " + this.data[index].age);
},
};
var btn = document.getElementById("btn");
btn.onclick = userInfo.getUserInfo;
</script>
</body>
修改后的代码:
var btn = document.getElementById("btn");
// btn.onclick = userInfo.getUserInfo;
btn.onclick = userInfo.getUserInfo.bind(userInfo);
第八、如下程序的输出结果是:
<button id="btn">获取用户信息</button>
<script>
var userInfo = {
data: [
{ userName: "zhangsan", age: 20 },
{ userName: "lisi", age: 21 },
],
getUserInfo: function () {
this.data.forEach(function (p) {
console.log(this);
});
},
};
var btn = document.getElementById("btn");
// btn.onclick = userInfo.getUserInfo;
btn.onclick = userInfo.getUserInfo.bind(userInfo);
</script>
修改后的代码:
<script>
var userInfo = {
data: [
{ userName: "zhangsan", age: 20 },
{ userName: "lisi", age: 21 },
],
getUserInfo: function () {
var that = this;//保存this
this.data.forEach(function (p) {
console.log(that);//这里的that 指的就是当前的userInfo对象。
});
},
};
var btn = document.getElementById("btn");
// btn.onclick = userInfo.getUserInfo;
btn.onclick = userInfo.getUserInfo.bind(userInfo);
</script>
或者是修改成箭头函数
var userInfo = {
data: [
{ userName: "zhangsan", age: 20 },
{ userName: "lisi", age: 21 },
],
getUserInfo: function () {
// var that = this;
this.data.forEach((p) => {
console.log(this);
});
},
};
var btn = document.getElementById("btn");
// btn.onclick = userInfo.getUserInfo;
btn.onclick = userInfo.getUserInfo.bind(userInfo);
# 13、call()函数,apply( )函数,bind( )函数的使用与区别
在前面我们简单的说过call( )
函数,apply( )
函数,bind( )
函数,的作用。
call( )
函数,apply( )
函数,bind( )
函数,的作用都是改变this
的指向,但是在使用方式上是有一定的区别的。
下面我们分别来看一下它们各自的使用方式:
# call( )
函数的基本使用
基本语法如下:
function.call(thisObj,arg1,arg2,...)
function
表示的是:需要调用的函数。
thisObj
表示:this
指向的对象,也就是this
将指向thisObj
这个参数,如果thisObj
的值为null
或者是undefined
,则this
指向的是全局对象。
arg1,arg2,..
表示:调用的函数需要的参数。
function add(a, b) {
console.log(this);
console.log(a + b);
}
function sub(a, b) {
console.log(a - b);
}
add.call(sub, 3, 1);// 调用add方法,但是add方法中的this指向的是sub,最终的输出结果是4
# apply( )
函数的基本使用
apply()
函数的作用与call()
函数的作用是一样的,不同的是在传递参数的时候有一定的差别
语法格式如下:
function.apply(thisObj,[argsArray])
function
表示的是:需要调用的函数。
thisObj
:this
指向的对象,也就是this
将指向thisObj
这个参数,如果thisObj
的值为null
或者是undefined
,则this
指向的是全局对象。
[argsArray]
:表示的是函数需要的参数会通过数组的形式进行传递,如果传递的不是数组或者是arguments对象,会抛出异常。
function add(a, b) {
console.log(this); // 这里指向的是sub
console.log(a + b);
}
function sub(a, b) {
console.log(a - b);
}
add.apply(sub, [3, 1]);
# bind
函数的基本使用
function.bind(thisObj,arg1,arg2,...)
通过上面语法格式,可以看出bind
函数与call
函数的参数是一样的。
不同 的是bind
函数会返回一个新的函数,可以在任何时候进行调用。
function add(a, b) {
console.log(this); // 这里指向的是sub
console.log(a + b);
}
function sub(a, b) {
console.log(a - b);
}
var newFun = add.bind(sub, 3, 1); //bind 返回的是一个新的函数。
newFun();//完成对add函数的调用,同时this指向了sub
# 三个函数的比较
通过前面对三个函数的基本使用,可以看出,它们共同点就是改变this
的指向。
不同点:
call()
函数与apply()
函数,会立即执行函数的调用,而bind
返回的是一个新的函数,可以在任何时候进行调用。
call()
函数与bind
函数的参数是一样的,而apply
函数第二个参数是一个数组或者是arguments
对象。
# 应用场景
这里,我们重点看一下,关于call()
函数,bind()
函数,apply()
函数的应用场景。
求数组中的最大值与最小值
var arr = [3, 6, 7, 1, 9];
console.log(Math.max.apply(null, arr));
console.log(Math.min.apply(null, arr));
将arguments
转换成数组
function fn() {
var arr = Array.prototype.slice.call(arguments);
arr.push(6);
return arr;
}
console.log(fn(1, 2));
继承的实现
function Person(userName, userAge) {
this.userName = userName;
this.userAge = userAge;
}
function Student(name, age, gender) {
Person.call(this, name, age);
this.gender = gender;
}
var student = new Student("zhangsan", 20, "男");
console.log(
"userName=" +
student.userName +
",userAge=" +
student.userAge +
",gender=" +
student.gender
);
改变匿名函数的this
指向
首先看一下如下程序的执行结果:
var person = [
{ id: 1, userName: "zhangsan" },
{ id: 2, userName: "lisi" },
];
for (var i = 0; i < person.length; i++) {
(function (i) {
this.print = function () {
console.log(this.id);
};
this.print();
})(i);
}
具体的实现方式如下:
var person = [
{ id: 1, userName: "zhangsan" },
{ id: 2, userName: "lisi" },
];
for (var i = 0; i < person.length; i++) {
(function (i) {
this.print = function () {
console.log(this.id);
};
this.print();
}.call(person[i], i));
}
# 手写call、apply及bind函数
call
方法的实现
Function.prototype.myCall = function (context) {
var args = [...arguments].slice(1);
context = context || window;
context.fn = this;
var result = context.fn(...args);
return result;
};
function Add(num1, num2) {
console.log(this);
console.log(num1 + num2);
}
function Sub(num1, num2) {
console.log(num1 - num2);
}
Add.myCall(Sub, 6, 3);
apply
函数的实现
Function.prototype.myApply = function (context) {
var result = null;
context = context || window;
context.fn = this;
if (arguments[1]) {
// console.log("arguments=", arguments[1]);// arguments= (2) [6, 3]
result = context.fn(...arguments[1]);
} else {
result = context.fn();
}
return result;
};
function Add(num1, num2) {
console.log(this);
console.log(num1 + num2);
}
function Sub(num1, num2) {
console.log(num1 - num2);
}
Add.myApply(Sub, [6, 3]);
bind
函数的实现
Function.prototype.myBind = function (context) {
// 获取参数
var args = [...arguments].slice(1), // [1,5]
fn = this;
// console.log(this);//Add
return function Fn() {
// console.log(this); //Window
return fn.apply(context, args);
};
};
function Add(num1, num2) {
console.log(this);
console.log(num1 + num2);
}
function Sub(num1, num2) {
console.log(num1 - num2);
}
var newFun = Add.myBind(Sub, 1, 5);
newFun();
<script>
function add(a, b) {
console.log(this); // 这里指向的是sub
console.log(a + b);
}
function sub(a, b) {
console.log(a - b);
}
var newFun = add.bind(sub, 3); //bind 返回的是一个新的函数。
newFun(2); //完成对add函数的调用,同时this指向了sub
</script>
下面,我们就实现一下关于myBind
方法参数的模拟。
Function.prototype.myBind = function (context) {
// 获取参数
var args = [...arguments].slice(1),
fn = this;
// console.log(this);//Add
return function Fn() {
// console.log(this); //Window
//这里是调用bind函数的时候传递的参数,将其转换成数组
var bindArgs = Array.prototype.slice.call(arguments);
//下面完成参数的拼接
return fn.apply(context, args.concat(bindArgs));
};
};
function Add(num1, num2) {
console.log(this);
console.log(num1 + num2);
return 10;
}
function Sub(num1, num2) {
console.log(num1 - num2);
}
var newFun = Add.myBind(Sub, 1);
console.log(newFun(8));
# 14、回调函数有什么缺点
在JavaScript
编程过程中,我们经常会写回调函数。
我们知道在JavaScript
中函数也是一种对象,对象可以作为参数传递给函数,因此函数也可以作为参数传递给另外一个函数,这个作为参数的函数就是回调函数。
例如,如下的代码示例:
const btn=document.getElementById('btn');
btn.addEventListener('click',function(event){
})
回调函数有一个比较严重的问题,就是很容易出现回调地狱的问题。也就是实现了回调函数不断的嵌套。
setTimeout(() => {
console.log(1)
setTimeout(() => {
console.log(2)
setTimeout(() => {
console.log(3)
},3000)
},2000)
},1000)
以上的代码就是典型的回调地狱的问题,这样的代码是非常不利于阅读和维护的。
所以在ES6
中提供了Promise
以及async/await
来解决地狱回调的问题。关于这块内容
# 15、 为什么函数被称为一等公民?
JavaScript 语言将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同。凡是可以使用值的地方,就能使用函数。比如,可以把函数赋值给变量和对象的属性,也可以当作参数传入其他函数,或者作为函数的结果返回。
同时函数还可以作为类的构造函数,完成对象实例的创建。所以说,这种多重身份让JavaScript
中的函数变得非常重要,所以说函数被称为一等公民。
#
# 四、对象
# 1、对象的属性
给对象添加属性非常的简单如下所示:
var person={
userName:'zhangsan'
}
如果想修改属性的特性,可以通过Object.defineProperty()
来完成。
var person = {
userName: "zhangsan",
};
Object.defineProperty(person, "userName", {
writable: false,
});
person.userName = "lisi"; //无法完成值的修改
console.log(person.userName); //zhangsan
我们可以给Object.defineProperty
添加getter()
函数和setter( )
函数,这两个函数可以实现对象的私有属性,私有属性不对外公布,如果想要对私有属性进行读取和写入,可以通过getter()
函数和setter( )
函数。
var person = {
_age: 20, // _age表示私有属性
};
Object.defineProperty(person, "age", {
get: function () {
return this._age;
},
//在给私有属性赋值的时候,完成对应的校验功能
set: function (value) {
if (value >= 18) {
this._age = value;
console.log("可以浏览该网站");
} else {
console.log("不可以浏览该网站");
}
},
});
console.log(person.age); //20
person.age = 12;
console.log(person.age); //20
person.age = 30;
console.log(person.age); // 30
关于Object.defineProperty
更详细的内容,可以参考vue
响应式原理的课程。
# 2、属性访问方式的区别
我们知道访问对象中的属性,有两种方式。
第一种方式:通过‘.’来访问。
第二种方式:通过‘[ ]’来访问属性。
两种方式有什么区别呢?
第一:使用方括号来访问属性,可以借助于变量来实现。
var person = {
userName: "zhangsan",
};
var myName = "userName";
console.log(person[myName]);
第二:使用方括号来访问属性,也可以通过数字来做属性。
var person = {};
person[1] = "hello";
console.log(person[1]);
# 3、创建对象有哪几种方式
字面量方式创建对象
var userInfo = {
userName: "zhangsan",
userAge: 18,
getUserInfo: function () {
console.log(this.userName + ":" + this.userAge);
},
};
userInfo.getUserInfo();
字面量创建对象比较简单,但是问题也比较突出,每次只能创建一个对象,复用性比较差,如果需要创建多个对象,代码冗余比较高。
通过工厂模式创建对象
工厂模式是一个比较重要的设计模式,该模式提供了一个函数,在该函数中完成对象的创建。
function createUser(userName, userAge) {
var o = new Object();
o.userName = userName;
o.userAge = userAge;
o.sayHi = function () {
console.log(this.userName + ":" + this.userAge);
};
return o;
}
var user1 = createUser("wangwu", 20);
var user2 = createUser("lisi", 20);
console.log(user1.userName + ":" + user2.userName);
通过工厂模式创建对象,解决了字面量创建对象的问题,也就是当创建多个相似对象的时候代码重复的问题。
但是问题是,所创建的所有对象都是Object
类型,无法进一步的区分对象的具体类型是什么。
通过构造函数创建对象
function Person(userName, userAge) {
this.userName = userName;
this.userAge = userAge;
this.sayHi = function () {
console.log(this.userName + ":" + this.userAge);
};
}
var p = new Person("zhangsan", 19);
p.sayHi();
构造函数创建对象的优点:解决了工厂模式中对象类型无法识别的问题,也就是说通过构造函数创建的对象可以确定其所属的类型。
但是通过构造函数创建对象的问题:
在使用构造函数创建对象的时候,每个方法都会在创建对象时重新创建一遍,也就是说,根据Person
构造函数每创建一个对象,我们就会创建一个sayHi
方法,但它们做的事情是一样的,因此会造成内存的浪费。
通过原型模式创建对象
我们知道,每个函数都有一个prototype
属性,这个属性指向函数的原型对象,而所谓的通过原型模式创建对象就是将属性和方法添加到prototype
属性上。
function Person() {}
Person.prototype.userName = "wangwu";
Person.prototype.userAge = 20;
Person.prototype.sayHi = function () {
console.log(this.userName + ":" + this.userAge);
};
var person1 = new Person();
person1.sayHi();
var person2 = new Person();
console.log(person1.sayHi === person2.sayHi); // true
通过上面的代码,我们可以发现,使用基于原型模式创建的对象,它的属性和方法都是相等的,也就是说不同的对象会共享原型上的属性和方法,这样我们就解决了构造函数
创建对象的问题。
但是这种方式创建的对象也是有问题的,因为所有的对象都是共享相同的属性,所以改变一个对象的属性值,会引起其他对象属性值的改变。而这种情况是我们不允许的,因为这样很容易造成数据的混乱。
function Person() {}
Person.prototype.userName = "wangwu";
Person.prototype.userAge = 20;
Person.prototype.arr = [1, 2];
Person.prototype.sayHi = function () {
console.log(this.userName + ":" + this.userAge);
};
var p1 = new Person();
var p2 = new Person();
console.log(p1.userName);
p2.userName = "zhangsan";
console.log(p1.userName); //wangwu,基本数据类型不受影响
p1.arr.push(3);
console.log(p1.arr); // [1,2,3]
console.log(p2.arr); // [1,2,3]
//引用类型受影响
组合使用构造函数模式和原型模式
通过构造函数和原型模式创建对象是比较常用的一种方式。
在构造函数中定义对象的属性,而在原型对象中定义对象共享的属性和方法。
//在构造函数中定义对象的属性
function Person(userName, userAge) {
this.userName = userName;
this.userAge = userAge;
}
//在原型对象中添加共享的方法
Person.prototype.sayHi = function () {
return this.userName;
};
var p = new Person("zhangsan", 21);
var p1 = new Person("lisi", 22);
console.log(p1.sayHi());
console.log(p.sayHi());
// 不同对象共享相同的函数,所以经过比较发现是相等的。
console.log(p.sayHi === p1.sayHi);
//修改p对象的userName属性的值,但是不会影响到p1对象的userName属性的值
p.userName = "admin";
console.log(p.sayHi());
console.log(p1.sayHi());
通过构造函数与原型模式组合创建对象的好处就是:每个对象都有自己的属性值,也就是拥有一份自己的实例属性的副本,同时又共享着方法的引用,最大限度的节省了内存。
使用动态原型模式创建对象
所谓的使用动态原型模式创建对象,其实就是将所有的内容都封装到构造函数中,而在构造函数中通过判断只初始化一次原型。
function Person(userName, userAge) {
this.userName = userName;
this.userAge = userAge;
if (typeof this.sayHi !== "function") {
console.log("abc"); //只输出一次
Person.prototype.sayHi = function () {
console.log(this.userName);
};
}
}
var person = new Person("zhangsan", 21);
var person1 = new Person("zhangsan", 21);
person.sayHi();
person1.sayHi();
通过上面的代码可以看出,我们将所有的内容写在了构造函数中,并且在构造函数中通过判断只初始化一次原型,而且只在第一次生成实例的时候进行原型的设置。这种方式创建的对象与构造函数和原型混合模式创建的对象功能上是相同的。
# 4、对象拷贝
拷贝指的就是将某个变量的值复制给另外一个变量的过程,关于拷贝可以分为浅拷贝与深拷贝。
针对不同的数据类型,浅拷贝与深拷贝会有不同的表现,主要表现于基本数据类型和引用数据类型在内存中存储的值不同。
对于基本数据类型,变量存储的是值本身,
对于引用数据类型,变量存储的是值在内存中的地址,如果有多个变量同时指向同一个内存地址,其中对一个变量的值进行修改以后,其它的变量也会受到影响。
var arr=[1,23,33]
var arr2=arr
arr2[0]=10;
console.log(arr) // [10, 23, 33]
在上面的代码中,我们把arr
赋值给了arr2
,然后修改arr2
的值,但是arr
也受到了影响。
正是由于数据类型的不同,导致在进行浅拷贝与深拷贝的时候首先的效果是不一样的。
基本数据类型不管是浅拷贝还是深拷贝都是对值的本身的拷贝。对拷贝后值的修改不会影响到原始的值。
对于引用数据类型进行浅拷贝,拷贝后的值的修改会影响到原始的值,如果执行的是深拷贝,则拷贝的对象和原始对象之间相互独立,互不影响。
所以,这里我们可以总结出什么是浅拷贝,什么是深拷贝。
浅拷贝:如果一个对象中的属性是基本数据类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址,也就是拷贝后的内容与原始内容指向了同一个内存地址,这样拷贝后的值的修改会影响到原始的值。
深拷贝:如果一个对象中的属性是基本数据类型,拷贝的也是基本类型的值,如果属性是引用类型,就将其从内存中完整的拷贝一份出来,并且会在堆内存中开辟出一个新的区域存来进行存放,而且拷贝的对象和原始对象之间相互独立,互不影响。
# 浅拷贝
下面我们先来看一下浅拷贝的内容
var obj = { a: 1, arr: [2, 3], o: { name: "zhangsan" } };
var shallowObj = shallowCopy(obj);
function shallowCopy(src) {
var dst = {};
for (var prop in src) {
if (src.hasOwnProperty(prop)) {
dst[prop] = src[prop];
}
}
return dst;
}
obj.o.name = "lisi";
console.log(shallowObj.o.name); //lisi,值受到了影响
obj.arr[0] = 20;
console.log(shallowObj.arr[0]); //20,值受到了影响
obj.a = 10;
console.log(shallowObj.a); // 1,值没有收到影响
除了以上方式实现浅拷贝以外,还可以通过ES6
中的Object.assign()
函数来实现,该函数可以将源对象中的可枚举的属性复制到目标对象中。
var obj = { a: 1, arr: [2, 3], o: { name: "zhangsan" } };
var result = {};
//将obj对象拷贝给result对象
Object.assign(result, obj);
console.log(result);
obj.a = 10;
console.log(result.a); // 1,不受影响
obj.arr[0] = 20;
console.log(result.arr[0]); //20 受影响
obj.o.name = "lisi";
console.log(result.o.name); // lisi 受影响
# 深拷贝
下面,我们来看一下深拷贝内容
这里,我们可以使用
JSON.parse(JSON.stringify());
来实现深拷贝。
JSON.stringify()
可以将对象转换为字符串
JSON.parse()
可以将字符串反序列为一个对象
var obj = { a: 1, arr: [2, 3], o: { name: "zhangsan" } };
var str = JSON.stringify(obj);
var resultObj = JSON.parse(str);
obj.a = 10;
console.log(resultObj.a); // 1 不受影响
obj.arr[0] = 20;
console.log(resultObj.arr[0]); // 2 不受影响
obj.o.name = "lisi";
console.log(resultObj.o.name); // zhangsan 不受影响
以上通过JSON
对象,虽然能够实现深拷贝,但是还是有一定的问题的。
第一:无法实现对函数的拷贝
第二:如果对象中存在循环引用,会抛出异常。
第三:对象中的构造函数会指向Object
,原型链关系被破坏。
function Person(userName) {
this.userName = userName;
}
var person = new Person("zhangsan");
var obj = {
fn: function () {
console.log("abc");
},
// 属性o的值为某个对象
o: person,
};
var str = JSON.stringify(obj);
var resultObj = JSON.parse(str);
console.log("resultObj=", resultObj); // 这里丢失了fn属性。因为该属性的值为函数
console.log(resultObj.o.constructor); //指向了Object,导致了原型链关系的破坏。
console.log(obj.o.constructor); // 这里指向Person构造函数,没有问题
下面我们再来看一下循环引用的情况:
var obj = {
userName: "zhangsan",
};
obj.a = obj;
var result = JSON.parse(JSON.stringify(obj));
以上的内容会抛出异常。
自己模拟实现深拷贝
这里,我们实现一个简单的深拷贝,当然也可以使用第三方库中的方法来实现深拷贝,例如:可以使用jQuery
中的$.extend()
在浅拷贝中,我们通过循环将源对象中属性依次添加到目标对象中,而在深拷贝中,需要考虑对象中的属性是否有嵌套的情况(属性的值是否还是一个对象),如果有嵌套可以通过递归的方式来实现,直到属性为基本类型,也就是说,我们需要将源对象各个属性所包含的对象依次采用递归的方式复制到新对象上。
function clone(target) {
if (typeof target === "object") {
let objTarget = {};
for (const key in target) {
//通过递归完成拷贝
objTarget[key] = clone(target[key]);
}
return objTarget;
} else {
return target;
}
}
var obj = {
userName: "zhangsan",
a: {
a1: "hello",
},
};
var result = clone(obj);
console.log(result);
以上就是一个最简单的深拷贝功能,但是在这段代码中我们只考虑了普通的object
,还没有实现数组,所以将上面的代码修改一下,让其能够兼容到数组。
function clone(target) {
if (typeof target === "object") {
//判断target是否为数组
let objTarget = Array.isArray(target) ? [] : {};
for (const key in target) {
objTarget[key] = clone(target[key]);
}
return objTarget;
} else {
return target;
}
}
var obj = {
userName: "zhangsan",
a: {
a1: "hello",
},
//添加数组
arr: [2, 3],
};
var result = clone(obj);
console.log(result);
在上面的代码中,添加了let objTarget = Array.isArray(target) ? [] : {};
判断target
是否为数组。
下面我们来看一下循环引用的情况:
function clone(target) {
if (typeof target === "object") {
//判断target是否为数组
let objTarget = Array.isArray(target) ? [] : {};
for (const key in target) {
objTarget[key] = clone(target[key]);
}
return objTarget;
} else {
return target;
}
}
var obj = {
userName: "zhangsan",
a: {
a1: "hello",
},
//添加数组
arr: [2, 3],
};
obj.o = obj; //构成了循环引用
var result = clone(obj);
console.log(result);
在上面的代码中,添加了obj.o=obj
.然后出现了Maximum call stack size exceeded
以上的错误表明了递归进入了死循环导致栈内存溢出。
原因是:对象存在循环引用的情况,也就是对象的属性间接或直接引用了自身的情况。
解决的方法:这里我们可以额外开辟一个存储空间,在这个存储空间中存储当前对象和拷贝对象之间的对应关系。
当需要拷贝当前的对象的时候,先去这个存储空间中进行查找,如果没有拷贝过这个对象,执行拷贝操作。如果已经拷贝过这个对象,直接返回,这样就可以解决循环引用的问题。
let map = new WeakMap();
function clone(target) {
if (typeof target === "object") {
//判断target是否为数组
let objTarget = Array.isArray(target) ? [] : {};
// 如果有直接返回
if (map.get(target)) {
return target;
}
//存储当前对象与拷贝对象的对应关系
map.set(target, objTarget);
for (const key in target) {
objTarget[key] = clone(target[key]);
}
return objTarget;
} else {
return target;
}
}
var obj = {
userName: "zhangsan",
a: {
a1: "hello",
},
//添加数组
arr: [2, 3],
};
obj.o = obj; //构成了循环引用
var result = clone(obj);
console.log(result);
以上就是一个基本的深拷贝的案例。
# 5、重写原型对象的问题
原型对象
在前面的课程中,我们讲解过原型对象,我们知道每个函数在创建的时候都会有一个prototype
属性,它指向函数的原型对象。
在这个对象中可以包含所有实例共享的属性和方法。例如上图中的sayName
方法。
同时在每个原型对象上都会增加一个constructor
属性,该属性指向prototype
属性所在的构造函数,如上图所示。
当我们通过new
操作符创建一个实例的时候,该实例就有了一个__proto__
属性,该属性指向了构造函数的原型对象,如上图所示:
所以说,__proto__
属性可以看作是一个连接实例与构造函数的原型对象的桥梁。
**所以三者的关系是,每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。**通俗点说就是,实例通过内部指针可以访问到原型对象,原型对象通过constructor指针,又可以找到构造函数。也就是上图体现的内容。
下面我们来看一个问题:重写原型对象
我们在前面写代码的时候,每次都是为原型对象添加一个属性或者函数时,都是直接给Person.prototype
上添加,这种写法比较冗余。
我们可以将属性和方法写成一个字面量对象的形式,然后在赋值给prototype
属性。
如下代码所示:
function Person() {}
Person.prototype = {
userName: "zhangsan",
age: 20,
sayHi: function () {
console.log(this.userName);
},
};
var person = new Person();
person.sayHi(); //zhangsan
通过执行的结果,依然可以获取到原型对象上属性的值。
当我们把一个字面量对象赋值给prototype
属性以后,实际上就是重写了原型对象。
但是这时候,我们打印Person.prototype.constructor
的时候,发现不在指向Person
这个构造函数,而是指向了Object
构造函数。
function Person() {}
Person.prototype = {
userName: "zhangsan",
age: 20,
sayHi: function () {
console.log(this.userName);
},
};
var person = new Person();
person.sayHi();
console.log(Person.prototype.constructor); // Object
原因是:在重写prototype
的时候,我们使用字面量创建了一个新的对象,并且这个新的对象中少了constructor
属性,
如下图所示
而我们可以看到在字面量对象中有一个__proto__
属性,指向了Object
的原型对象,这时,只能去Object
原型对象中查找是否有constructor
属性,而Object
原型对象中的constructor
指向的还是Object
.所以最终输出结果为Object
.
我们怎样避免这种情况呢?
可以在重写原型对象的时候添加constructor
属性。这样就不用在去新对象的原型对象中查找constructor
属性了。
function Person() {}
Person.prototype = {
constructor: Person, //添加constructor
userName: "zhangsan",
age: 20,
sayHi: function () {
console.log(this.userName);
},
};
var person = new Person();
person.sayHi();
console.log(Person.prototype.constructor);// Person
重写了原型对象以后,还需要注意一个问题,就是在重写原型对象之前,已经生成的对象的实例,无法获取新的原型对象中的属性和方法。
如下代码所示:
<script>
function Person() {}
var person = new Person();//在重写原型对象之前,生成对象的实例
Person.prototype = {
constructor: Person, //添加constructor
userName: "zhangsan",
age: 20,
sayHi: function () {
console.log(this.userName);
},
};
// var person = new Person();
person.sayHi(); // person.sayHi is not a function,无法获取sayHi函数。
console.log(Person.prototype.constructor);
</script>
造成上面错误的原因是:person
这个对象指向的是最初的原型对象,而最初的原型对象中是没有sayHi
这个方法的。
所以在执行的时候会抛出异常。
# 6、继承的实现方式有哪些
关于继承的实现方式,在前面,我们也已经讲解过,这里做一个汇总。
# 原型链继承
代码如下:
function Animal() {
this.superType = "Animal";
this.name = name || "动物";
//实例方法
this.sleep = function () {
console.log(this.name + "正在睡觉!!");
};
}
//原型上的函数
Animal.prototype.eat = function (food) {
console.log(this.name + "正在吃:" + food);
};
function Dog(name) {
this.name = name;
}
// 改变Dog的prototype指向,指向了一个Animal实例,实现了原型继承
Dog.prototype = new Animal();
var doggie = new Dog("wangcai");
console.log(doggie.superType);
doggie.sleep();
doggie.eat("狗粮");
在上面的代码中,将Animal
的实例赋值给了Dog
的原型对象,这样就实现了原型的继承,所以Dog
的实例可以获取父类Animal
中的superType
属性,调用父类中的实例方法,原型上的函数。
下面,可以通过一张图来理解一下:
原来的构造函数Dog
的prototype
指向的是Dog
的原型对象,但是现在指向了Animal
的实例对象。也就是说构造函数Dog
的原型对象为Animal
的实例对象。
这样会出现什么样的效果呢?
注意:上面我们所写的代码还是有一个小的问题的。
Dog.prototype.constructor
指向了Animal
Dog.prototype.constructor===Animal // true
这里,还是要求Dog.prototype.constructor
指向Dog
function Animal() {
this.superType = "Animal";
this.name = name || "动物";
//实例方法
this.sleep = function () {
console.log(this.name + "正在睡觉!!");
};
}
Animal.prototype.eat = function (food) {
console.log(this.name + "正在吃:" + food);
};
function Dog(name) {
this.name = name;
}
// 改变Dog的prototype指向,指向了一个Animal实例,实现了原型继承
Dog.prototype = new Animal();
// 将Dog的构造函数指向自身
Dog.prototype.constructor = Dog;
var doggie = new Dog("wangcai");
console.log(doggie.superType);
doggie.sleep();
doggie.eat("狗粮");
原型继承的优点:
第一:实现起来非常简单
只要设置子类的portotype
属性为父类的实例即可。
第二:可以通过子类的实例直接访问父类原型链中的属性和函数。
原型继承的缺点:
第一:我们知道子类的所有实例将共享父类的属性,这样就会导致一个问题:如果父类中的某个属性的值为引用类型,某个子类的实例去修改这个属性的值,就会影响到其它实例的值。
如下代码所示:
function Person() {
this.emotion = ["吃饭", "睡觉", "学习"]; // 爱好
}
function Studnet(id) {
this.id = id; // 学号
}
Studnet.prototype = new Person();
Studnet.prototype.constructor = Studnet;
var stu1 = new Studnet(1001);
console.log(stu1.emotion); // ["吃饭", "睡觉", "学习"]
stu1.emotion.push("玩游戏");
console.log(stu1.emotion); // ["吃饭", "睡觉", "学习", "玩游戏"]
//创建 stu2对象
var stu2 = new Studnet(1002);
console.log(stu2.emotion); // ["吃饭", "睡觉", "学习", "玩游戏"]
通过上面的代码,我们可以看到stu1
对象向数组emotion
数组中添加了一项以后,stu2
对象也收到了影响。
第二:在创建子类的实例的时候,无法向父类的构造函数中传递参数。
在通过new
操作符创建子类的实例的时候,会调用子类的构造函数,而在子类的构造函数中并没有设置与父类的关联操作,所以无法向父类的构造函数中传递参数。
第三:在给子类的原型对象上添加属性或者是方法的时候,一定要放在Student.prototype=new Person
语句的后面。
如下代码:
function Person() {
this.emotion = ["吃饭", "睡觉", "学习"]; // 爱好
}
function Studnet(id) {
this.id = id; // 学号
}
//在Studnet.prototype = new Person();代码前给Student的prototype添加study方法。
Studnet.prototype.study = function () {
console.log("好好学习,天天向上");
};
Studnet.prototype = new Person();
Studnet.prototype.constructor = Studnet;
var stu1 = new Studnet(1001);
stu1.study();
指向上面的代码,会出现stu1.study is not a function
的错误。
原因:后面通过Studnet.prototype = new Person();
这行代码对Student
的原型对象进行了重写,所以导致study
方法无效了。
修改后的代码:
function Person() {
this.emotion = ["吃饭", "睡觉", "学习"]; // 爱好
}
function Studnet(id) {
this.id = id; // 学号
}
Studnet.prototype = new Person();
Studnet.prototype.constructor = Studnet;
//放在了Studnet.prototype=new Person语句的后面
Studnet.prototype.study = function () {
console.log("好好学习,天天向上");
};
var stu1 = new Studnet(1001);
stu1.study();
# 构造函数继承
在子类的构造函数中,通过apply()
方法或者是call()
方法,调用父类的构造函数,从而实现继承功能。
function Person() {
this.emotion = ["吃饭", "睡觉", "学习"]; // 爱好
}
function Studnet(id) {
this.id = id; // 学号
Person.call(this);
}
var stu1 = new Studnet(1001);
console.log(stu1.emotion);
如下代码:
function Person() {
this.emotion = ["吃饭", "睡觉", "学习"]; // 爱好
}
function Studnet(id) {
this.id = id; // 学号
Person.call(this);
}
var stu1 = new Studnet(1001);
var stu2 = new Studnet(1002);
stu1.emotion.push("玩游戏");
console.log(stu1.emotion); // ["吃饭", "睡觉", "学习", "玩游戏"]
console.log(stu2.emotion); // ["吃饭", "睡觉", "学习"]
通过上面的代码,可以看到stu1
对象向emotion
数组中添加数据,并不会影响到stu2
对象。
构造函数继承的优点
第一:由于在子类的构造中通过call
改变了父类中的this
指向,导致了在父类构造函数中定义的属性或者是方法都赋值给了子类,这样生成的每个子类的实例中都具有了这些属性和方法。而且它们之间是互不影响的,及时是引用类型。
第二:创建子类的实例的时候,可以向父类的构造函数中传递参数。
//传递age参数
function Person(age) {
this.emotion = ["吃饭", "睡觉", "学习"];
this.age = age;
}
//传递age参数
function Studnet(id, age) {
this.id = id;
// 传递age参数
Person.call(this, age);
}
var stu1 = new Studnet(1001, 20);//传递年龄
var stu2 = new Studnet(1002, 21);
stu1.emotion.push("玩游戏");
console.log(stu1.emotion); // ["吃饭", "睡觉", "学习", "玩游戏"]
console.log(stu2.emotion); // ["吃饭", "睡觉", "学习"]
console.log(stu1.age); // 20
console.log(stu2.age); // 21
构造函数继承的缺点
第一:子类只能继承父类中实例的属性和方法,无法继承父类原型对象上的属性和方法。
function Person(age) {
this.emotion = ["吃饭", "睡觉", "学习"]; // 爱好
this.age = age;
}
// 原型上的方法
Person.prototype.study = function () {
console.log("好好学习,天天向上");
};
function Studnet(id, age) {
this.id = id; // 学号
Person.call(this, age);
}
var stu = new Studnet(1001, 20);
console.log(stu.age); // 20
stu.study(); //stu.study is not a function
第二:在父类的构造函数中添加一个实例方法,对应的子类也就有了该实例方法,但是问题时,每创建一个子类的实例,都会有一个父类中的实例方法,这样导致的结果就是占用内存比较大。以前我们是定义在prototype
原型上来解决这个问题的,但是在构造函数的继承中,又出现了这个。
function Person(age) {
this.emotion = ["吃饭", "睡觉", "学习"]; // 爱好
this.age = age;
this.study = function () {
console.log(this.id + "号同学要努力学习");
};
}
function Studnet(id, age) {
this.id = id; // 学号
Person.call(this, age);
}
var stu = new Studnet(1001, 20);
stu.study();
var stu1 = new Studnet(1002, 20);
stu1.study();
//stu对象和stu1对象都单独有一个study方法。
# 拷贝继承
所谓的拷贝继承指的是先创建父类的实例,然后通过for...in
的方式来遍历父类实例中的所有属性和方法,并依次赋值给子类的实例,同时原型上的属性和函数也赋给子类的实例。
function Person(age) {
this.emotion = ["吃饭", "睡觉", "学习"]; // 爱好
this.age = age;
this.study = function () {
console.log(this.id + "号同学要努力学习");
};
}
Person.prototype.run = function () {
console.log(this.id + "号学生正在跑步,年龄是:" + this.age);
};
function Studnet(id, age) {
var person = new Person(age);
for (var key in person) {
if (person.hasOwnProperty(key)) {
this[key] = person[key];
} else {
Studnet.prototype[key] = person[key];
}
}
// 子类自身的属性
this.id = id;
}
var student = new Studnet(1001, 21);
student.study();
student.run();
在上面的代码中,创建了父类Person
,并且在该类中指定了相应的实例属性和实例方法,同时为其原型对象中也添加了方法。
在Studnet
这个子类中,首先会创建父类Person
的实例,然后通过for...in
来进行遍历,获取父类中的属性和方法,获取以后进行判断,如果person.hasOwnProperty(key)
返回值为false
,表示获取到的是父类原型对象上的属性和方法,所以也要添加到子类的prototype
属性上,成为子类的原生对象上的属性或者是方法。
最后创建子类的实例student
,通过子类的实例student
,可以访问继承到的属性或者是方法。
拷贝继承的优点
第一:可以实现向父类中的构造方法中传递参数。
第二:能够实现让子类继承父类中的实例属性,实例方法以及原型对象上的属性和方法。
拷贝继承的缺点
父类的所有属性和方法,子类都需要复制拷贝一遍,所以比较消耗内存。
# 组合继承
组合继承的核心思想是将构造函数继承与原型继承两种方式组合在一起。
function Person(age) {
this.emotion = ["吃饭", "睡觉", "学习"]; // 爱好
this.age = age;
this.study = function () {
console.log(this.id + "号同学要努力学习");
};
}
Person.prototype.run = function () {
console.log(this.id + "号学生正在跑步,年龄是:" + this.age);
};
function Studnet(id, age) {
Person.call(this, age);
this.id = id; //子类独有的属性
}
Studnet.prototype = new Person();
Studnet.prototype.constructor = Studnet;
var student = new Studnet(1001, 21);
student.run();
console.log("爱好是:" + student.emotion);
组合继承的优点
第一:通过Person.call(this,ge)
这个行代码,可以将父类中的实例属性和方法添加到子类Student
中,另外通过Studnet.prototype = new Person();
可以将父类的原型对象上的属性和函数绑定到Student
的原型对象上。
第二:可以向父类的构造函数中传递参数。
组合继承的缺点
组合继承的主要缺点是父类的实例属性会绑定两次。
第一次是在子类的构造函数中通过call( )
函数调用了一次父类的构造函数,完成实例属性和方法的绑定操作。
第二次是在改写子类prototype
属性的时候,我们执行了一次new Person()
的操作,这里又将父类的构造函数调用了一次,完成了属性的绑定操作。
所以在整个组合继承的过程中,父类实例的属性和方法会进行两次的绑定操作。当然这里需要你注意的一点是:通过call()
函数完成父类中实例属性和方法的绑定的优先级要高于通过改写子类prototype
的方式。也就是说第一种方式会覆盖第二种方式:
如下代码所示:
function Person(age) {
this.emotion = ["吃饭", "睡觉", "学习"]; // 爱好
this.age = age;
//实例方法
this.study = function () {
console.log(this.id + "号同学要努力学习");
};
}
Person.prototype.run = function () {
console.log(this.id + "号学生正在跑步,年龄是:" + this.age);
};
// 原型方法
Person.prototype.study = function () {
console.log(this.id + "号学生需要好好学习");
};
function Studnet(id, age) {
Person.call(this, age);
this.id = id; //子类独有的属性
}
Studnet.prototype = new Person();
Studnet.prototype.constructor = Student;
var student = new Studnet(1001, 21);
student.run();
console.log("爱好是:" + student.emotion);
student.study(); //调用父类的实例方法student
在上面的代码中,在父类Person
的构造函数中定义了实例方法study
,同时在其原型对象上也定义了一个study
方法。
通过子类的实例调用study
方法的时候,调用的是父类的实例方法study
.
# 寄生式组合继承
function Person(age) {
this.emotion = ["吃饭", "睡觉", "学习"]; // 爱好
this.age = age;
this.study = function () {
console.log(this.id + "号同学要努力学习");
};
}
Person.prototype.run = function () {
console.log(this.id + "号学生正在跑步,年龄是:" + this.age);
};
Person.prototype.study = function () {
console.log(this.id + "号学生需要好好学习");
};
function Studnet(id, age) {
Person.call(this, age);
this.id = id;
}
// 定义Super构造函数
function Super() {}
//Super.prototype原型对象指向了Person.prototype
Super.prototype = Person.prototype;
//Student.prototype原型对象指向了Super的实例,这样就去掉了Person父类的实例属性。
Studnet.prototype = new Super();
Studnet.prototype.constructor = Studnet;
var student = new Studnet(1001, 21);
student.run();
console.log("爱好是:" + student.emotion);
student.study();
在上面的代码中,创建了一个Super
构造函数,让Super.prototype
的原型指向了Person.prototype
,同时将Super
的对象赋值给了Student.prototype
,这样就去掉了Person
父类的实例属性。
通过寄生式组合继承解决了组合继承的问题。
同时,在以后的应用中,可以使用组合继承,也可以使用寄生式组合继承。
# 7、模拟jQuery实现
下面我们通过模拟实现一个简单的jQuery
,来巩固原型的应用。
<script>
// 为jQuery起一个别名,模仿jQuery的框架
var $ = (jQuery = function () {});
// 为jQuery原型起一个别名
//这里没有直接赋值给fn,否则它属于window对象,容易造成全局污染
//后面要访问jquery的原型,可以直接通过jQuery.fn来实现
jQuery.fn = jQuery.prototype = {
version: "6.1.1", //添加原型属性,表示jquery的版本
//添加原型方法,表示返回jquery对象的长度
size: function () {
return this.length;
},
};
</script>
下面,我们使用jQuery
原型中的size
方法和version
属性。
// 为jQuery起一个别名,模仿jQuery的框架
var $ = (jQuery = function () {});
// 为jQuery原型起一个别名
//这里没有直接赋值给fn,否则它属于window对象,容易造成全局污染
//后面要访问jquery的原型,可以直接通过jQuery.fn来实现
jQuery.fn = jQuery.prototype = {
version: "6.1.1", //添加原型属性,表示jquery的版本
//添加原型方法,表示返回jquery对象的长度
size: function () {
return this.length;
},
};
var jq = new $();
console.log(jq.version); // 6.1.1
console.log(jq.size()); // undefined
在上面的代码中,我们是创建了一个jquery
的实例,然后通过该实例完成了原型属性和方法的调用。
但是在jquery
库中,是采用如下的方式进行调用。
$().version;
$().size()
通过以上的两行代码,我们可以看到在jQuery
库中,并没有使用new
操作符,而是直接使用小括号运算符完成了对jQuery
构造函数的调用。然后后面直接访问原型成员。
那应该怎样实现这种操作?
我们想到的就是,在jquery
的构造函数中,直接创建jQuery
类的实例。
// 为jQuery起一个别名,模仿jQuery的框架
var $ = (jQuery = function () {
return new jQuery();
});
// 为jQuery原型起一个别名
//这里没有直接赋值给fn,否则它属于window对象,容易造成全局污染
//后面要访问jquery的原型,可以直接通过jQuery.fn来实现
jQuery.fn = jQuery.prototype = {
version: "6.1.1", //添加原型属性,表示jquery的版本
//添加原型方法,表示返回jquery对象的长度
size: function () {
return this.length;
},
};
$().version;
// var jq = new $();
// console.log(jq.version); // 6.1.1
// console.log(jq.size());
在上面的代码中,给jQuery
构造函数直接返回了它的实例
,return new jQuery();
然后获取原型对象中的size
属性的值:$().version
.
但是,出现了如下的错误:
Uncaught RangeError: Maximum call stack size exceeded
以上错误的含义是栈内存溢出。
原因就是:当我们通过$()
调用构造函数的时候,内部有执行了new
操作,这时,又会重新执行jQuery
的构造函数,这样就造成了死循环。
var $ = (jQuery = function () {
return jQuery.fn.init(); //调用原型中的`init方法`
});
jQuery.fn = jQuery.prototype = {
init: function () {
return this; //返回jquery的原型对象
},
version: "6.1.1",
size: function () {
return this.length;
},
};
console.log($().version);
在上面的代码中,在jQuery
的构造方法中,调用的是原型中的init
方法,在该方法中,返回了jquery
的原型对象。
最后进行输出:cosnole.log($().version)
但是,以上的处理还是隐藏一个问题,具体看如下代码:
var $ = (jQuery = function () {
return jQuery.fn.init();
});
jQuery.fn = jQuery.prototype = {
init: function () {
this.length = 0; //原型属性length
this._size = function () { //原型方法
return this.length;
};
return this;
},
version: "6.1.1",
length: 1, // 原型属性
size: function () {
return this.length;
},
};
console.log($().version);
console.log($()._size()); // 0
console.log($().size()); // 0
在上面的代码中,在init
这个原型方法中添加了lenght
属性与_size
方法,在该方法中打印length
的值。
var $ = (jQuery = function () {
return new jQuery.fn.init(); //调用原型中的`init方法`
});
在jQuery
的构造函数中,通过new
操作符创建了一个实例对象,这样init()
方法中的this
指向的就是init
方法的实例,而不是jQuery.prototype
这个原型对象了。
console.log($().version); // 返回undefined
console.log($()._size()); // 0
console.log($().size()); // 抛出异常:Uncaught TypeError: $(...).size is not a function
下面,我们来看一下怎样解决现在面临的问题。
var $ = (jQuery = function () {
return new jQuery.fn.init(); //调用原型中的`init方法`
});
jQuery.fn = jQuery.prototype = {
init: function () {
this.length = 0;
this._size = function () {
return this.length;
};
return this;
},
version: "6.1.1",
length: 1,
size: function () {
return this.length;
},
};
// 将`jQuery`的原型对象覆盖掉init的原型对象。
jQuery.fn.init.prototype = jQuery.fn;
console.log($().version); //6.1.1
console.log($()._size()); // 0
console.log($().size()); // 0
在上面的代码中,我们添加了一行代码:
jQuery.fn.init.prototype = jQuery.fn;
console.log($().version);
下面,要实现的是选择器功能
jQuery
构造函数包括两个参数,分别是selector
和context
,selector
表示的是选择器,context
表示匹配的上下文,也就是可选择的访问,一般表示的是一个DOM
元素。这里我们只考虑标签选择器。
<script>
// 给构造函数传递selector,context两个参数
var $ = (jQuery = function (selector, context) {
return new jQuery.fn.init(selector, context); //调用原型中的`init方法`
});
jQuery.fn = jQuery.prototype = {
init: function (selector, context) {
selector = selector || document; //初始化选择器,默认值为document
context = context || document; // 初始化上下文对象,默认值为document
if (selector.nodeType) {
// 如果是DOM元素
// 把该DOM元素赋值给实例对象
this[0] = selector;
this.length = 1; //表示包含了1个元素
this.context = selector; //重新设置上下文对象
return this; //返回当前实例
}
if (typeof selector === "string") {
//如果选择器是一个字符串
var e = context.getElementsByTagName(selector); // 获取指定名称的元素
//通过for循环将所有元素存储到当前的实例中
for (var i = 0; i < e.length; i++) {
this[i] = e[i];
}
this.length = e.length; //存储元素的个数
this.context = context; //保存上下文对象
return this; //返回当前的实例
} else {
this.length = 0;
this.context = context;
return this;
}
// this.length = 0;
// console.log("init==", this);
// this._size = function () {
// return this.length;
// };
// return this;
},
// version: "6.1.1",
// length: 1,
// size: function () {
// return this.length;
// },
};
jQuery.fn.init.prototype = jQuery.fn;
window.onload = function () {
console.log($("div").length);
};
// console.log($().version);
// console.log($()._size()); // 0
// console.log($().size()); // 0
// var jq = new $();
// console.log(jq.version); // 6.1.1
// console.log(jq.size());
</script>
<div></div>
<div></div>
</body>
在上面的代码中,当页面加载完以后,这时会触发onload
事件,在该事件对应的处理函数中,通过$("div")
,传递的是字符串,
selector
参数表示的就是div
这个字符串,这里没有传递context
参数,表示的就是document
对象。
最后打印元素的个数。
在使用jQuery
库的时候,我们经常可以看到如下的操作:
$('div').html()
以上代码的含义就是直接在jQuery
对象上调用html( )
方法来操作jQuery
包含所有的DOM
元素。
html()
方法的实现如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
// 给构造函数传递selector,context两个参数
var $ = (jQuery = function (selector, context) {
return new jQuery.fn.init(selector, context); //调用原型中的`init方法`
});
jQuery.fn = jQuery.prototype = {
init: function (selector, context) {
selector = selector || document; //初始化选择器,默认值为document
context = context || document; // 初始化上下文对象,默认值为document
if (selector.nodeType) {
// 如果是DOM元素
// 把该DOM元素赋值给实例对象
this[0] = selector;
this.length = 1; //表示包含了1个元素
this.context = selector; //重新设置上下文对象
return this; //返回当前实例
}
if (typeof selector === "string") {
//如果选择器是一个字符串
var e = context.getElementsByTagName(selector); // 获取指定名称的元素
//通过for循环将所有元素存储到当前的实例中
for (var i = 0; i < e.length; i++) {
this[i] = e[i];
}
this.length = e.length; //存储元素的个数
this.context = context; //保存上下文对象
return this; //返回当前的实例
} else {
this.length = 0;
this.context = context;
return this;
}
// this.length = 0;
// console.log("init==", this);
// this._size = function () {
// return this.length;
// };
// return this;
},
html: function (val) {
jQuery.each(
this,
function (val) {
this.innerHTML = val;
},
val
);
},
// version: "6.1.1",
// length: 1,
// size: function () {
// return this.length;
// },
};
jQuery.fn.init.prototype = jQuery.fn;
//提供each扩展方法
jQuery.each = function (object, callback, args) {
//通过for循环的方式来遍历jQuery对象中的每个DOM元素。
for (var i = 0; i < object.length; i++) {
// 在每个DOM元素上调用回调函数
callback.call(object[i], args);
}
return object; //返回jQuery对象。
};
window.onload = function () {
// console.log($("div").length);
$("div").html("<h2>hello<h2>");
};
// console.log($().version);
// console.log($()._size()); // 0
// console.log($().size()); // 0
// var jq = new $();
// console.log(jq.version); // 6.1.1
// console.log(jq.size());
</script>
<div></div>
<div></div>
</body>
</html>
在上面的代码中,首先添加了jQuery.each
方法。
//提供each扩展方法
jQuery.each = function (object, callback, args) {
//通过for循环的方式来遍历jQuery对象中的每个DOM元素。
for (var i = 0; i < object.length; i++) {
// 在每个DOM元素上调用回调函数
//这里的让回调函数中的this指向了dom元素。
callback.call(object[i], args);
}
return object; //返回jQuery对象。
};
在上面的代码中,通过for
循环遍历jQuery
对象中的每个DOM
元素。然后执行回调函数callback
在jQuery
的原型对象上,添加html
方法
html: function (val) {
jQuery.each(
this, //表示jQuery原型对象
function (val) {
//this表示的是dom元素,这里是div元素
this.innerHTML = val;
},
val //表示传递过来的`<h2>hello<h2>`
);
},
在html
方法中完成对jQuery.each
方法的调用。
window.onload
的方法修改成如下的形式:
window.onload = function () {
// console.log($("div").length);
$("div").html("<h2>hello<h2>");
};
# **下面我们实现jQuery
的扩展功能
jQuery 提供了良好的扩展接口,方便用户自定义 jQuery 方法。根据设计习惯,如果为 jQuery 或者 jQuery.prototype 新增方法时,我们可以直接通过点语法来实现,例如上面我们扩展的html
方法,或者在 jQuery.prototype 对象结构内增加。但是,如果分析 jQuery 源码,会发现它是通过 extend() 函数来实现功能扩展的。
通过extend()
方法来实现扩展的好处是:方便用户快速的扩展jQuery
功能,但不会破坏jQuery
框架的结构。如果直接在jQuery
源码中添加方法,这样就破坏了Jquery
框架的结构,不方便后期的代码维护。
如果后期不需要某个功能,可以直接使用Jquery
提供的方法删除,而不需要从源码中在对该功能进行删除。
extend() 函数的功能很简单,它只是把指定对象的方法复制给jQuery
对象或者 jQuery.prototype
。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
// 给构造函数传递selector,context两个参数
var $ = (jQuery = function (selector, context) {
return new jQuery.fn.init(selector, context); //调用原型中的`init方法`
});
jQuery.fn = jQuery.prototype = {
init: function (selector, context) {
selector = selector || document; //初始化选择器,默认值为document
context = context || document; // 初始化上下文对象,默认值为document
if (selector.nodeType) {
// 如果是DOM元素
// 把该DOM元素赋值给实例对象
this[0] = selector;
this.length = 1; //表示包含了1个元素
this.context = selector; //重新设置上下文对象
return this; //返回当前实例
}
if (typeof selector === "string") {
//如果选择器是一个字符串
var e = context.getElementsByTagName(selector); // 获取指定名称的元素
//通过for循环将所有元素存储到当前的实例中
for (var i = 0; i < e.length; i++) {
this[i] = e[i];
}
this.length = e.length; //存储元素的个数
this.context = context; //保存上下文对象
return this; //返回当前的实例
} else {
this.length = 0;
this.context = context;
return this;
}
// this.length = 0;
// console.log("init==", this);
// this._size = function () {
// return this.length;
// };
// return this;
},
// html: function (val) {
// jQuery.each(
// this,
// function (val) {
// this.innerHTML = val;
// },
// val
// );
// },
// version: "6.1.1",
// length: 1,
// size: function () {
// return this.length;
// },
};
jQuery.fn.init.prototype = jQuery.fn;
//提供each扩展方法
jQuery.each = function (object, callback, args) {
//通过for循环的方式来遍历jQuery对象中的每个DOM元素。
for (var i = 0; i < object.length; i++) {
// 在每个DOM元素上调用回调函数
callback.call(object[i], args);
}
return object; //返回jQuery对象。
};
jQuery.extend = jQuery.fn.extend = function (obj) {
for (var prop in obj) {
this[prop] = obj[prop];
}
return this;
};
jQuery.fn.extend({
html: function (val) {
jQuery.each(
this,
function (val) {
this.innerHTML = val;
},
val
);
},
});
window.onload = function () {
// console.log($("div").length);
$("div").html("<h2>hello<h2>");
};
// console.log($().version);
// console.log($()._size()); // 0
// console.log($().size()); // 0
// var jq = new $();
// console.log(jq.version); // 6.1.1
// console.log(jq.size());
</script>
<div></div>
<div></div>
</body>
</html>
在上面的代码中,我们为jQuery
的原型对象添加了extend
方法
jQuery.extend = jQuery.fn.extend = function (obj) {
for (var prop in obj) {
this[prop] = obj[prop];
}
return this;
};
把obj
对象中的属性添加到jQuery
原型对象上。
下面调用extend
方法,同时设置html
属性
jQuery.fn.extend({
html: function (val) {
jQuery.each(
this,
function (val) {
this.innerHTML = val;
},
val
);
},
});
这样jQuery
原型对象上就有了html
方法。
而把原来的html
方法的代码注释掉。
刷新浏览器,查看对应的效果。
参数传递
我们在使用jquery
的方法的时候,需要进行参数的传递,而且一般都要求传递的参数都是对象。
使用对象作为参数进行传递的好处,就是方便参数的管理,例如参数个数不受限制。
如果使用对象作为参数进行传递,需要解决的问题:如何解决并提取参数,如何处理默认值等问题。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
// 给构造函数传递selector,context两个参数
var $ = (jQuery = function (selector, context) {
return new jQuery.fn.init(selector, context); //调用原型中的`init方法`
});
jQuery.fn = jQuery.prototype = {
init: function (selector, context) {
selector = selector || document; //初始化选择器,默认值为document
context = context || document; // 初始化上下文对象,默认值为document
if (selector.nodeType) {
// 如果是DOM元素
// 把该DOM元素赋值给实例对象
this[0] = selector;
this.length = 1; //表示包含了1个元素
this.context = selector; //重新设置上下文对象
return this; //返回当前实例
}
if (typeof selector === "string") {
//如果选择器是一个字符串
var e = context.getElementsByTagName(selector); // 获取指定名称的元素
//通过for循环将所有元素存储到当前的实例中
for (var i = 0; i < e.length; i++) {
this[i] = e[i];
}
this.length = e.length; //存储元素的个数
this.context = context; //保存上下文对象
return this; //返回当前的实例
} else {
this.length = 0;
this.context = context;
return this;
}
// this.length = 0;
// console.log("init==", this);
// this._size = function () {
// return this.length;
// };
// return this;
},
// html: function (val) {
// jQuery.each(
// this,
// function (val) {
// this.innerHTML = val;
// },
// val
// );
// },
// version: "6.1.1",
// length: 1,
// size: function () {
// return this.length;
// },
};
jQuery.fn.init.prototype = jQuery.fn;
//提供each扩展方法
jQuery.each = function (object, callback, args) {
console.log("args=", args);
//通过for循环的方式来遍历jQuery对象中的每个DOM元素。
for (var i = 0; i < object.length; i++) {
// 在每个DOM元素上调用回调函数
callback.call(object[i], args);
}
return object; //返回jQuery对象。
};
// jQuery.extend = jQuery.fn.extend = function (obj) {
// for (var prop in obj) {
// this[prop] = obj[prop];
// }
// return this;
// };
jQuery.extend = jQuery.fn.extend = function () {
var destination = arguments[0],
source = arguments[1];
//如果存在两个参数,并且都是对象
if (typeof destination === "object" && typeof source === "object") {
//把第二个对象合并到第一个参数对象中,并返回合并后的对象
for (var property in source) {
destination[property] = source[property];
}
return destination;
} else {
for (var prop in destination) {
this[prop] = destination[prop];
}
return this;
}
};
jQuery.fn.extend({
html: function (val) {
jQuery.each(
this,
function (val) {
this.innerHTML = val;
},
val
);
},
});
jQuery.fn.extend({
fontStyle: function (obj) {
var defaults = {
color: "#ccc",
size: "16px",
};
//如果有参数,会覆盖掉默认的参数
defaults = jQuery.extend(defaults, obj || {});
//为每个DOM元素执设置样式.
jQuery.each(this, function () {
this.style.color = defaults.color;
this.style.fontSize = defaults.size;
});
},
});
window.onload = function () {
// console.log($("div").length);
$("div").html("<h2>hello<h2>");
$("p").fontStyle({
color: "red",
size: "30px",
});
};
// console.log($().version);
// console.log($()._size()); // 0
// console.log($().size()); // 0
// var jq = new $();
// console.log(jq.version); // 6.1.1
// console.log(jq.size());
</script>
<div></div>
<div></div>
<p>学习前端</p>
<p>学习前端</p>
</body>
</html>
在上面的代码中,重新改造extend
方法。
jQuery.extend = jQuery.fn.extend = function () {
var destination = arguments[0],
source = arguments[1];
//如果存在两个参数,并且都是对象
if (typeof destination === "object" && typeof source === "object") {
//把第二个对象合并到第一个参数对象中,并返回合并后的对象
for (var property in source) {
destination[property] = source[property];
}
return destination;
} else {
for (var prop in destination) {
this[prop] = destination[prop];
}
return this;
}
};
在extend
方法中,首先获取两个参数,然后判断这两个参数是否都是对象,如果都是对象,把第二个参数对象合并到第一个参数对象中,并返回合并后的对象。
否则,将第一个参数对象复制到jquery
的原型对象上。
jQuery.fn.extend({
fontStyle: function (obj) {
var defaults = {
color: "#ccc",
size: "16px",
};
//如果有参数,会覆盖掉默认的参数
defaults = jQuery.extend(defaults, obj || {});
// console.log("this==", this);//init {0: p, 1: p, length: 2, context: document}
//为每个DOM元素执设置样式.
jQuery.each(this, function () {
//这里的this表示的是p标签,因为在each方法内部通过call改变了this指向,让this指向了每个遍历得到的p元素
this.style.color = defaults.color;
this.style.fontSize = defaults.size;
});
},
});
在上面的代码中, 调用了extend
方法,然后传递了fontStyle
,这个fontStyle
可以用来设置文本的颜色与字体大小。
当我们第一次调用extend
方法的时候,只是传递了fontStyle
这个对象,这时,会将该对象添加到jQuery
原型对象上。
window.onload = function () {
// console.log($("div").length);
$("div").html("<h2>hello<h2>");
$("p").fontStyle({
color: "red",
size: "30px",
});
};
<div></div>
<div></div>
<p>学习前端</p>
<p>学习前端</p>
在onload
事件中,调用fontStyle
方法,并且传递了一个对象,这时在fontStyle
方法的内部,首先会创建一个defaults
默认的对象,然后再次调用extend
方法,将传递的对象合并到默认对象上,当然完成了值的覆盖。
下面调用each
方法,在each
方法中遍历每个元素,执行回调函数,并且改变this
的指向。
封装成独立的命名空间
以上已经实现了一个简单的jQuery
库,
但是这里还有一个问题,需要解决:当编写了大量的javascript
代码以后,引入该jquery
库就很容易出现代码冲突的问题,所以这里需要将jquery
库的代码与其他的javascript
代码进行隔离,这里使用闭包。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
(function (window) {
// 给构造函数传递selector,context两个参数
var $ = (jQuery = function (selector, context) {
return new jQuery.fn.init(selector, context); //调用原型中的`init方法`
});
jQuery.fn = jQuery.prototype = {
init: function (selector, context) {
selector = selector || document; //初始化选择器,默认值为document
context = context || document; // 初始化上下文对象,默认值为document
if (selector.nodeType) {
// 如果是DOM元素
// 把该DOM元素赋值给实例对象
this[0] = selector;
this.length = 1; //表示包含了1个元素
this.context = selector; //重新设置上下文对象
return this; //返回当前实例
}
if (typeof selector === "string") {
//如果选择器是一个字符串
var e = context.getElementsByTagName(selector); // 获取指定名称的元素
//通过for循环将所有元素存储到当前的实例中
for (var i = 0; i < e.length; i++) {
this[i] = e[i];
}
this.length = e.length; //存储元素的个数
this.context = context; //保存上下文对象
return this; //返回当前的实例
} else {
this.length = 0;
this.context = context;
return this;
}
// this.length = 0;
// console.log("init==", this);
// this._size = function () {
// return this.length;
// };
// return this;
},
// html: function (val) {
// jQuery.each(
// this,
// function (val) {
// this.innerHTML = val;
// },
// val
// );
// },
// version: "6.1.1",
// length: 1,
// size: function () {
// return this.length;
// },
};
jQuery.fn.init.prototype = jQuery.fn;
//提供each扩展方法
jQuery.each = function (object, callback, args) {
//通过for循环的方式来遍历jQuery对象中的每个DOM元素。
for (var i = 0; i < object.length; i++) {
// 在每个DOM元素上调用回调函数
callback.call(object[i], args);
}
return object; //返回jQuery对象。
};
// jQuery.extend = jQuery.fn.extend = function (obj) {
// for (var prop in obj) {
// this[prop] = obj[prop];
// }
// return this;
// };
jQuery.extend = jQuery.fn.extend = function () {
var destination = arguments[0],
source = arguments[1];
//如果存在两个参数,并且都是对象
if (typeof destination === "object" && typeof source === "object") {
//把第二个对象合并到第一个参数对象中,并返回合并后的对象
for (var property in source) {
destination[property] = source[property];
}
return destination;
} else {
for (var prop in destination) {
this[prop] = destination[prop];
}
return this;
}
};
// 开发jqueyr
window.jQuery = window.$ = jQuery;
})(window);
jQuery.fn.extend({
html: function (val) {
jQuery.each(
this,
function (val) {
this.innerHTML = val;
},
val
);
},
});
jQuery.fn.extend({
fontStyle: function (obj) {
var defaults = {
color: "#ccc",
size: "16px",
};
//如果有参数,会覆盖掉默认的参数
defaults = jQuery.extend(defaults, obj || {});
// console.log("this==", this);//init {0: p, 1: p, length: 2, context: document}
//为每个DOM元素执设置样式.
jQuery.each(this, function () {
//这里的this表示的是p标签,因为在each方法内部通过call改变了this指向,让this指向了每个遍历得到的p元素
this.style.color = defaults.color;
this.style.fontSize = defaults.size;
});
},
});
window.onload = function () {
// console.log($("div").length);
$("div").html("<h2>hello<h2>");
$("p").fontStyle({
color: "red",
size: "30px",
});
};
// console.log($().version);
// console.log($()._size()); // 0
// console.log($().size()); // 0
// var jq = new $();
// console.log(jq.version); // 6.1.1
// console.log(jq.size());
</script>
<div></div>
<div></div>
<p>学习前端</p>
<p>学习前端</p>
</body>
</html>
在上面的代码中,将jQuery
库放在匿名函数中,然后进行自调用,并且传入window
对象。
在上面所添加的代码中还要注意如下语句:
window.jQuery = window.$ = jQuery;
以上语句的作用:把闭包中的私有变量jQuery
传递给window
对象的jQuery
属性。这样就可以在全局作用域中通过jQuery
变量来访问闭包体内的jQuery
框架了。
以上就是我们模拟的jQuery
库。
# 五、DOM与事件
# 1、选择器
getElementById()
:通过id
来查找对应的元素。
getElementsByClassName()
:通过类名来查找对应的元素,返回的是一个HTMLCollection
对象。
getElementsByName()
:通过元素的name
属性查找对应的元素,返回的是NodeList
对象,它是一个类似于数组的结构。
getElementsByTagName()
: 通过标签的名称来查找对应的元素,返回的是HTMLCollection
对象。
querySelector
:该选择器返回的是在基准元素下,选择器匹配到的元素集合中的第一个元素。该选择器的参数接收的是一个css
选择
<body>
<div>
<h4>标题内容</h4>
<span>span标签内容</span>
<p>
段落内容
<span>段落中的第一个span标签</span><br />
<span>段落中的第二个span标签</span>
</p>
</div>
</body>
<script>
console.log(document.querySelector("p span").innerHTML);// 获取p标签中第一个span标签中的内容,所以输出结果为:段落中的第一个span标签
console.log(document.querySelector("h4,span").innerHTML);//获取第一个h4或者是span元素的内容:所以输出结果为:标题内容
var ele = document.querySelector("p");
console.log(ele.querySelector("div span").innerHTML);//段落中的第一个span标签。
// 首先先找到`p`元素,然后看一下p元素下面有没有div,我们发现没有,但是依然能够匹配到span元素。
//原因是:在匹配的过程中会优先找出最外层div元素下的span元素的集合,然后在判断span元素是否属于p元素的子元素,最后返回
//第一个匹配到的span元素的值。
</script>
下面,把HTML
文档的结构修改成如下的形式:
<div>
<h4>标题内容</h4>
<span>span标签内容</span>
<!--这里增加了一个p标签-->
<p>第一个段落</p>
<p>
段落内容
<span>段落中的第一个span标签</span><br />
<span>段落中的第二个span标签</span>
</p>
</div>
执行如下代码会出现异常:
var ele = document.querySelector("p");
console.log(ele.querySelector("div span").innerHTML);//Cannot read property 'innerHTML' of null
原因:会找到第一个p
元素,然后看一下p
标签中是否有div
,发现没有,但是会找出最外层div
下的所有span
元素的集合,看一下span
元素是否属于p
元素,而第一个p
元素中没有span
元素,所以抛出异常。
querySelectorAll()
选择器:
querySelectorAll
选择器与querySelector
选择器的区别是:querySelectAll
选择器会获取到基准元素下匹配到所有子元素的集合。返回的是一个NodeList
集合。
<div>
<h4>标题内容</h4>
<span>span标签内容</span>
<p>
段落内容
<span>段落中的第一个span标签</span><br />
<span>段落中的第二个span标签</span>
</p>
</div>
<script>
console.log(document.querySelectorAll("span"));//返回所有的span标签。
</script>
下面,再来看一段代码:
<div id="container">
<div class="bar"></div>
<div class="foo">
<div class="inner"></div>
</div>
</div>
<script>
// 获取container下的所有div元素。
var div1 = document.querySelectorAll("#container div");
console.log(div1);// NodeList(3) [div.bar, div.foo, div.inner]
</script>
# 2、HTMLCollection对象与NodeList对象区别
在介绍前面的选择器的时候,它们返回的值有些是HTMLCollection
对象,有些是NodeList
对象,它们有什么区别?
HTMLCollection
对象具有1ength
属性,返回集合的长度,可以通过item()
和namedItem()
函数来访问特定的元素。
item()
函数:通过序号索引值来获取特定的某个节点,超过索引则返回null
.
<div id="container">
<div class="bar"></div>
<div class="foo">
<div class="inner"></div>
</div>
</div>
<script>
var main = document.getElementById("container").children;
console.log(main); //HTMLCollection
console.log(main.item(0)); //输出:<div class="bar"></div>
console.log(main.item(1)); // 输出:foo元素
</script>
namedItem()
函数:该函数用来返回一个节点,首先通过id
属性去匹配,然后如果没有匹配到则使用name
属性匹配,如果还没有匹配到则返回null
. 当出现重复的id
或者name
属性时,只返回匹配到的第一个值。
<form id="form1">
<input type="text" id="userName" />
<input type="password" id="password" name="userPwd" />
</form>
<script>
var form1 = document.getElementById("form1").children;
console.log(form1.namedItem("userPwd"));// <input type="password" id="password" name="userPwd" />
</script>
NodeList
对象也具有length
属性,返回集合的长度,同样也有item
函数,也是通过索引定位子元素的位置。但是NodeList
对象没有namedItem
方法。
HTMLCollection
对象与NodeList
对象都是类似数组的结构,如果想调用数组中的方法,需要通过call()
函数或者是apply()
函数,转换为真正的数组后,可以使用数组中的函数。
同时,当我们对DOM
树新增或者是删除一个节点的时候,都会立即的放映在HTMLCollection
对象与NodeList
对象中。
<form id="form1">
<input type="text" id="userName" />
<input type="password" id="password" name="userPwd" />
</form>
<script>
//获取HTMLCollection集合
var form1Children = document.getElementById("form1").children;
// 获取form元素
var form1 = document.getElementById("form1");
console.log(form1Children.length); // 2 HTMLCollection中有两个子元素
var input = document.createElement("input"); //创建input元素
form1.appendChild(input); // 把创建的input元素添加到form元素中
console.log(form1Children.length); // 3 可以看到HTMLCollection立即受到了影响
最后,总结一下HTMLCollection
对象与NodeList
对象的相同点与不同点
相同点:
第一:都是类似数组的结构,有length
属性,可以通过call()
函数或者是apply()
函数转换成数组,使用数组中的函数。
第二:都用item
函数,通过索引值获取相应的元素。
第三:都是实时的,当在DOM
树上添加元素或者是删除元素,都会立即反应到HTMLCollection
对象和NodeList
对象上。
不同点:
第一:HTMLCollection
对象中,有namedItem()
函数,而NodeList
对象中没有.
第二:NodeList
对象中存储的是元素节点的集合,包括元素,以及节点,例如text
文本节点,而HTMLCollection
对象中只包含了元素的集合。
<form id="form1">
用户名<input type="text" id="userName" /> <br />
用户密码<input type="password" id="password" name="userPwd" />
</form>
<script>
//获取HTMLCollection集合
var form1Children = document.getElementById("form1").children;
console.log(form1Children);
//获取NodeList对象
var formNodes = document.getElementById("form1").childNodes;
console.log(formNodes);
</script>
通过查看浏览器控制台输出的结果,可以看出HTMLCollection
对象与NodeList
对象的区别。
# 3、常见的DOM操作有哪些?
添加节点
<form id="form1">
用户名<input type="text" id="userName" /> <br />
用户密码<input type="password" id="password" name="userPwd" />
</form>
<script>
var form1 = document.getElementById("form1");
//创建一个input元素
var newInput = document.createElement("input");
//创建属性
var newAttr = document.createAttribute("type");
newAttr.value = "password";
//将属性绑定到元素上
newInput.setAttributeNode(newAttr);
//创建一个文本节点
var newTextNode = document.createTextNode("用户密码");
form1.appendChild(newTextNode); //添加文本节点
form1.appendChild(newInput);
</script>
删除节点
<form id="form1">
用户名<input type="text" id="userName" /> <br />
用户密码<input type="password" id="password" name="userPwd" />
</form>
<script>
var form1 = document.getElementById("form1");
var nodeChilds = form1.childNodes;
console.log(nodeChilds);
form1.removeChild(nodeChilds[0]);
form1.removeChild(nodeChilds[0]);
</script>
在上面的代码中,我们想将表单中的用户名
这一项内容删除掉。
首先获取form
表单,然后在获取对应的子元素。
通过执行removeChild
方法删除第一个元素,而第一个元素是用户名
这个文本字符串,
下面还要删除文本框,所以再次调用了removeChild
函数,注意由于前面已经删除了用户名
这个文本元素了,所以文本框成为了第一个元素,所以这里写到索引值也是0.
删除文本框的id
属性
<form id="form1">
用户名<input type="text" id="userName" /> <br />
用户密码<input type="password" id="password" name="userPwd" />
</form>
<script>
var input = document.querySelector("#userName");
input.removeAttribute("id");
</script>
修改节点
修改元素节点
修改元素的节点的操作,一般是直接用新的节点替换旧的节点。关于节点的替换可以使用,replaceChild
函数来实现,该函数的调用是通过父元素来调用的,例如:把div1
中的内容替换掉,这里就需要通过container.replaceChild
方法来完成,replaceChild
方法需要两个参数,第一个参数表示的是新元素,第二个参数表示的是旧元素。
<div id="container">
<div id="div1">hello</div>
</div>
<script>
var container = document.getElementById("container"); //获取父元素container
var div1 = document.getElementById("div1"); //获取子元素
var newDiv = document.createElement("div"); // 创建一个新的div元素
var newText = document.createTextNode("nihao"); //创建一个文本内容
newDiv.appendChild(newText); //把创建的文本内容添加到新的div中
container.replaceChild(newDiv, div1); //用新的div替换旧的div,完成节点的修改操作。
</script>
修改属性节点
修改属性的节点,我们可以通过setAttribute()
函数来完成,如果想获取属性节点可以通过getAttribute()
函数来完成。
<div id="container">
<div id="div1" style="color: red">hello</div>
</div>
<script>
var div1 = document.getElementById("div1");
div1.setAttribute("style", "color:blue"); //设置style属性
console.log(div1.getAttribute("style")); // 获取style属性的值
</script>
修改属性节点除了通过setAttribute()
方法完成以外,还可以通过属性名直接进行修改
<div id="container">
<div id="div1" style="color: red">hello</div>
</div>
<script>
var div1 = document.getElementById("div1");
div1.style.color = "blue";
</script>
但是通过这种方式进行修改,还需要注意一个问题:直接修改的属性名与元素节点中的属性名不一定是一致的。例如class
这个属性,在javascript
中是关键字,不能直接作为属性使用,这时需要通过className
来完成。
<div id="container">
<div id="div1" style="color: red">hello</div>
<div id="div2" class="foo">前端学习</div>
</div>
<script>
var div2 = document.getElementById("div2");
div2.className = "bar"; //注意这里使用的是className
</script>
通过查看浏览器控制台,可以看到对应的样式发生了修改。
修改文本节点
文本节点的修改,可以通过innerHTML
属性来完成。
<div id="container">
<div id="div1" style="color: red">hello</div>
<div id="div2" class="foo">前端学习</div>
</div>
<script>
var div2 = document.getElementById("div2");
div2.innerHTML = "Vue 学习";
</script?>
# 4、DOM性能问题
Dom
操作非常消耗性能,应该尽量避免频繁的操作DOM
.
导致浏览器重绘,重新渲染,比较消耗cpu
资源,比较消耗性能。
提升性能的方案:
第一:对DOM
查询操作进行缓存
第二:将频繁操作修改为一次性操作
首先看第一种情况:
这里需要对页面中所有p
标签内文字调整大小(单击按钮完成)
//不缓存的结果
for (let i = 0; i < document.getElementsByTagName("p").length; i++) {
//每次循环,都会计算lenght,频繁进行DOM查询
}
const pList = document.getElementsByTagName("p");
const length = pList.length;
for (let i = 0; i < length; i++) {
//缓存length,只进行一次DOM查询
}
下面看一下第二种情况:
需求:页面中有一个ul
列表,需要单击按钮一次性插入10个或者100个li
?
传统的做法:
<ul id="list"></ul>
const listNode = document.getElementById("list");
for (let i = 0; i < 10; i++) {
const li = document.createElement("li");
li.innerHTML = `item${i}`;
list.appendChild(li);
}
执行上面的代码,可以实现对应的需求,但是问题是上面的操作是频繁操作dom
,性能比较低。
const listNode = document.getElementById("list");
//创建一个文档片段,文档片段存在于内存中,并不在DOM树中,所以此时还没有插入到DOM中
//也就是先将dom插入到临时区域中
const frag = document.createDocumentFragment();
//执行插入
for (let i = 0; i < 10; i++) {
const li = document.createElement("li");
li.innerHTML = `item${i}`;
frag.appendChild(li);
}
//都完成后,再插入到DOM树中
listNode.appendChild(frag);
# 5、什么是事件传播
在浏览器中,JavaScript
和HTML
之间的交互是通过事件实现的,常用的事件包括了鼠标点击的事件,鼠标移动事件等等。
当事件发生以后,会触发绑定在元素上的事件处理程序,执行相应的操作。
问题是当事件发生后,事件是怎样传播的呢?
事件发生后会在目标节点和根节点之间按照特定的顺序进行传播,路径经过的节点都会接收到事件。
这里的特定顺序是怎样的顺序呢?
第一种:事件传递的顺序是先触发最外层的元素,然后依次向内传播,这样的传递顺序我们称之为事件的捕获阶段。
第二种:事件传递的顺序是先触发最内层的元素,然后依次向外进行传播,这样的传递顺序我们称之为事件冒泡阶段。
当然,一个完整的事件传播包含了三个阶段
首先就是事件的捕获阶段
然后是事件的目标阶段,目标阶段指的就是事件已经到达目标元素。
最后是事件的冒泡阶段
以上就是关于事件传播的描述
# 6、什么是事件的捕获
关于事件捕获,在上一小节,我们已经介绍过:事件的传递是从最外层开始,依次向内传播,在捕获阶段,事件从window
开始,一直到触发事件的元素。
window----> document----> html----> body ---->目标元素
如下代码所示:
<body>
<table border="1">
<tbody>
<tr>
<td>单元格内容</td>
</tr>
</tbody>
</table>
<script>
var table = document.querySelector("table");
var tbody = document.querySelector("tbody");
var tr = document.querySelector("tr");
var td = document.querySelector("td");
table.addEventListener(
"click",
function () {
console.log("table");
},
true
);
tbody.addEventListener(
"click",
function () {
console.log("tbody");
},
true
);
tr.addEventListener(
"click",
function () {
console.log("tr");
},
true
);
td.addEventListener(
"click",
function () {
console.log("td");
},
true
);
</script>
</body>
在上面的代码中,有一个表格,给表格中的每个元素通过addEventListener
方法绑定了单击事件,同时该方法的第三个参数,设置为了true
,这样就表明事件将在捕获阶段发生。
所以当我们单击td
单元格的时候,事件的执行结果是:table
,tbody
,tr
,td
.也就是说事件从table
开始,依次向下传播。这个传播的过程就是事件捕获。
# 7、什么是事件冒泡
关于事件的冒泡,在前面也已经提到过:事件传递的顺序是先触发最内层的元素,然后依次向外进行传播,这样的传递顺序我们称之为事件冒泡阶段。
如下代码所示:
<body>
<table border="1">
<tbody>
<tr>
<td>单元格内容</td>
</tr>
</tbody>
</table>
<script>
var table = document.querySelector("table");
var tbody = document.querySelector("tbody");
var tr = document.querySelector("tr");
var td = document.querySelector("td");
table.addEventListener("click", function () {
console.log("table");
});
tbody.addEventListener("click", function () {
console.log("tbody");
});
tr.addEventListener("click", function () {
console.log("tr");
});
td.addEventListener("click", function () {
console.log("td");
});
</script>
</body>
上面的代码,我们将addEventListener
方法的第三个参数true
去掉了,这时就有事件的捕获变成了事件的冒泡。默认值为(false
).
但单击单元格的时候,执行的结果为:td
,tr
,tbody
,table
, 这个过程就是事件的冒泡。
# 8、阻止事件冒泡
现在,我们已经了解了事件冒泡的过程,但是在很多的情况下,我们需要阻止事件冒泡的发生。
例如:在上一小节的案例中,当我们单击了单元格后,不仅触发单元格元素的事件,同时也会触发其它元素的事件,而这里我们只希望触发单元格的事件。所以这里需要阻止事件的冒泡。
阻止事件的冒泡需要使用:event.stopPropagation()
函数
如下案例:
<script>
var table = document.querySelector("table");
var tbody = document.querySelector("tbody");
var tr = document.querySelector("tr");
var td = document.querySelector("td");
table.addEventListener("click", function () {
console.log("table");
});
tbody.addEventListener("click", function () {
console.log("tbody");
});
tr.addEventListener("click", function () {
console.log("tr");
});
td.addEventListener("click", function (event) {
//阻止了事件的冒泡操作
event.stopPropagation();
console.log("td");
});
</script>
在单元格的事件处理函数中,通过event.stopPropagation()
方法阻止了事件的冒泡。
与stopPropagation()
函数相对的还有一个stopImmediatePropagation
函数,它们两者之间有什么区别呢?
stopPropagation()
:函数会阻止事件冒泡,其它事件处理程序仍然可以调用
stopImmediatePropagation
函数不仅可以阻止事件冒泡,也会阻止其它事件处理程序的调用。
如下代码所示:
<body>
<table border="1">
<tbody>
<tr>
<td>单元格内容</td>
</tr>
</tbody>
</table>
<script>
var table = document.querySelector("table");
var tbody = document.querySelector("tbody");
var tr = document.querySelector("tr");
var td = document.querySelector("td");
table.addEventListener("click", function () {
console.log("table");
});
tbody.addEventListener("click", function () {
console.log("tbody");
});
tr.addEventListener("click", function () {
console.log("tr");
});
//单元格第一个单击事件
td.addEventListener("click", function (event) {
console.log("td1");
});
//单元格第二个单击事件
td.addEventListener("click", function (event) {
//阻止了事件的冒泡操作
// event.stopImmediatePropagation();
event.stopPropagation();
console.log("td2");
});
// 单元格第三个单击事件
td.addEventListener("click", function (event) {
console.log("td3");
});
</script>
</body>
在上面的代码中,给单元格添加了三个单击的事件,同时第二个单击事件使用了stopPropagation
方法来阻止冒泡行为。
执行结果如下:td1
,td2
,td3
通过执行结果,可以看到单元格的三个单击事件全部触发,并且阻止了冒泡的行为。
如果使用stopImmediatePropagation
方法,执行结果为:td1
,td2
通过执行的结果可以看到,阻止了冒泡的行为,但是没有触发单元格的第三个单击的事件,也就是说会阻止其它事件的执行。
# 9、事件冒泡与事件捕获问题
下面我们来看一段代码:
<body>
<table border="1">
<tbody>
<tr>
<td>单元格内容</td>
</tr>
</tbody>
</table>
<script>
var table = document.querySelector("table");
var tbody = document.querySelector("tbody");
var tr = document.querySelector("tr");
var td = document.querySelector("td");
//事件捕获
table.addEventListener(
"click",
function () {
console.log("table");
},
true
);
//事件冒泡
tbody.addEventListener("click", function () {
console.log("tbody");
});
//事件捕获
tr.addEventListener(
"click",
function () {
console.log("tr");
},
true
);
//事件冒泡
td.addEventListener("click", function (event) {
console.log("td");
});
</script>
</body>
在上面的代码中,既有事件捕获又有事件冒泡,那么执行的结果是怎样的呢?
当单击td
单元格后
执行结果:table
,tr
,td
,tbody
分析:前面我们讲解过,事件传播的循序是:先事件捕获阶段,然后事件的目标阶段,最后是事件冒泡阶段
所以说,在一个程序中有事件的捕获阶段,又有事件的冒泡阶段,会优先执行捕获阶段的事件。
所以上面代码整个执行的流程:
先执行table
这个捕获阶段,输出table
这个字符串
下面执行tbody
,但是tbody
绑定的是冒泡类的事件,所以不执行,跳过。
下面是tr
,而tr
是捕获类型的事件,所以会执行,输出字符串tr
下面是td
,由于我们单击的是td
元素,所以该元素就是事件目标元素,则会执行,输出td
字符串。
当单击了td
元素以后,就开始进入了事件冒泡阶段。这时会冒泡到tr
元素,但是tr
元素绑定的是捕获阶段的事件,所以不执行,直接跳过,下面继续冒泡到了tbody
元素,该元素绑定的是冒泡类型的事件,所以执行,输出字符串tbody
.
下面继续冒泡,执行到table
元素,该原生是捕获类型的事件,所以直接跳过,没有输出。
# 10、Event对象使用
在JavaScrip
中,每触发一个事件,就会产生一个Event
对象,在该对象中包含了所有与事件相关的内容,包括事件的元素,事件类型等。
当给某个元素绑定了事件处理程序后,就可以获取到Event
对象,但是在不同的浏览器下,Event
对象的实现还是有一定的差异的。
关于获取Event
对象的方式有两种:
第一种:在事件的处理程序中,可以通过参数来获取Event
对象。
第二种:在事件的处理程序中,可以通过window.event
属性获取Event
对象。
具体的示例代码如下:
<body>
<button id="btn">单击</button>
<script>
var btn = document.getElementById("btn");
btn.addEventListener("click", function (event) {
//通过参数获取Event对象
console.log("event=", event);
//通过window.event的方式来获取Event对象
var windEvent = window.event;
console.log("windEvent=", windEvent);
//判断两种方式是否相等
console.log(event === windEvent);
});
</script>
</body>
在谷歌浏览器中,测试上面的代码,可以发现两种获取Event
对象的方式是相等的。
但是注意,在其它的浏览中进行测试可能会出现不相等的情况,也就是有的浏览器会出现不支持window.event
这种方式来获取Event
对象,这里可以自行进行测试。
为了能够在获取事件对象的时候,支持不同的浏览器,我们可以做兼容性的处理。
<body>
<button id="btn">单击</button>
<script>
var eventFn = {
event: function (e) {
return e || window.event;
},
};
var btn = document.getElementById("btn");
btn.addEventListener("click", function (event) {
//通过参数获取Event对象
console.log("event=", eventFn.event(event));
//通过window.event的方式来获取Event对象
var windEvent = eventFn.event(window.event);
console.log("windEvent=", windEvent);
//判断两种方式是否相等
console.log(event === windEvent);
});
</script>
在上面的代码中定义了eventFn
对象,在该对象中有一个属性event
,在该属性中,判断返回Eevent
对象的方式。
在对应的事件处理函数中,可以调用eventFn
对象中的event
方法来获取Event
对象。
获取事件的目标元素
在事件的处理程序中,我们可能需要获取 事件的目标元素。
在IE
浏览器中,可以使用event
对象中的srcElement
属性来获取事件的目标元素,在非IE
浏览器中可以通过event
对象的target
属性来获取事件的目标元素,当然在有的非IE
浏览器下也支持event
对象中的srcElement
属性,目的是为了保持与ie
保持一致,但是要注意的是并不是所有的非IE
浏览器都支持srcElement
属性。
<script>
var eventFn = {
event: function (e) {
return e || window.event;
},
};
var btn = document.getElementById("btn");
btn.addEventListener("click", function (event) {
var event = eventFn.event(event);
console.log("target=", event.target);
console.log("srcElement=", event.srcElement);
});
</script>
在谷歌浏览器中进行测试,都可以获取target
属性和srcElement
属性的值。
关于其它浏览器的情况,可以自行测试。
当然为了能够兼容其它的浏览器,可以做一下兼容的处理。
var eventFn = {
event: function (e) {
return e || window.event;
},
target: function (e) {
return e.target || e.srcElement;
},
};
var btn = document.getElementById("btn");
btn.addEventListener("click", function (event) {
var event = eventFn.event(event);
console.log("target=", eventFn.target(event));
console.log("srcElement=", eventFn.target(event));
});
这里在eventFn
对象中封装了一个target
属性。
阻止默认行为
<a href="https://www.baidu.com" id="a1">链接</a>
<script>
var a1 = document.getElementById("a1");
a1.addEventListener("click", function (event) {
event.preventDefault(); //阻止默认行为
alert("你点击了链接");
});
</script>
关于Event
对象中的其他内容,可以参考文档。
# 11、介绍一下三种事件模型
关于JavaScript
的事件模型有三类,分别是DOM0
,DOM2
,DOM3
DOM0事件模型
DOM0
的事件模型指的是:将一个函数赋值给一个事件处理属性。
如下代码:
var btn=document.getElementById('btn')
btn.onclick=function(){}
或者也可以采用如下的方式:
<button onclick="fn()">
单击
</button>
<script>
function fn(){
console.log('hello')
}
</script>
需要注意的是:DOM0
事件模型的处理程序只支持冒泡阶段。
DOM0
事件模型的优点与缺点:
优点:实现起来非常简单,并且可以跨浏览器。
缺点:一个事件处理程序只能绑定一个函数。
<body>
<button id="btn" onclick="btnClick()">单击按钮</button>
<script>
var btn = document.getElementById("btn");
btn.onclick = function () {
console.log("hello");
};
function btnClick() {
console.log("123");
}
</script>
</body>
在上面的代码中,我们给按钮使用两种方法绑定了事件处理程序,但是DOM0
这种事件模型只能绑定一个函数,并且在JavaScript
中绑定事件处理程序的优先级高于在HTML
元素中定义的事件处理程序,所以打印结果为hello
.
如果删除元素绑定的事件,只需要将对应的事件处理程序设置为null
即可
btn.onclick=null
DOM2事件模型
针对DOM2
事件模型不同的浏览器厂商制定了不同的的实现方式,主要分为IE
浏览器和非IE浏览器
在IE10
及以下版本中只支持事件的冒泡,在IE11
中同时支持事件的捕获与事件冒泡。在IE10
及以下版本中,可以通过attachEvent
函数来添加事件处理程序,通过detachEvent
函数删除事件处理程序。
element.attachEvent('on'+ eventName,handler) // 添加事件处理程序
element.detachEvent('on'+ eventName,handler) // 删除事件处理程序
在IE11
和非IE
浏览器中,同时支持事件捕获和事件冒泡两个阶段,可以通过addEventListener()
函数添加事件处理程序,可以通过removeEventListener()
函数删除事件处理程序。
addEventListener(eventName,handler,useCapture) //添加事件处理程序
removeEventListener(eventName,handler,useCapture) // 删除事件处理程序
其中useCapture
如果为true
表示支持事件捕获,为false
b表示支持事件冒泡,默认是为false
通过上面的介绍,我们知道了DOM2
的事件处理程序存在两种情况,那这两种实现的方式之间有没有相同点和不同点呢?
相同点
第一:在DOM2
的事件处理中不管是IE
浏览器还是非IE
浏览器都支持对同一个事件绑定多个处理函数。
<body>
<button id="btn">单击按钮</button>
<script>
var btn = document.getElementById("btn");
btn.addEventListener("click", function () {
console.log("hello");
});
btn.addEventListener("click", function () {
console.log("nihao");
});
</script>
</body>
以上程序的输出结果为:hello
,nihao
第二:在需要删除绑定的事件的时候,,不能删除匿名函数,因为添加的函数和删除的函数必须是同一个函数。
下面的代码中,同时绑定和删除了handler
函数,这样做是完全可以的。
var btn=document.getElementById('btn')
var handle=function(){
console.log('hello');
}
btn.addEventListener('click',handle,false);
btn.removeEventListener('click',handle)
但是如果采用如下的删除方式是无法取消绑定的事件的。因为它们都是匿名函数,而并不是同一个函数。
btn.addEventListener('click',function(){
console.log('hello')
},false)
btn.removeEventListener('click',function(){})
不同点
第一:在IE
浏览器中,使用attachEvent
函数为同一个事件添加多个处理程序时,会按照添加的相反顺序执行。
<script>
var btn = document.getElementById("btn");
btn.attachEvent("onclick", function () {
console.log("hello");
});
btn.attachEvent("onclick", function () {
console.log("nihao");
});
</script>
当单击按钮的时候,先输出nihao
,再输出hello
.
第二:在IE
浏览中,attachEvent
函数添加的事件处理程序会在全局作用域中运行,因此this
指向的是window
.
在非IE
浏览器中,addEventListener()
函数添加的处理程序在指定的元素内部执行,this
指向所绑定的元素。
既然DOM2
事件的处理有浏览器的兼容性问题,那应该怎样进行处理呢?
var EventHandler = {
addEventHandler: function (ele, type, handler) {
if (ele.addEventListener) {
ele.addEventListener(type, handler);
} else if (ele.attachEvent) {
ele.attachEvent("on" + type, handler);
} else {
ele["on" + type] = handler;
}
},
removeEventHandler: function (ele, type, handler) {
if (ele.addEventListener) {
ele.removeEventHandler(type, handler);
} else if (ele.detachEvent) {
ele.detachEvent("on" + type, handler);
} else {
ele["on" + type] = null;
}
},
};
DOM3事件模型
DOM3
事件模型中允许自定义事件,自定义事件有createEvent("CustomEvent")
函数来完成。返回的对象有一个initCustomEvent
()方法接收如下四个参数。
1)type:字符串,触发的事件类型,自定义。例如 “keyDown”,“selectedChange”;
2)bubble(布尔值):标示事件是否应该冒泡;
3)cancelable(布尔值):标示事件是否可以取消;
4)detail(对象):任意值,保存在event对象的detail属性中;
具体的示例代码如下
<body>
<div id="div1">监听自定义事件</div>
<button id="btn">单击</button>
<script>
var customeEvent;
//在立即执行函数中创建自定义事件
(function () {
//判断浏览器是否支持DOM3事件处理程序,如果条件成立表示支持,固定写法
if (document.implementation.hasFeature("CustomEvents", "3.0")) {
var user = { userName: "zhangsan" };
customeEvent = document.createEvent("CustomEvent"); //创建自定义事件
customeEvent.initCustomEvent("myEvent", true, false, user);