所有的编程语言都有数据类型的概念。
在JavaScript
中,数据类型可以分为基本数据类型和引用数据类型。其中基本数据类型包括Undefined
,Null
,Boolean
,Number
,String
5种类型。在ES6
中新增了一种基本的数据类型Symbol
.
引用类型有Object
,Function
,Array
,Date
等。
问题:两种类型有什么区别?
1、存储位置不同
区别 | 基本数据类型 | 引用数据类型 |
---|---|---|
存储位置 | 栈(stack) | 堆(heap) |
占据空间 | 小,大小固定 | 大,大小不固定 |
问题:哪些场景中会出现
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)
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)相同点
第一:Undefined
和Null
两种数据类型都只有一个字面值,分别是undefined
和null
.
第二:Undefined
和Null
类型在转换为Boolean
类型的值时,都会转换为false
.
第三:在需要将两者转换成对象的时候,都会抛出一个TypeError
的异常。
第四:Undefined
类型派生自Null
类型,所以在非严格相等的比较下,两者是相等的。如下面
(2)不同点
第一:null
是JavaScript
的关键字,而undefined
是JavaScript
的一个全局变量,也就是挂载在window
对象上的一个变量,并不是关键字。
第二:在使用typeof
运算符进行检测时,Undefined
类型的值会返回undefined
.而Null
类型的值返回为object
第三:在需要进行字符串类型的转换时,null
会转换成字符串null
,而undefined
会转换字符串undefined
.
第四:在进行数值类型的转换时,undefined
会转换为NaN
,无法参与计算,而null
会转换为0
,可以参与计算。
第五:建议:无论在什么情况下都没有必要将一个变量显示的赋值为undefined
。如果需要定义某个变量来保存将来要使用的对象,应该将其初始化为null
.
Boolean
类型(布尔类型)的字面量只有两个,分别是true
和false
,它们是区分大小写的。
第一:String
类型转换为Boolean
类型
空字符都会转换成false
,而任何非空字符串都会转换为true
第二:Number
类型转换为Boolean
类型
0
和NaN
都会转换为false
.而除了0
和NaN
以外都会转换true
.
第三:Object
类型转换Boolean
类型
如果object
为null
时,会转换为false
,如果object
不为null
,则都会转换成true
.
第四:Function
类型转换Boolean
类型
任何Function
类型都会转换为true
第五:Null
类型转换为Boolean
类型,我们知道Null
类型只有一个null
值,会转换为false
.
第六:Undefined
类型转换Boolean
类型,我们知道Undefined
类型只有一个undefined
值,会转换为false
.
一共有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()
函数在解析时会依赖于出入的第二个参数来做值的转换。
总结:
isNaN( )
函数与Number.isNaN( )
函数的区别如下:
第一:isNaN( )
函数在判断是否为NaN
时,需要进行数据类型转换,只有在无法转换为数字时才会返回true
第二:Number.isNaN( )
函数在判断是否为NaN
时,只需要判断传入的值是否为NaN
,并不会进行数据类型转换。
在JavaScript
中的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( )
方式生成的字符串是字符串对象。
基本字符串在比较的时候,只需要比较字符串的值即可,而在比较字符串对象时,比较的是对象所在的地址。
我们来看一下常见的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的值hg
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"));
在JavaScript
中的运算符包括:算术运算符,关系运算符,等于运算符,位运算符(与、或、非)等
在JavaScript
中等于分为双等()比较,和三等于(=)比较。
(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 两个不同对象,地址不相同
相比于三等于运算符,双等于运算符在进行相等比较的时候,要复杂一点。因为它不区分数据类型,而且会做隐式类型的转换
。
双等于在进行比较的时候要注意的点:
如果比较的值类型不相同,则会按照下面的规则进行转换后再进行比较
(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
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
进行计算。
在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
引用类型有Object
,Function
,Array
,Date
,Math
等。
引用类型与基本数据类型的区别:
(1)引用数据类型的实例需要通过new
关键字创建。
(2)将引用数据类型赋值给变量,实际上赋值的是内存地址
(3)引用数据类型的比较是对内存地址的比较,而基本数据类型的比较是对值的比较。
我们创建的每一个函数都有一个 prototype
属性,这个属性是一个指针,指向一个对象。这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法,简单来说,该函数实例化的所有对象的__proto__
的属性指向这个对象,它是该函数所有实例化对象的原型。
下面我们通过一个案例来看一个简单的原型链过程。初步代码如下
var A=function(){ }
var a=new A( );
通过a
实例沿着原型链第一次的追溯,__proto__
属性指向A()
构造函数的原型对象。
a.__proto__===A.prototype ![请添加图片描述](https://img-blog.csdnimg.cn/ff9f832966284dc0bb8a512489dacf5c.png)
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
具体的图如下所示:
上面的图其实并不完整,因为漏掉了Object
.所以完整的图如下
关于原型链的特点,主要有两个
第一个特点:由于原型链的存在,属性查找的过程不再只是查找自身的原型对象,而是会沿着整个原型链一直向上,直到追溯到Object.prototype
.也就是说,当js
引擎在查找对象的属性时,先查找对象本身是否存在该属性,如果不存在,会在原型链上查找,直到Object.prototype
.如果Object.prototype
上也找不到该属性,则返回undefined
,如果期间在对象本身找到了或者是某个原型对象上找到了该属性,则会返回对应的结果。
第二个特点:由于属性查找会经历整个原型链,因此查找的链路越长,对性能的影响越大。
Array
类型中提供了丰富的函数用于对数组进行处理,例如,过滤,去重,遍历等等操作。
instanceof
运算符instanceof
运算符用于通过查找原型链来检查某个变量是否为某个类型数据的实例,使用instanceof
运算符可以判断一个变量是数组还是对象。
判断一个变量是否是数组还是对象,其实就是判断变量的构造函数是Array
类型还是Object
类型。
因为一个对象的实例都是通过构造函数创建的。
var a = [1, 2, 3];
console.log(a.__proto__.constructor === Array);
console.log(a.__proto__.constructor === Object); // false
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
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
对数组中的数据进行过滤,我们使用比较多的是filter
方法。
对数组中的元素做累加的处理,可以通过reduce
函数来完成。
reduce
函数最主要的作用就是做累加的操作,该函数接收一个函数作为累加器,将数组中的每个元素从左到右依次执行累加器,返回最终的处理结果。
reduce
函数的语法如下:
arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
关于查询出数组中的最大值与最小值的实现方式有很多种,下面我们来看一下具体的实现。
第一:通过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));
数组遍历是我们针对数组最频繁的操作。下面我们看一下常见的数组的遍历方式。
这时最基本的实现方式
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( )
函数 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
函数。
数组去重是指当数组中出现重复的元素的时候,通过一定的方式,将重复的元素去掉。
// 数组去重
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"]));
<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
构造函数来完成函数的定义。
首先来看一下函数的声明。
函数声明是直接通过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 ')
其中的参数,除了最后一个参数是要执行的函数体,其它的参数都是函数的形参。
函数声明与函数表达式虽然是两种定义函数的方式,但是两者之间还是有区别的。
第一点就是:函数名称
// 函数声明,函数名称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;
};
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);
第一:进行参数个数的判断。
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));
第一:构造函数的函数名的第一字母通常会大写。
第二:在构造函数的函数体内可以使用this
关键字,表示创生成的对象实例。
第三:在使用构造函数的时候,必须与new
操作符配合使用。
第四:构造函数的执行过程与普通函数也是不一样的。
在JavaScript
中,一个变量的定义与调用都是在一个固定的范围内的,这个范围我们称之为作用域。
作用域可以分为全局的作用域,局部作用域(函数作用域)和块级作用域
我们在查找userName
这个变量的时候,现在函数的作用域中进行查找,没有找到,再去全局作用域中查找。你会注意到,这是一个往外层查找的过程,即顺着一条链条从下往上查找变量。这个链条,我们就称之为作用域链。
所谓变量提升,是将变量的声明提升到函数顶部的位置,也就是将变量声明提升到变量所在的作用域的顶端,而变量的赋值并不会被提升。
不仅通过var
定义的变量会出现提升的情况,使用函数声明方式定义的函数也会出现提升。
在正常的情况下,如果定义了一个函数,就会产生一个函数作用域,在函数体中的局部变量会在这个函数的作用域中使用。
一旦函数执行完毕后,函数所占用的空间就会被回收,存在于函数体中的局部变量同样也会被回收,回收后将不能被访问。
如果我们期望在函数执行完毕以后,函数中的局部变量仍然可以被访问到,应该怎样实现呢?
这里我们可以通过闭包来实现。
关于闭包的官方概念:一个拥有许多变量和绑定了这些变量执行上下文环境的表达式,通常是一个函数。
简单的理解就是:闭包就是能够读取其它函数内部变量的函数。由于在JavaScript
语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。
所以,本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
闭包有两个比较显著的特点:
第一:函数拥有的外部变量的引用,在函数返回时,该变量仍然处于活跃状态。
第二:闭包作为一个函数返回时,其执行上下文环境不会销毁,仍然处于执行上下文环境中。
在JavaScript
中存在一种内部函数,即函数声明和函数表达式可以位于另一个函数的函数体内,在内部函数中可以访问外部函数声明的变量,当这个内部函数在包含它们外部函数之外被调用时,就会形成闭包。
应用缓存
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>
闭包的优点:
第一:保护函数内变量的安全,实现封装,防止变量流入其它环境发生命名冲突,造成环境污染。
第二:在适当的时候,可以在内存中维护变量并缓存,提高执行效率
闭包的缺点:
消耗内存:通常来说,函数的活动对象会随着执行上下文环境一起被销毁,但是由于闭包引用的是外部函数的活动对象,因此这个活动对象无法被销毁,所以说,闭包比一般的函数需要消耗更多的内存。
我们知道,当我们创建一个构造函数的实例的时候,需要通过new
操作符来完成创建,当创建完成后,函数体中的this
指向了这个实例。
第七:this
指向call()
函数,apply()
函数,bind()
函数调用后重新绑定的对象。
我们知道通过call()
函数,apply()
函数,bind()
函数可以改变函数执行的主体,如果函数中存在this
关键字,则this
指向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));
}
字面量方式创建对象
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);
通过构造函数创建对象
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(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();
通过上面的代码可以看出,我们将所有的内容写在了构造函数中,并且在构造函数中通过判断只初始化一次原型,而且只在第一次生成实例的时候进行原型的设置。这种方式创建的对象与构造函数和原型混合模式创建的对象功能上是相同的。
基本数据类型不管是浅拷贝还是深拷贝都是对值的本身的拷贝。对拷贝后值的修改不会影响到原始的值。
对于引用数据类型进行浅拷贝,拷贝后的值的修改会影响到原始的值,如果执行的是深拷贝,则拷贝的对象和原始对象之间相互独立,互不影响。
浅拷贝:如果一个对象中的属性是基本数据类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址,也就是拷贝后的内容与原始内容指向了同一个内存地址,这样拷贝后的值的修改会影响到原始的值。
深拷贝:如果一个对象中的属性是基本数据类型,拷贝的也是基本类型的值,如果属性是引用类型,就将其从内存中完整的拷贝一份出来,并且会在堆内存中开辟出一个新的区域存来进行存放,而且拷贝的对象和原始对象之间相互独立,互不影响。
下面我们先来看一下浅拷贝的内容
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()
可以将字符串反序列为一个对象
getElementById()
:通过id
来查找对应的元素。
getElementsByClassName()
:通过类名来查找对应的元素,返回的是一个HTMLCollection
对象。
getElementsByName()
:通过元素的name
属性查找对应的元素,返回的是NodeList
对象,它是一个类似于数组的结构。
getElementsByTagName()
: 通过标签的名称来查找对应的元素,返回的是HTMLCollection
对象。
querySelector
:该选择器返回的是在基准元素下,选择器匹配到的元素集合中的第一个元素。该选择器的参数接收的是一个css
选择
querySelectorAll
选择器与querySelector
选择器的区别是:querySelectAll
选择器会获取到基准元素下匹配到所有子元素的集合。返回的是一个NodeList
集合。
最后,总结一下HTMLCollection
对象与NodeList
对象的相同点与不同点
相同点:
第一:都是类似数组的结构,有length
属性,可以通过call()
函数或者是apply()
函数转换成数组,使用数组中的函数。
第二:都用item
函数,通过索引值获取相应的元素。
第三:都是实时的,当在DOM
树上添加元素或者是删除元素,都会立即反应到HTMLCollection
对象和NodeList
对象上。
不同点:
第一:HTMLCollection
对象中,有namedItem()
函数,而NodeList
对象中没有.
第二:NodeList
对象中存储的是元素节点的集合,包括元素,以及节点,例如text
文本节点,而HTMLCollection
对象中只包含了元素的集合。
添加节点appendChild
删除节点removeChild
删除文本框的id
属性removeAttribute
修改元素节点container.replaceChild(newDiv, div1); //用新的div替换旧的div,完成节点的修改操作。
修改属性节点修改属性的节点,我们可以通过
setAttribute()函数来完成,如果想获取属性节点可以通过
getAttribute()函数来完成。
修改文本节点文本节点的修改,可以通过
innerHTML属性来完成。
Dom
操作非常消耗性能,应该尽量避免频繁的操作DOM
.
导致浏览器重绘,重新渲染,比较消耗cpu
资源,比较消耗性能。
提升性能的方案:
第一:对DOM
查询操作进行缓存
第二:将频繁操作修改为一次性操作
这里的特定顺序是怎样的顺序呢?
第一种:事件传递的顺序是先触发最外层的元素,然后依次向内传播,这样的传递顺序我们称之为事件的捕获阶段。
第二种:事件传递的顺序是先触发最内层的元素,然后依次向外进行传播,这样的传递顺序我们称之为事件冒泡阶段。
首先就是事件的捕获阶段然后是事件的目标阶段,目标阶段指的就是事件已经到达目标元素。最后是事件的冒泡阶段
阻止事件的冒泡需要使用:
event.stopPropagation()
函数
与stopPropagation()
函数相对的还有一个stopImmediatePropagation
函数,它们两者之间有什么区别呢?
stopPropagation()
:函数会阻止事件冒泡,其它事件处理程序仍然可以调用
stopImmediatePropagation
函数不仅可以阻止事件冒泡,也会阻止其它事件处理程序的调用。
事件冒泡的一个应用就是事件代理,也叫做事件委托
事件委托:利用事件冒泡的特性,将本应该注册在子元素上的处理事件注册在父元素上。
在前面的课程中,我们也提到过对DOM
的操作是比较消耗性能的,这是因为它会带来浏览器的重绘与重排。
在讲解什么是重排与重绘之前,先来说一下浏览器渲染HTML
的过程。
浏览器渲染HTML
的过程大体上可以分为4步
第一:HTML
代码被HTML
解析器解析成对应的DOM
树,CSS
代码被CSS
解析器解析成对应的样式规则集。
第二:DOM
树与CSS
解析完成后,附加在一起形成一个渲染树
第三:节点信息的计算,即根据渲染树计算出每个节点的几何信息(宽,高等信息)
第四:渲染绘制,根据计算完成的节点信息绘制整个页面。
而我们所要讲解的重排与重绘就发生在第三步和第四步中。
回流
:对DOM元素的修改引发DOM元素的几何尺寸发生变化(比如修改元素的宽高,隐藏元素 等)时,浏览器需要重新计算元素的几何属性(其他元素的几何位置也会因此受到影响),然后再 将计算的结果绘制出来。这个过程就是回流(重排)
重绘
:当我们对DOM的修改导致了样式的变化(比如修改了颜色或者背景颜色)却并未影响其几 何属性,浏览器不需要重新计算元素的几何属性,直接为该元素绘制新的样式(跳过了重排的环节)。
当网页生成的时候,浏览器至少会渲染一次。在用户访问的过程中还会不断重新渲染。重新渲染会导致 回流或者重绘。回流必定会引发重绘,但是重绘未必引发回流。不断的重绘和回流会影响页面的性能。回流的成本也远高于重绘。
常见引发回流和重绘的操作
:
•会使任何元素的几何属性发生变化的操作(如元素的位置和尺寸),都会触发回流。
•添加或删除可见的DOM元素。
•元素尺寸的改变:边距、边框、宽高。
•内容变化,比如用户在input中输入文字。
•浏览器窗口尺寸的改变——resize事件。
•计算 offsetWidth 和 offsetHeight 属性。
常见引起重绘的操作:color,background,border-style。
减少回流与重绘:
•避免逐个修改节点样式,尽量一次修改:使用DocumentFragment将需要多次修改的DOM元素缓 存之后,最后一次性append到真是的DOM中。
•使用 transform做形变和位移可以减少。
•reflow CSS选择符避免节点层级过多。
•避免多次读取某些属性。
性能优化
浏览器的重排与重绘是比较消耗性能的,所以我们应该尽量减少重排与重绘的操作,这也是优化网页性能的一种方式。
常见的方法如下:
第一:将样式属性值的修改合并为一次。
第二:将需要多次重排的元素,position
属性设为absolute
或fixed
,这样此元素就脱离了文档流,它的变化不会影响到其他元素。例如有动画效果的元素就最好设置为绝对定位。
第三:在对多节点操作的时候,可以现在内测中完成,然后在添加到文档中。
第四:由于display
属性为none
的元素不在渲染树中,对隐藏的元素操作不会引发其他元素的重排。如果要对一个元素进行复杂的操作时,可以先隐藏它,操作完成后再显示。这样只在隐藏和显示时触发两次重排。
第五: 尽量减少table
布局,随便修改一个单元格的高度或宽度都会让整个表格进行重排,性能非常差。
第六:在对多个同级元素做事件绑定的时候,推荐使用事件委托机制来处理。
第七:文档片段createDocumentFragment
的使用,关于这块内容,在前面的课程中已经使用过。使用批量插入元素, n 例如:向页面的ul
元素中添加100
个li
元素,
在这里告诉大家一个最简单的方法: 有一段代码是用大括号包裹起来的,那么大括号里面就是一个块级作用域
ES5 只有全局作用域和函数作用域,没有块级作用域,这样就会带来一些问题,
第一:内层变量可能会覆盖外层变量
第二: 用来计数的循环变量成为了全局变量
let不像var那样会发生“变量提升”现象。所以,变量一定要在声明后使用,否则会出错。
关于这一点,ES6明确规定,如果在区域中存在let命令,那么在这个区域中通过let命令所声明的变量从一开始就生成了一个封闭的作用域,只要在声明变量前使用,就会出错。
所以说,所谓的“暂时性死区”指的就是,在代码块内,使用let命令声明变量之前,该变量都是不可用的。
let 不允许在相同的作用域内重复声明一个变量,如果使用var声明变量是没有这个限制的。
const用来声明常量,常量指的就是一旦声明,其值是不能被修改的。这一点与变量是不一样的,而变量指的是在程序运行中,是可以改变的量。
const命令的作用域与let命令相同:只在声明的块级作用域内有效
**在箭头函数中是没有this的,如果在箭头函数中使用了this,那么实际上使用的是外层代码块的this. 箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this
**或者通俗的理解:找出定义箭头函数的上下文(即包含箭头函数最近的函数或者是对象),那么上下文所处的父上下文即为this.
Object.assign( )
方法用来源对象的所有可枚举的属性复制到目标对象。该方法至少需要两个对象作为参数,第一个参数是目标对象,后面的参数都是源对象。只要有一个参数不是对象,就会抛出异常。
示例代码如下:
let target = {
a: 1,
b: 2
};
let source = {
c: 3,
d: 4
};
Object.assign(target, source);
console.log(target);
最终的结果:将source对象中的属性拷贝到target对象上。
通过Object.assign( )
方法,实现的拷贝只拷贝了属性的值,属于浅拷贝。
1、如果目标对象与源对象有同名属性,那么后面的属性会覆盖前面的属性。
2、不可枚举的属性不会被复制。
那么就能够从根本上防止属性名称的冲突问题。这也就是ES6引入Symbol的原因。
Symbol是一种数据类型,是JavaScript语言的第7种数据类型,前6种分别是:undefined,null,布尔值,字符串,数值和对象。
在开发中经常使用Ajax发送请求,那么就会出现如下的情况:
$.ajax(url, success() {
$.ajax(url2, success() {
$.ajax(url3, success() {
})
})
})
以上的代码反映了,在一个Ajax的回调中,又去发送了另外一个Ajax请求,依次类推,导致了多个回调函数的嵌套,导致代码不够直观并且难以维护,这就是常说的回调地狱。
同步模式指的就是代码中的任务依次执行。后一个任务必须等待前一个任务结束后,才能执行。程序的执行顺序与我们代码的编写顺序是完全一致的。
异步模式对应的API
是不会等待这个任务的结束才开始下一个任务,对于耗时操作,开启过后就立即往后执行下一个任务。
耗时任务的后续逻辑一般会通过回调函数的方式定义(例如ajax回调函数)。
所谓的Promise就是一个对象,而Promise对象代表的是一个异步任务,也就是需要很长时间去执行的任务。也就是通过Promise对象,可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数问题,也就是回调地狱的问题。
链式调用
变量提升(hoisting),是负责解析执行代码的 JavaScript 引擎的工作方式产生的一个特性。
JS引擎在运行一份代码的时候,会按照下面的步骤进行工作:
首先,对代码进行预解析,并获取声明的所有变量
然后,将这些变量的声明语句统一放到代码的最前面
最后,开始一行一行运行代码
我们通过一段代码来解释这个运行过程:
console.log(a)
var a = 1
function b() {
console.log(a)
}
b() // 1
上⾯这段代码的实际执⾏顺序为:
var a = 1
分解为两个部分:变量声明语句 var a = undefined
和变量赋值语句 a = 1
var a = undefined
放到代码的最前面,而 a = 1
保留在原地也就是说经过了转换,代码就变成了:
var a = undefined
console.log(a) // undefined
a = 1
function b() {
console.log(a)
}
b() // 1
变量的这一转换过程,就被称为变量的声明提升。
而这是不规范, 不合理的, 我们用的 let 就没有这个变量提升的问题
基本数据类型和复杂数据类型的数据在传递时,会有不同的表现。
基本类型:是值传递!
基本类型的传递方式比较简单,是按照 值传递
进行的。
let a = 1
function test(x) {
x = 10 // 并不会改变实参的值
console.log(x)
}
test(a) // 10
console.log(a) // 1
复杂类型: 传递的是地址! (变量中存的就是地址)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YqVfYnUT-1659968893234)(images/image-20210305165413588.png)]
来看下面的代码:
let a = {
count: 1
}
function test(x) {
x.count = 10
console.log(x)
}
test(a) // { count: 10 }
console.log(a) // { count: 10 }
从运行结果来看,函数内改变了参数对象内的 count
后,外部的实参对象 a
的内容也跟着改变了,所以传递的是地址。
思考题:
let a = {
count: 1
};
function test(x) {
x = { count: 20 };
console.log(x);
}
test(a); // { count: 20 }
console.log(a); // { count: 1 }
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VkHoXsAR-1659968893235)(images/image-20210305165848781.png)]
我们会发现外部的实参对象 a
并没有因为在函数内对形参的重新赋值而被改变!
因为当我们直接为这个形参变量重新赋值时,其实只是让形参变量指向了别的堆内存地址,而外部实参变量的指向还是不变的。
下图展示的是复杂类型参数传递后的状态:
下图展示的是重新为形参赋值后的状态:
JS中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收。
正因为垃圾回收器的存在,许多人认为JS不用太关心内存管理的问题,
但如果不了解JS的内存管理机制,我们同样非常容易成内存泄漏(内存无法被回收)的情况。
JS环境中分配的内存, 一般有如下生命周期:
内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存
内存使用:即读写内存,也就是使用变量、函数等
内存回收:使用完毕,由垃圾回收自动回收不再使用的内存
全局变量一般不会回收, 一般局部变量的的值, 不用了, 会被自动回收掉
内存分配:
// 为变量分配内存
let i = 11
let s = "ifcode"
// 为对象分配内存
let person = {
age: 22,
name: 'ifcode'
}
// 为函数分配内存
function sum(a, b) {
return a + b;
}
所谓垃圾回收, 核心思想就是如何判断内存是否已经不再会被使用了, 如果是, 就视为垃圾, 释放掉
下面介绍两种常见的浏览器垃圾回收算法: 引用计数 和 标记清除法
IE采用的引用计数算法, 定义“内存不再使用”的标准很简单,就是看一个对象是否有指向它的引用。
如果没有任何变量指向它了,说明该对象已经不再需要了。
// 创建一个对象person, person指向一块内存空间, 该内存空间的引用数 +1
let person = {
age: 22,
name: 'ifcode'
}
let p = person // 两个变量指向一块内存空间, 该内存空间的引用数为 2
person = 1 // 原来的person对象被赋值为1,对象内存空间的引用数-1,
// 但因为p指向原person对象,还剩一个对于对象空间的引用, 所以对象它不会被回收
p = null // 原person对象已经没有引用,会被回收
由上面可以看出,引用计数算法是个简单有效的算法。
但它却存在一个致命的问题:循环引用。
如果两个对象相互引用,尽管他们已不再使用,垃圾回收器不会进行回收,导致内存泄露。
function cycle() {
let o1 = {}
let o2 = {}
o1.a = o2
o2.a = o1
return "Cycle reference!"
}
cycle()
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HyZkVqPR-1659968893235)(images/image-20210305172448582.png)]
现代的浏览器已经不再使用引用计数算法了。
现代浏览器通用的大多是基于标记清除算法的某些改进算法,总体思想都是一致的。
标记清除法:
标记清除算法将“不再使用的对象”定义为“无法达到的对象”。
简单来说,就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。
凡是能从根部到达的对象,都是还需要使用的。那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。
从这个概念可以看出,无法触及的对象包含了没有引用的对象这个概念(没有任何引用的对象也是无法触及的对象)。
根据这个概念,上面的例子可以正确被垃圾回收处理了。
参考文章:JavaScript内存管理
JavaScript 在执⾏过程中会创建一个个的可执⾏上下⽂。 (每个函数执行都会创建这么一个可执行上下文)
每个可执⾏上下⽂的词法环境中包含了对外部词法环境的引⽤,可通过该引⽤来获取外部词法环境中的变量和声明等。
这些引⽤串联起来,⼀直指向全局的词法环境,形成一个链式结构,被称为作⽤域链。
简而言之: 函数内部 可以访问到 函数外部作用域的变量, 而外部函数还可以访问到全局作用域的变量,
这样的变量作用域访问的链式结构, 被称之为作用域链
let num = 1
function fn () {
let a = 100
function inner () {
console.log(a)
console.log(num)
}
inner()
}
fn()
下图为由多个可执行上下文组成的调用栈:
全局可执行上下文
全局可执行上下文
之上有多个 函数可执行上下文
全局可执行上下文
时它指向 null
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A8wJJex8-1659968893235)(images/image-20210306093300970.png)]
js全局有全局可执行上下文, 每个函数调用时, 有着函数的可执行上下文, 会入js调用栈
每个可执行上下文, 都有者对于外部上下文词法作用域的引用, 外部上下文也有着对于再外部的上下文词法作用域的引用
=> 就形成了作用域链
这个问题想考察的主要有两个方面:
什么是闭包?
MDN的官方解释:
闭包是函数和声明该函数的词法环境的组合
更通俗一点的解释是:
内层函数, 引用外层函数上的变量, 就可以形成闭包
需求: 定义一个计数器方法, 每次执行一次函数, 就调用一次进行计数
let count = 0
function fn () {
count++
console.log('fn函数被调用了' + count + '次')
}
fn()
这样不好! count 定义成了全局变量, 太容易被别人修改了, 我们可以利用闭包解决
闭包实例:
function fn () {
let count = 0
function add () {
count++
console.log('fn函数被调用了' + count + '次')
}
return add
}
const addFn = fn()
addFn()
addFn()
addFn()
闭包的主要作用是什么?
在实际开发中,闭包最大的作用就是用来 变量私有。
下面再来看一个简单示例:
function Person() {
// 以 let 声明一个局部变量,而不是 this.name
// this.name = 'zs' => p.name
let name = 'hm_programmer' // 数据私有
this.getName = function(){
return name
}
this.setName = function(value){
name = value
}
}
// new:
// 1. 创建一个新的对象
// 2. 让构造函数的this指向这个新对象
// 3. 执行构造函数
// 4. 返回实例
const p = new Person()
console.log(p.getName()) // hm_programmer
p.setName('Tom')
console.log(p.getName()) // Tom
p.name // 访问不到 name 变量:undefined
在此示例中,变量 name
只能通过 Person 的实例方法进行访问,外部不能直接通过实例进行访问,形成了一个私有变量。
在if语句、逻辑语句、数学运算逻辑、== 等情况下都可能出现隐式类型转换。
下表展示了一系列原始值,通过隐式转换成数字、字符串、布尔类型后所得到的值:
坑: 判断时, 尽量不要用 = =
, 要用 = = =
( 两个等号判断, 如果类型不同, 默认会进行隐式类型转换再比较)
要讲清楚这个问题,主要着重这几个方面:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FpWIAVGw-1659968893236)(images/image-20210306104516852.png)]
原型对象
在 JavaScript 中,除去一部分内建函数,绝大多数的函数都会包含有一个叫做 prototype
的属性,指向原型对象,
基于构造函数创建出来的实例, 都可以共享访问原型对象的属性。
例如我们的 hasOwnProperty
, toString
⽅法等其实是 Obejct 原型对象的方法,它可以被任何对象当做⾃⼰的⽅法来使⽤。
hasOwnProperty
用于判断, 某个属性, 是不是自己的 (还是原型链上的)
来看一段代码:
let person = {
name: "Tom",
age: 18,
job: "student"
}
console.log(person.hasOwnProperty("name")) // true
console.log(person.hasOwnProperty("hasOwnProperty")) // false
console.log(Object.prototype.hasOwnProperty("hasOwnProperty")) // true
可以看到,hasOwnProperty
并不是 person
对象的属性,但是 person
却能调用它。
那么 person
对象是如何找到 Object 原型中的 hasOwnProperty
的呢?这就要靠原型链的能力了。
需求: 简单绘制原型三角关系图!
原型链
在 JavaScript 中,每个对象中都有一个 __proto__
属性,这个属性指向了当前对象的构造函数的原型。
对象可以通过自身的 __proto__
属性与它的构造函数的原型对象连接起来,
而因为它的原型对象也有 __proto__
,因此这样就串联形成一个链式结构,也就是我们称为的原型链。
为什么要学习继承 ?
写的构造函数, 定义了一个类型 (人类), 万一项目非常大, 又有了细化的多个类型 (老师, 工人, 学生)
学习继承, 可以让多个构造函数之间建立关联, 便于管理和复用
什么是继承 ?
继承: 从别人那里, 继承东西过来 (财产, 房产)
代码层面的继承: 继承一些属性构造的过程和方法
原型继承: 通过改造原型链, 利用原型链的语法, 实现继承方法!
分析需求:
人类, 属性: name, age
学生, 属性: name, age, className
工人, 属性: name, age, companyName
无论学生, 还是工人, => 都是人类, 所以人类原型上有的方法, 他们都应该要有
// 1. 定义Person构造函数
function Person (name, age) {
this.name = name
this.age = age
}
Person.prototype.say = function () {
console.log('人类会说话')
}
// 2. 定义Student构造函数
function Student (name, age, className) {
this.name = name
this.age = age
this.className = className
}
// 3. 原型继承: 利用原型链, 继承于父级构造函数, 继承原型上的方法
// 语法: 子构造函数.prototype = new 父构造函数()
Student.prototype = new Person()
Student.prototype.study = function() {
console.log('学生在学习')
}
let stu = new Student('张三', 18, '80期')
stu.say()
console.log(stu)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RCT7DdKN-1659968893236)(images/image-20210306111112493.png)]
组合继承有时候也叫伪经典继承,指的是将原型链 和 借用构造函数 call 技术组合到一块,
从而发挥二者之长的一种继承模式,其背后的思路: 是使用原型链实现对原型属性和方法的继承 (主要是方法),
而通过借用构造函数来实现对实例属性构造的继承。这样既通过在原型上定义方法实现了函数复用,又能保证每个实例都有它的自己的属性。
// 1. 定义Person构造函数
function Person (name, age) {
this.name = name
this.age = age
}
Person.prototype.say = function () {
console.log('人类会说话')
}
// 2. 定义Student构造函数
function Student (name, age, className) {
Person.call(this, name, age) // 实现构造属性的继承
this.className = className
}
// 3. 原型继承: 利用原型链, 继承于父级构造函数, 继承原型上的方法
// 语法: 子构造函数.prototype = new 父构造函数()
Student.prototype = new Person()
Student.prototype.study = function() {
console.log('学生在学习')
}
let stu = new Student('张三', 18, '80期')
stu.say()
console.log(stu)
// 方法通过 原型继承
// 属性通过 父构造函数的.call(this, name, age)
student实例上有 name age, 而原型 __proto__
上不需要再有这些属性, 所以利用 Object.create 改装下
Object.create(参数对象),
__proto__
会指向传入的参数对象// 1. 定义Person构造函数
function Person (name, age) {
this.name = name
this.age = age
}
Person.prototype.say = function () {
console.log('人类会说话')
}
// 2. 定义Student构造函数
function Student (name, age, className) {
Person.call(this, name, age)
this.className = className
}
// 3. 原型继承: 利用原型链, 继承于父级构造函数, 继承原型上的方法
// 语法: 子构造函数.prototype = new 父构造函数()
Student.prototype = Object.create(Person.prototype)
Student.prototype.study = function() {
console.log('学生在学习')
}
let stu = new Student('张三', 18, '80期')
stu.say()
console.log(stu)
// 总结:
// Object.create() 以参数的对象, 作为新建对象的__proto__属性的值, 返回新建的对象
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i0GyprZh-1659968893236)(images/image-20210306114638139.png)]
// 继承关键字 => extends
class Person {
constructor (name, age) {
this.name = name
this.age = age
}
jump () {
console.log('会跳')
}
}
class Teacher extends Person {
constructor (name, age, lesson) {
super(name, age) // extends 中, 必须调用 super(), 会触发执行父类的构造函数
this.lesson = lesson
console.log('构造函数执行了')
}
sayHello () {
console.log('会打招呼')
}
}
let teacher1 = new Teacher('zs', 18, '体育')
console.log(teacher1)
方法一:使用 toString
方法
function isArray(arg) {
return Object.prototype.toString.call(arg) === '[object Array]'
}
let arr = [1,2,3]
isArray(arr) // true
方法二:使用 ES6 新增的 Array.isArray
方法
let arr = [1,2,3]
Array.isArray(arr) // true
this
是一个在运行时才进行绑定的引用,在不同的情况下它可能会被绑定不同的对象。
默认绑定 (指向window的情况) (函数调用模式 fn() )
默认情况下,this
会被绑定到全局对象上,比如在浏览器环境中就为window
对象,在node.js环境下为global
对象。
如下代码展示了这种绑定关系:
message = "Hello";
function test () {
console.log(this.message);
}
test() // "Hello"
隐式绑定 (谁调用, this指向谁) (方法调用模式 obj.fn() )
如果函数的调用是从对象上发起时,则该函数中的 this
会被自动隐式绑定为对象:
function test() {
console.log(this.message);
}
let obj = {
message: "hello,world",
test: test
}
obj.test() // "hello,world"
显式绑定 (又叫做硬绑定) (上下文调用模式, 想让this指向谁, this就指向谁)
硬绑定 => call apply bind
可以显式的进行绑定:
function test() {
console.log(this.message);
}
let obj1 = {
message: "你好世界123"
}
let obj2 = {
message: "你好世界456"
}
test.bind(obj1)() // "你好世界123"
test.bind(obj2)() // "你好世界456"
new 绑定 (构造函数模式)
另外,在使用 new
创建对象时也会进行 this
绑定
当使用 new
调用构造函数时,会创建一个新的对象并将该对象绑定到构造函数的 this
上:
function Greeting(message) {
this.message = message;
}
var obj = new Greeting("hello,world")
obj.message // "hello,world"
小测试:
let obj = {
a: {
fn: function () {
console.log(this)
},
b: 10
}
}
obj.a.fn()
let temp = obj.a.fn;
temp()
// -------------------------------------------------------------
function Person(theName, theAge){
this.name = theName
this.age = theAge
}
Person.prototype.sayHello = function(){ // 定义函数
console.log(this)
}
let per = new Person("小黑", 18)
per.sayHello()
箭头函数不同于传统函数,它其实没有属于⾃⼰的 this
,
它所谓的 this
是, 捕获其外层 上下⽂的 this
值作为⾃⼰的 this
值。
并且由于箭头函数没有属于⾃⼰的 this
,它是不能被 new
调⽤的。
我们可以通过 Babel 转换前后的代码来更清晰的理解箭头函数:
// 转换前的 ES6 代码
const obj = {
test() {
return () => {
console.log(this === obj)
}
}
}
// 转换后的 ES5 代码
var obj = {
test: function getArrow() {
var that = this
return function () {
console.log(that === obj)
}
}
}
这里我们看到,箭头函数中的 this
就是它上层上下文函数中的 this
。
promise的三个状态: pending(默认) fulfilled(成功) rejected(失败)
Promise.reject()
new Promise((resolve, reject) => {
reject()
})
Promise.resolve()
new Promise((resolve, reject) => {
resolve()
})
Promise.all([promise1, promise2, promise3]) 等待原则, 是在所有promise都完成后执行, 可以用于处理一些并发的任务
// 后面的.then中配置的函数, 是在前面的所有promise都完成后执行, 可以用于处理一些并发的任务
Promise.all([promise1, promise2, promise3]).then((values) => {
// values 是一个数组, 会收集前面promise的结果 values[0] => promise1的成功的结果
})
Promise.race([promise1, promise2, promise3]) 赛跑, 竞速原则, 只要三个promise中有一个满足条件, 就会执行.then(用的较少)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N9Nfzvxp-1659968893237)(images/image-20210306144638905.png)]
小例题:
console.log(1)
setTimeout(function() {
console.log(2)
}, 0)
console.log(3)
宏任务: 主线程代码, setTimeout 等属于宏任务, 上一个宏任务执行完, 才会考虑执行下一个宏任务
微任务: promise .then .catch的需要执行的内容, 属于微任务, 满足条件的微任务, 会被添加到当前宏任务的最后去执行
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5YHJfzAH-1659968893237)(images/image-20201208040306978.png)]
事件循环队列 eventLoop
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ERoJeTfB-1659968893237)(images/image-20201208040235602.png)]
例题1:
console.log(1)
setTimeout(function() {
console.log(2) // 宏任务
}, 0)
const p = new Promise((resolve, reject) => {
resolve(1000)
})
p.then(data => {
console.log(data) // 微任务
})
console.log(3)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YVmsRKRi-1659968893238)(images/image-20210306151137688.png)]
例题2:
async function fn () {
console.log(111)
}
fn()
console.log(222)
例题3:
async function fn () {
const res = await 2
console.log(res)
}
fn()
console.log(222)
例题4:
async function fn () {
console.log('嘿嘿')
const res = await fn2()
console.log(res) // 微任务
}
async function fn2 () {
console.log('gaga')
}
fn()
console.log(222)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aEmabKxh-1659968893238)(images/image-20210306152010989.png)]
考察点: async 函数只有从 await 往下才是异步的开始
ES7 标准中新增的 async
函数,从目前的内部实现来说其实就是 Generator
函数的语法糖。
它基于 Promise,并与所有现存的基于Promise 的 API 兼容。
async 关键字
async
关键字用于声明⼀个异步函数(如 async function asyncTask1() {...}
)
async
会⾃动将常规函数转换成 Promise,返回值也是⼀个 Promise 对象
async
函数内部可以使⽤ await
await 关键字
await
用于等待异步的功能执⾏完毕 var result = await someAsyncCall()
await
放置在 Promise 调⽤之前,会强制async函数中其他代码等待,直到 Promise 完成并返回结果
await
只能与 Promise ⼀起使⽤
await
只能在 async
函数内部使⽤
引用类型, 进行赋值时, 赋值的是地址
浅拷贝
let obj = {
name: 'zs',
age: 18
}
let obj2 = {
...obj
}
深拷贝
let obj = {
name: 'zs',
age: 18,
car: {
brand: '宝马',
price: 100
}
}
let obj2 = JSON.parse(JSON.stringify(obj))
console.log(obj2)
当然递归也能解决, 只是比较麻烦~
…
其他方案, 可以参考一些博客
事件流
⼜称为事件传播,是⻚⾯中接收事件的顺序。DOM2级事件规定的事件流包括了3个阶段:
[外链图片转存中…(img-zikPdAtK-1659968995442)]
如上图所示,事件流的触发顺序是:
事件冒泡(Event Bubbling)
事件开始由最具体的元素(⽂档中嵌套层次最深的那个节点)接收到后,开始逐级向上传播到较为不具体的节点。
<html>
<head>
<title>Documenttitle>
head>
<body>
<button>按钮button>
body>
html>
如果点击了上面页面代码中的 按钮,那么该
click
点击事件会沿着 DOM 树向上逐级传播,在途经的每个节点上都会发生,具体顺序如下:
事件捕获(Event Capturing)
事件开始由较为不具体的节点接收后,然后开始逐级向下传播到最具体的元素上。
事件捕获的最大作用在于:事件在到达预定⽬标之前就可以捕获到它。
如果仍以上面那段 HTML 代码为例,当点击按钮后,在事件捕获的过程中,document 对象会首先接收到这个 click
事件,然后再沿着 DOM 树依次向下,直到 。具体顺序如下:
事件委托,就是利用了事件冒泡的机制,在较上层位置的元素上添加一个事件监听函数,
来管理该元素及其所有子孙元素上的某一类的所有事件。
示例
<ul id="list">
<li>111li>
<li>222li>
<li>333li>
<li>444li>
<li>555li>
ul>
<script type="text/javascript">
// ⽗元素
var list = document.getElementById('list');
// 为⽗元素绑定事件,委托管理它的所有⼦元素li的点击事件
list.onclick = function (event) {
var currentTarget = event.target;
if (currentTarget.tagName.toLowerCase() === 'li') {
alert(currentTarget.innerText)
}
}
script>
适用场景:在绑定大量事件的时候,可以选择事件委托
优点