变量的数据类型
JS中一共有六种数据类型
基本数据类型(值类型):String 字符串、Number 数值、Boolean 布尔值、Null 空值、Undefined 未定义。
引用数据类型(引用类型):Object 对象。
注意:内置对象 Function、Array、Date、RegExp、Error等都是属于 Object 类型。也就是说,除了那五种基本数据类型之外,其他的,都称之为 Object类型。
数据类型之间最大的区别:
基本数据类型:参数赋值的时候,传数值。
引用数据类型:参数赋值的时候,传地址(修改的同一片内存空间)。
栈内存和堆内存
我们首先记住一句话:JS中,所有的变量都是保存在栈内存中的。
然后来看看下面的区别。
基本数据类型:
基本数据类型的值,直接保存在栈内存中。值与值之间是独立存在,修改一个变量不会影响其他的变量。
引用数据类型:
对象是保存到堆内存中的。每创建一个新的对象,就会在堆内存中开辟出一个新的空间;而变量保存了对象的内存地址(对象的引用),保存在栈内存当中。如果两个变量保存了同一个对象的引用,当一个通过一个变量修改属性时,另一个也会受到影响。
转义字符
在字符串中我们可以使用\
作为转义字符,当表示一些特殊符号时可以使用\
进行转义。
\"
表示"
双引号\'
表示'
单引号\\
表示\
\r
表示回车\n
表示换行。n 的意思是 newline。\t
表示缩进。t 的意思是 tab。\b
表示空格。b 的意思是 blank。
字符串的不可变性
字符串里面的值不可被改变。虽然看上去可以改变内容,但其实是地址变了,内存中新开辟了一个内存空间。
代码举例:
var str = 'hello';
str = 'qianguyihao';
比如上面的代码,当重新给变量 str 赋值时,常量hello
不会被修改,依然保存在内存中;str 会改为指向qianguyihao
。
模板字面量(模板字符串)
ES6中引入了模板字面量,让我们省去了字符串拼接的烦恼。
有了 ES6 语法,字符串拼接可以这样写:
// 在模板字符串中插入变量
var name = 'qianguyihao';
var age = '26';
console.log('我是' + name + ',age:' + age); //传统写法
console.log(`我是${name},age:${age}`); //ES6 写法。注意语法格式
// 在模板字面量中插入表达式
const a = 5;
const b = 10;
console.log(`this is ${a + b} and
not ${2 * a + b}.`);
// 在模板字面量中插入函数返回值
function getName() {
return 'qianguyihao';
}
console.log(`www.${getName()}.com`); // 打印结果:www.qianguyihao.com
// 模板字面量支持嵌套使用
const nameList = ['千古壹号', '许嵩', '解忧少帅'];
function myTemplate() {
// join('') 的意思是,把数组里的内容合并成一个字符串
return `<ul>
${nameList
.map((item) => `<li>${item}</li>`)
.join('')}
</ul>`;
}
document.body.innerHTML = myTemplate();
数值型:Number
数值范围
由于内存的限制,ECMAScript 并不能保存世界上所有的数值。
最大值:
Number.MAX_VALUE
,这个值为: 1.7976931348623157e+308最小值:
Number.MIN_VALUE
,这个值为: 5e-324
如果使用 Number 表示的变量超过了最大值,则会返回Infinity。
无穷大(正无穷):Infinity
无穷小(负无穷):-Infinity
注意:typeof Infinity
的返回结果是number。
注意:typeof NaN
的返回结果是 number。
Undefined和任何数值计算的结果为 NaN。NaN 与任何值都不相等,包括 NaN 本身。
浮点数的运算
运算精度问题
在JS中,整数的运算基本可以保证精确;但是小数的运算,可能会得到一个不精确的结果。所以,千万不要使用JS进行对精确度要求比较高的运算。
var a = 0.1 + 0.2;
console.log(a); //打印结果:0.30000000000000004
处理数学运算的精度问题
如果只是一些简单的精度问题,可以使用 toFixd()
方法进行小数的截取。备注:关于 toFixed()
方法。
Null:空对象
null 专门用来定义一个空对象(例如:let a = null
)。
如果你想定义一个变量用来保存引用类型,但是还没想好放什么内容,这个时候,可以在初始化时将其设置为 null。
比如:
let myObj = null;
cosole.log(typeof myObj); // 打印结果:object
补充:
Null 类型的值只有一个,就是 null。比如
let a = null
。使用 typeof 检查一个 null 值时,会返回 object。
undefined
case1:变量已声明,未赋值时
声明了一个变量,但没有赋值,此时它的值就是 undefined
。举例:
let name;
console.log(name); // 打印结果:undefined
console.log(typeof name); // 打印结果:undefined
补充:
Undefined 类型的值只有一个,就是 undefind。比如
let a = undefined
。使用 typeof 检查一个 undefined 值时,会返回 undefined。
case2:变量未声明(未定义)时
如果你从未声明一个变量,就去使用它,则会报错(这个大家都知道);此时,如果用 typeof
检查这个变量时,会返回 undefined
。举例:
console.log(typeof a); // undefined
console.log(a); // 打印结果:Uncaught ReferenceError: a is not defined
case3:函数无返回值时
如果一个函数没有返回值,那么,这个函数的返回值就是 undefined。
或者,也可以这样理解:在定义一个函数时,如果末尾没有 return 语句,那么,其实就是 return undefined
。
举例:
function foo() {}
console.log(foo()); // 打印结果:undefined
case4:调用函数时,未传参
调用函数时,如果没有传参,那么,这个参数的值就是 undefined。
举例:
function foo(name) {
console.log(name);
}
foo(); // 调用函数时,未传参。执行函数后的打印结果:undefined
实际开发中,如果调用函数时没有传参,我们可以给形参设置一个默认值:
function foo(name) {
name = name || 'qianguyihao';
}
foo();
等学习了 ES6 之后,上方代码也可以这样写:
function foo(name = 'qianguyihao') {}
foo();
null和undefined区别
null 和 undefined 有很大的相似性。看看 null == undefined
的结果为 true
也更加能说明这点。
但是 null === undefined
的结果是 false。它们虽然相似,但还是有区别的,其中一个区别是,和数字运算时:
10 + null 结果为 10。
10 + undefined 结果为 NaN。
规律总结:
任何数据类型和 undefined 运算都是 NaN;
任何值和 null 运算,null 可看做 0 运算。
其他类型转换为 Boolean
其他的数据类型都可以转换为 Boolean类型。情况如下:
情况一:数字 –> 布尔。除了 0 和 NaN,其余的都是 true。也就是说,
Boolean(NaN)
的结果是 false。情况二:字符串 —> 布尔。除了空串,其余的都是 true。全是空格的字符串,转换结果也是 true。字符串
'0'
的转换结果也是 true。情况三:null 和 undefined 都会转换为 false。
情况四:引用数据类型会转换为 true。注意,空数组
[]
和空对象{}
,转换结果也是 true,这个一点,很多人都不知道。
PS:转换为 Boolean 的这几种情况,很重要,开发中会经常用到。
知识补充:其他进制的数字
16 进制的数字,以
0x
开头8 进制的数字,以
0
开头2 进制的数字,
0b
开头(不是所有的浏览器都支持:chrome 和火狐支持,IE 不支持)
比如070
这个字符串,如果我调用 parseInt()转成数字时,有些浏览器会当成 8 进制解析,有些会当成 10 进制解析。
所以,比较建议的做法是:可以在 parseInt()中传递第二个参数,来指定当前数字的进制。例如:
var a = "070";
a = parseInt(a, 8); //将 070 当成八进制来看待,转换结果为十进制。
console.log(a); // 打印结果:56。这个地方要好好理解。
隐式类型转换
重点:隐式类型转换,内部调用的都是显式类型的方法。下面来详细介绍。
isNaN() 函数
语法:
isNaN(参数);
解释:判断指定的参数是否为 NaN(非数字类型),返回结果为 Boolean 类型。也就是说:任何不能被转换为数值的参数,都会让这个函数返回 true。
执行过程:
(1)先调用Number(参数)
函数;
(2)然后将Number(参数)
的返回结果和NaN
进行比较。
代码举例:
console.log(isNaN('123')); // 返回结果:false。
console.log(isNaN('abc')); // 返回结果:true。因为 Number('abc') 的返回结果是 NaN
console.log(isNaN(null)); // 返回结果:false
console.log(isNaN(undefined)); // 返回结果:true
console.log(isNaN(NaN)); // 返回结果:true
任何值和NaN做运算的结果都是NaN。
非布尔值的与或运算【重要】
之所以重要,是因为在实际开发中,我们经常用这种代码做容错处理或者兜底处理。
非布尔值进行与或运算时,会先将其转换为布尔值,然后再运算,但返回结果是原值。比如说:
var result = 5 && 6; // 运算过程:true && true;
console.log('result:' + result); // 打印结果:6(也就是说最后面的那个值。)
上方代码可以看到,虽然运算过程为布尔值的运算,但返回结果是原值。
那么,返回结果是哪个原值呢?我们来看一下。
与运算的返回结果:(以多个非布尔值的运算为例)
如果第一个值为false,则执行第一条语句,并直接返回第一个值;不会再往后执行。
如果第一个值为true,则继续执行第二条语句,并返回第二个值(如果所有的值都为true,则返回的是最后一个值)。
或运算的返回结果:(以多个非布尔值的运算为例)
如果第一个值为true,则执行第一条语句,并直接返回第一个值;不会再往后执行。
如果第一个值为false,则继续执行第二条语句,并返回第二个值((如果所有的值都为false,则返回的是最后一个值)。
实际开发中,我们经常是这样来做「容错处理」的:
当前端成功调用一个接口后,返回的数据为 result 对象。这个时候,我们用变量 a 来接收 result 里的图片资源。通常的写法是这样的:
if (result.resultCode == 0) {
var a = result && result.data && result.data.imgUrl || 'http://img.smyhvae.com/20160401_01.jpg';
}
上方代码的意思是,获取返回结果中的result.data.imgUrl
这个图片资源;如果返回结果中没有 result.data.imgUrl
这个字段,就用 http://img.smyhvae.com/20160401_01.jpg
作为兜底图片。这种写法,在实际开发中经常用到。
短路运算的妙用【重要】
下方举例中的写法技巧,在实际开发中,经常用到。这种写法,是一种很好的「容错、容灾、降级」方案,需要多看几遍。
1、JS中的&&
属于短路的与:
如果第一个值为false,则不会执行后面的内容。
如果第一个值为 true,则继续执行第二条语句,并返回第二个值。
举例:
const a1 = 'qianguyihao';
//第一个值为true,会继续执行后面的内容
a1 && alert('看 a1 出不出来'); // 可以弹出 alert 框
const a2 = undefined;
//第一个值为false,不会继续执行后面的内容
a2 && alert('看 a2 出不出来'); // 不会弹出 alert 框
2、JS中的||
属于短路的或:
如果第一个值为true,则不会执行后面的内容。
如果第一个值为 false,则继续执行第二条语句,并返回第二个值。
举例:
const result; // 请求接口时,后台返回的内容
let errorMsg = ''; // 前端的文案提示
if (result && result.retCode != 0) {
// 接口返回异常码时
errorMsg = result.msg || '活动太火爆,请稍后再试'; // 文案提示信息,优先用 接口返回的msg字段,其次用 '活动太火爆,请稍后再试' 这个文案兜底。
}
if (!result) {
// 接口挂掉时
errorMsg = '网络异常,请稍后再试';
}
非数值的比较
(1)对于非数值进行比较时,会将其转换为数字然后再比较。
举例如下:
console.log(1 > true); //false
console.log(1 >= true); //true
console.log(1 > "0"); //true
//console.log(10 > null); //true
//任何值和NaN做任何比较都是false
console.log(10 <= "hello"); //false
console.log(true > false); //true
(2)特殊情况:如果符号两侧的值都是字符串时,不会将其转换为数字进行比较。比较两个字符串时,比较的是字符串的Unicode编码。【非常重要,这里是个大坑,很容易踩到】
比较字符编码时,是一位一位进行比较。如果两位一样,则比较下一位。
比如说,当你尝试去比较"123"
和"56"
这两个字符串时,你会发现,字符串”56”竟然比字符串”123”要大(因为 5 比 1 大)。也就是说,下面这样代码的打印结果,其实是true:(这个我们一定要注意,在日常开发中,很容易忽视)
// 比较两个字符串时,比较的是字符串的字符编码,所以可能会得到不可预期的结果
console.log("56" > "123"); // true
因此:当我们在比较两个字符串型的数字时,一定一定要先转型再比较大小,比如 parseInt()
。
(3)任何值和NaN做任何比较都是false。
==
符号的强调
注意==
这个符号,它是判断是否等于,而不是赋值。
(1)==
这个符号,还可以验证字符串是否相同。例如:
console.log("我爱你中国" == "我爱你中国"); // 输出结果为true
(2)==
这个符号并不严谨,会做隐式转换,将不同的数据类型,转为相同类型进行比较(大部分情况下,都是转换为数字)。例如:
console.log("6" == 6); // 打印结果:true。这里的字符串"6"会先转换为数字6,然后再进行比较
console.log(true == "1"); // 打印结果:true
console.log(0 == -0); // 打印结果:true
console.log(null == 0); // 打印结果:false
(3)undefined 衍生自 null,所以这两个值做相等判断时,会返回true。
console.log(undefined == null); //打印结果:true。
(4)NaN不和任何值相等,包括他本身。
console.log(NaN == NaN); //false
console.log(NaN === NaN); //false
问题:那如果我想判断 b的值是否为NaN,该怎么办呢?
答案:可以通过isNaN()函数来判断一个值是否是NaN。举例:
console.log(isNaN(b));
如上方代码所示,如果 b 为 NaN,则返回true;否则返回false。
===
全等符号的强调
全等在比较时,不会做类型转换。如果要保证绝对等于(完全等于),我们就要用三个等号===
。例如:
console.log("6" === 6); //false
console.log(6 === 6); //true
上述内容分析出:
==
两个等号,不严谨,”6”和6是true。===
三个等号,严谨,”6”和6是false。
另外还有:==
的反面是!=
,===
的反面是!==
。例如:
console.log(3 != 8); //true
console.log(3 != "3"); //false,因为3=="3"是true,所以反过来就是false。
console.log(3 !== "3"); //true,应为3==="3"是false,所以反过来是true。
Unicode 编码
这一段中,我们来讲引申的内容:Unicode编码的使用。
各位同学可以先在网上查一下“Unicode 编码表”。
1、在字符串中可以使用转义字符输入Unicode编码。格式如下:
\u四位编码
举例如下:
console.log("\u2600"); // 这里的 2600 采用的是16进制
console.log("\u2602"); // 这里的 2602 采用的是16进制。
打印结果:
2、我们还可以在 HTML 网页中使用Unicode编码。格式如下:
&#四位编码;
PS:我们知道,Unicode编码采用的是16进制,但是,这里的编码需要使用10进制。
举例如下:
<h1 style="font-size: 100px;">⚄</h1>
打印结果:
switch 语句
switch 和 case 后面的值
switch 后面的括号里可以是表达式或者值, 通常是一个变量(通常做法是:先把表达式或者值存放到变量中)。
JS 是属于弱类型语言,case 后面的值1
、值2
可以是 'a'
、6
、true
等任意数据类型的值,也可以是表达式。注意,在这里,字符串'6'
和 数字 6
是不一样的。
switch 语句的结束条件【非常重要】
情况 a:遇到 break 就结束,而不是遇到 default 就结束。(因为 break 在此处的作用就是退出 switch 语句)
情况 b:执行到程序的末尾就结束。
我们来看下面的两个例子就明白了。
case 穿透
switch 语句中的break
可以省略,但一般不建议(对于新手而言)。否则结果可能不是你想要的,会出现一个现象:case 穿透。
当然,如果你能利用好 case 穿透,会让代码些得十分优雅。
举例 1:(case 穿透的情况)
var num = 4;
//switch判断语句
switch (num) {
case 1:
console.log('星期一');
break;
case 2:
console.log('星期二');
break;
case 3:
console.log('星期三');
break;
case 4:
console.log('星期四');
//break;
case 5:
console.log('星期五');
//break;
case 6:
console.log('星期六');
break;
case 7:
console.log('星期日');
break;
default:
console.log('你输入的数据有误');
break;
}
上方代码的运行结果,可能会令你感到意外:
星期四
星期五
星期六
上方代码的解释:因为在 case 4 和 case 5 中都没有 break,那语句走到 case 6 的 break 才会停止。
举例 2:
//switch判断语句
var number = 5;
switch (number) {
default:
console.log('我是defaul语句');
// break;
case 2:
console.log('第二个呵呵:' + number);
//break;
case 3:
console.log('第三个呵呵:' + number);
break;
case 4:
console.log('第四个呵呵:' + number);
break;
}
上方代码的运行结果,你也许会意外:
我是defaul语句
第二个呵呵:5
第三个呵呵:5
上方代码的解释:代码走到 default 时,因为没有遇到 break,所以会继续往下走,直到遇见 break 或者走到程序的末尾。 从这个例子可以看出:switch 语句的结束与 default 的顺序无关。
switch 语句的实战举例:替换 if 语句
我们实战开发中,经常需要根据接口的返回码 retCode ,来让前端做不同的展示。
这种场景是业务开发中经常出现的,请一定要掌握。然而,很多人估计会这么写:
写法 1(不推荐。这种写法太挫了)
let retCode = 1003; // 返回码 retCode 的值可能有很多种情况
if (retCode == 0) {
alert('接口联调成功');
} else if (retCode == 101) {
alert('活动不存在');
} else if (retCode == 103) {
alert('活动未开始');
} else if (retCode == 104) {
alert('活动已结束');
} else if (retCode == 1001) {
alert('参数错误');
} else if (retCode == 1002) {
alert('接口频率限制');
} else if (retCode == 1003) {
alert('未登录');
} else if (retCode == 1004) {
alert('(风控用户)提示 活动太火爆啦~军万马都在挤,请稍后再试');
} else {
// 其他异常返回码
alert('系统君失联了,请稍候再试');
}
如果你是按照上面的 if else
的方式来写各种条件判断,说明你的代码水平太初级了,会被人喷的,千万不要这么写。这种写法,容易导致嵌套太深,可读性很差。
那要怎么改进呢?继续往下看。
写法 2(推荐。通过 return 的方式,将上面的写法进行改进)
let retCode = 1003; // 返回码 retCode 的值可能有很多种情况
handleRetCode(retCode);
// 方法:根据接口不同的返回码,处理前端不同的显示状态
function handleRetCode(retCode) {
if (retCode == 0) {
alert('接口联调成功');
return;
}
if (retCode == 101) {
alert('活动不存在');
return;
}
if (retCode == 103) {
alert('活动未开始');
return;
}
if (retCode == 104) {
alert('活动已结束');
return;
}
if (retCode == 1001) {
alert('参数错误');
return;
}
if (retCode == 1002) {
alert('接口频率限制');
return;
}
if (retCode == 1003) {
alert('未登录');
return;
}
if (retCode == 1004) {
alert('(风控用户)提示 活动太火爆啦~军万马都在挤,请稍后再试');
return;
}
// 其他异常返回码
alert('系统君失联了,请稍候再试');
return;
}
上面的写法 2,是比较推荐的写法:直接通过 return 的方式,让 function 里的代码不再继续往下走,这就达到目的了。对了,因为要用到 return ,所以需要单独封装到一个 function 里面。
如果你以后看到有前端小白采用的是写法 1,请一定要把写法 2传授给他:不需要那么多的 if else,直接用 return 返回就行了。
写法 3(推荐。将 if else 改为 switch)
let retCode = 1003; // 返回码 retCode 的值可能有很多种情况
switch (retCode) {
case 0:
alert('接口联调成功');
break;
case 101:
alert('活动不存在');
break;
case 103:
alert('活动未开始');
break;
case 104:
alert('活动已结束');
break;
case 1001:
alert('参数错误');
break;
case 1002:
alert('接口频率限制');
break;
case 1003:
alert('未登录');
break;
case 1004:
alert('(风控用户)提示 活动太火爆啦~军万马都在挤,请稍后再试');
break;
// 其他异常返回码
default:
alert('系统君失联了,请稍候再试');
break;
}
在实战开发中,方式 3 是非常推荐的写法,甚至比方式 2 还要好。我们尽量不要写太多的 if 语句,避免代码嵌套过深。
switch 语句的优雅写法:适时地去掉 break
我们先来看看下面这段代码:(不推荐)
let day = 2;
switch (day) {
case 1:
console.log('work');
break;
case 2:
console.log('work');
break;
case 3:
console.log('work');
break;
case 4:
console.log('work');
break;
case 5:
console.log('work');
break;
case 6:
console.log('relax');
break;
case 7:
console.log('relax');
break;
default:
break;
}
上面的代码,咋一看,好像没啥毛病。但你有没有发现,重复代码太多了?
实战开发中,凡是有重复的地方,我们都必须要想办法简化。写代码就是在不断重构的过程。
上面的代码,可以改进如下:(推荐,非常优雅)
let day = 2;
switch (day) {
case 1:
case 2:
case 3:
case 4:
case 5:
console.log('work');
break; // 在这里放一个 break
case 6:
case 7:
console.log('relax');
break; // 在这里放一个 break
default:
break;
}
}
你没看错,就是上面的这种写法,能达到同样的效果,非常优雅。
小白可能认为这样的写法可读性不强,所以说他是小白。我可以明确告诉你,改进后的这种写法,才是最优雅的、最简洁、可读性最好的。
while 循环和 do…while 循环的区别
这两个语句的功能类似,不同的是:
- while 是先判断后执行,而 do…while 是先执行后判断。
也就是说,do…while 可以保证循环体至少执行一次,而 while 不能。
while 循环举例
题目:假如投资的年利率为 5%,试求从 1000 块增长到 5000 块,需要花费多少年?
代码实现:
<!DOCTYPE html>
<html lang="">
<head>
<meta />
<meta />
<meta />
<title>Document</title>
</head>
<body>
<script>
/*
* 假如投资的年利率为5%,试求从1000块增长到5000块,需要花费多少年
*
* 1000 1000*1.05
* 1050 1050*1.05
*/
//定义一个变量,表示当前的钱数
var money = 1000;
//定义一个计数器
var count = 0;
//定义一个while循环来计算每年的钱数
while (money < 5000) {
money *= 1.05;
//使count自增
count++;
}
console.log(money);
console.log('一共需要' + count + '年');
</script>
</body>
</html>
打印结果:
5003.18854203379
一共需要33年
另外,你也可以自己算一下,假如投资的年利率为 5%,从 1000 块增长到 1 万块,需要花费 48 年:
10401.269646942128
一共需要48年
break 和 continue
这个知识点非常重要。
break
break 可以用来退出 switch 语句或退出整个循环语句(循环语句包括 for 循环、while 循环。不包括 if。if 里不能用 break 和 continue,否则会报错)。
break 会立即终止离它最近的那个循环语句。
可以为循环语句创建一个 label,来标识当前的循环(格式:label:循环语句)。使用 break 语句时,可以在 break 后跟着一个 label,这样 break 将会结束指定的循环,而不是最近的。
举例 1:通过 break 终止循环语句
for (var i = 0; i < 5; i++) {
console.log('i的值:' + i);
if (i == 2) {
break; // 注意,虽然在 if 里 使用了 break,但这里的 break 是服务于外面的 for 循环。
}
}
打印结果:
i的值:0
i的值:1
i的值:2
举例 2:label 的使用
outer: for (var i = 0; i < 5; i++) {
console.log('外层循环 i 的值:' + i);
for (var j = 0; j < 5; j++) {
break outer; // 直接跳出outer所在的外层循环(这个outer是我自定义的label)
console.log('内层循环 j 的值:' + j);
}
}
打印结果:
外层循环 i 的值:0
continue
continue 可以用来跳过当次循环,继续下一次循环。
同样,continue 默认只会离他最近的循环起作用。
同样,如果需要跳过指定的当次循环,可以使用 label 标签。
举例:
for (var i = 0; i < 10; i++) {
if (i % 2 == 0) {
continue;
}
console.log('i的值:' + i);
}
打印结果:
i的值:1
i的值:3
i的值:5
i的值:7
i的值:9
对象传值和传址的区别
传值
代码举例:
let a = 1;
let b = a;// 将 b 赋值给 a
b = 2; // 修改 b 的值
上方代码中,当我修改 b 的值之后,a 的值并不会发生改变。这个大家都知道。我们继续往下看。
传址(一个经典的例子)
代码举例:
var obj1 = new Object();
obj1.name = "孙悟空";
var obj2 = obj1; // 将 obj1 的地址赋值给 obj2。从此, obj1 和 obj2 指向了同一个堆内存空间
//修改obj2的name属性
obj2.name = "猪八戒";
上面的代码中,当我修改 obj2 的name属性后,会发现,obj1 的 name 属性也会被修改。因为obj1和obj2指向的是堆内存中的同一个地址。
这个例子要尤其注意,实战开发中,很容易忽略。
对于引用类型的数据,赋值相当于地址拷贝,a、b指向了同一个堆内存地址。所以改了b,a也会变;本质上a、b就是一个东西。
如果你打算把引用类型 A 的值赋值给 B,让A和B相互不受影响的话,可以通过 Object.assign() 来复制对象。效果如下:
var obj1 = {name: '孙悟空'};
// 复制对象:把 obj1 赋值给 obj3。两者之间互不影响
var obj3 = Object.assign({}, obj1);
对象的分类
1.内置对象:
由ES标准中定义的对象,在任何的ES的实现中都可以使用
比如:Object、Math、Date、String、Array、Number、Boolean、Function等。
2.宿主对象:
由JS的运行环境提供的对象,目前来讲主要指由浏览器提供的对象。
比如 BOM DOM。比如
console
、document
。
3.自定义对象:
- 由开发人员自己创建的对象
通过 new 关键字创建出来的对象实例,都是属于对象类型,比如Object、Array、Date等。
JavaScript的内置对象:
内置对象 | 对象说明 |
---|---|
Arguments | 函数参数集合 |
Array | 数组 |
Boolean | 布尔对象 |
Math | 数学对象 |
Date | 日期时间 |
Error | 异常对象 |
Function | 函数构造器 |
Number | 数值对象 |
Object | 基础对象 |
RegExp | 正则表达式对象 |
String | 字符串对象 |
基本数据类型不能绑定属性和方法
属性和方法只能添加给对象,不能添加给基本数据类型。
1、基本数据类型:
注意,基本数据类型string
是无法绑定属性和方法的。比如说:
var str = 'qianguyihao';
str.aaa = 12;
console.log(typeof str); //打印结果为:string
console.log(str.aaa); //打印结果为:undefined
上方代码中,当我们尝试打印str.aaa
的时候,会发现打印结果为:undefined。也就是说,不能给 string
绑定属性和方法。
当然,我们可以打印 str.length、str.indexOf(“m”)等等。因为这两个方法的底层做了数据类型转换(临时将 string
字符串转换为 String
对象,然后再调用内置方法),也就是我们在上一段中讲到的包装类。
2、引用数据类型:
引用数据类型String
是可以绑定属性和方法的。如下:
var strObj = new String('smyhvae');
strObj.aaa = 123;
console.log(strObj);
console.log(typeof strObj); //打印结果:Object
console.log(strObj.aaa);
打印结果:
内置对象 Number 也有一些自带的方法,比如:
Number.MAX_VALUE;
Number.MIN_VALUE;
内置对象 Boolean 也有一些自带的方法,但是用的不多。
基本包装类型
介绍
我们都知道,js 中的数据类型包括以下几种。
基本数据类型:String、Number、Boolean、Null、Undefined
引用数据类型:Object
JS 为我们提供了三个基本包装类型:
String():将基本数据类型字符串,转换为 String 对象。
Number():将基本数据类型的数字,转换为 Number 对象。
Boolean():将基本数据类型的布尔值,转换为 Boolean 对象。
通过上面这这三个包装类,我们可以将基本数据类型的数据转换为对象。
代码举例:
let str1 = 'qianguyihao';
let str2 = new String('qianguyihao');
let num = new Number(3);
let bool = new Boolean(true);
console.log(typeof str1); // 打印结果:string
console.log(typeof str2); // 注意,打印结果:object
需要注意的是:我们在实际应用中一般不会使用基本数据类型的对象。如果使用基本数据类型的对象,在做一些比较时可能会带来一些不可预期的结果。
比如说:
var boo1 = new Boolean(true);
var boo2 = new Boolean(true);
console.log(boo1 === boo2); // 打印结果竟然是:false
再比如说:
var boo3 = new Boolean(false);
if (boo3) {
console.log('qianguyihao'); // 这行代码竟然执行了
}
显然,使用 typeof 去检查类型时,
基本包装类型的作用
当我们对一些基本数据类型的值去调用属性和方法时,浏览器会临时使用包装类将基本数据类型转换为引用数据类型,这样的话,基本数据类型就有了属性和方法,然后再调用对象的属性和方法;调用完以后,再将其转换为基本数据类型。
举例:
var str = 'qianguyihao';
console.log(str.length); // 打印结果:11
比如,上面的代码,执行顺序是这样的:
// 步骤(1):把简单数据类型 string 转换为 引用数据类型 String,保存到临时变量中
var temp = new String('qianguyihao');
// 步骤(2):把临时变量的值 赋值给 str
str = temp;
// 步骤(3):销毁临时变量
temp = null;
在底层,字符串以字符数组的形式保存
在底层,字符串是以字符数组的形式保存的。代码举例:
var str = 'smyhvae';
console.log(str.length); // 获取字符串的长度
console.log(str[2]); // 获取字符串中的第2个字符
上方代码中,smyhvae
这个字符串在底层是以["s", "m", "y", "h", "v", "a", "e"]
的形式保存的。因此,我们既可以获取字符串的长度,也可以获取指定索引 index 位置的单个字符。这很像数组中的操作。
再比如,String 对象的很多内置方法,也可以直接给字符串用。此时,也是临时将字符串转换为 String 对象,然后再调用内置方法。
数组
使用构造函数创建数组
语法:
let arr = new Array(参数);
let arr = Array(参数);
如果参数为空,则表示创建一个空数组;如果参数是一个数值时,表示数组的长度;如果有多个参数时,表示数组中的元素。
来举个例子:
// 方式一
var arr1 = [11, 12, 13];
// 方式二
var arr2 = new Array(); // 参数为空
var arr3 = new Array(4); // 参数为一个数值
var arr4 = new Array(15, 16, 17); // 参数为多个数值
console.log(typeof arr1); // 打印结果:object
console.log('arr1 = ' + JSON.stringify(arr1));
console.log('arr2 = ' + JSON.stringify(arr2));
console.log('arr3 = ' + JSON.stringify(arr3));
console.log('arr4 = ' + JSON.stringify(arr4));
打印结果:
object;
arr1 = [11, 12, 13];
arr2 = [];
arr3 = [null, null, null, null];
arr4 = [15, 16, 17];
从上方打印结果的第一行里,可以看出,数组的类型其实也是属于对象。
数组中的元素的类型
数组中可以存放任意类型的数据,例如字符串、数字、布尔值、对象等。
比如:
const arr = ['qianguyihao', 28, true, { name: 'qianguyihao' }];
我们甚至还可以存放多维数组(数组里面放数组)。比如:
const arr2 = [
[11, 12, 13],
[21, 22, 23],
];
获取数组中的元素
语法:
数组[索引];
如果读取不存在的索引(比如元素没那么多),系统不会报错,而是返回 undefined。
对于连续的数组,使用 length 可以获取到数组的长度(元素的个数);对于非连续的数组,使用 length 会获取到数组的最大的索引+1。因此,尽量不要创建非连续的数组。
修改数组的长度(修改 length)
如果修改的 length 大于原长度,则多出部分会空出来,置为 null。
如果修改的 length 小于原长度,则多出的元素会被删除,数组将从后面删除元素。
(特例:伪数组 arguments 的长度可以修改,但是不能修改里面的元素,后面单独讲。)
代码举例:
var arr1 = [11, 12, 13];
var arr2 = [21, 22, 23];
// 修改数组 arr1 的 length
arr1.length = 1;
console.log(JSON.stringify(arr1));
// 修改数组 arr2 的 length
arr2.length = 5;
console.log(JSON.stringify(arr2));
打印结果:
[11][(21, 22, 23, null, null)];
Array.from():将伪数组转换为真数组
语法:
array = Array.from(arrayLike);
作用:将伪数组或可遍历对象转换为真数组。
代码举例:
const name = 'qianguyihao';
console.log(Array.from(name)); // 打印结果是数组:["q","i","a","n","g","u","y","i","h","a","o"]
伪数组与真数组的区别
伪数组:包含 length 属性的对象或可迭代的对象。
另外,伪数组的原型链中没有 Array.prototype,而真数组的原型链中有 Array.prototype。因此伪数组没有数组的一般方法,比如 pop()、join() 等方法。
伪数组举例
<body>
<button>按钮1</button>
<button>按钮2</button>
<button>按钮3</button>
<script>
let btnArray = document.getElementsByTagName('button');
console.log(btnArray);
console.log(btnArray[0]);
</script>
</body>
上面的布局中,有三个 button 标签,我们通过getElementsByTagName
获取到的btnArray
实际上是伪数组,并不是真实的数组:
既然btnArray
是伪数组,它就不能使用数组的一般方法,否则会报错:
解决办法:采用Array.from
方法将btnArray
这个伪数组转换为真数组即可:
Array.from(btnArray);
然后就可以使用数组的一般方法了:
Array.of():创建数组
语法:
Array.of(value1, value2, value3);
作用:根据参数里的内容,创建数组。
举例:
const arr = Array.of(1, 'abc', true);
console.log(arr); // 打印结果是数组:[1, "abc", true]
补充:new Array()
和 Array.of()
的区别在于:当参数只有一个时,前者表示数组的长度,后者表示数组中的内容。
数组合并的另一种方式
我们可以使用...
这种展开语法,将两个数组进行合并。举例如下:
const arr1 = [1, 2, 3];
const result = ['a', 'b', 'c', ...arr1];
console.log(JSON.stringify(result)); // 打印结果:["a","b","c",1,2,3]
sort()方法
sort()方法需要好好理解。
sort()
:对数组的元素进行从小到大来排序(会改变原来的数组)。
sort()方法:无参时
如果在使用 sort() 方法时不带参,则默认按照Unicode 编码,从小到大进行排序。
举例 1:(当数组中的元素为字符串时)
let arr1 = ['e', 'b', 'd', 'a', 'f', 'c'];
let result = arr1.sort(); // 将数组 arr1 进行排序
console.log('arr1 =' + JSON.stringify(arr1));
console.log('result =' + JSON.stringify(result));
打印结果:
arr1 =["a","b","c","d","e","f"]
result =["a","b","c","d","e","f"]
从上方的打印结果中,我们可以看到,sort 方法会改变原数组,而且方法的返回值也是同样的结果。
举例 2:(当数组中的元素为数字时)
let arr2 = [5, 2, 11, 3, 4, 1];
let result = arr2.sort(); // 将数组 arr2 进行排序
console.log('arr2 =' + JSON.stringify(arr2));
console.log('result =' + JSON.stringify(result));
打印结果:
arr2 =[1,11,2,3,4,5]
result =[1,11,2,3,4,5]
上方的打印结果中,你会发现,使用 sort() 排序后,数字11
竟然在数字2
的前面。这是为啥呢?因为上面讲到了,sort()
方法是按照Unicode 编码进行排序的。
那如果我想让 arr2 里的数字,完全按照从小到大排序,怎么操作呢?继续往下看。
sort()方法:带参时,自定义排序规则
如果在 sort()方法中带参,我们就可以自定义排序规则。具体做法如下:
我们可以在 sort()添加一个回调函数,来指定排序规则。回调函数中需要定义两个形参,浏览器将会分别使用数组中的元素作为实参去调用回调函数。
浏览器根据回调函数的返回值来决定元素的排序:(重要)
如果返回一个大于 0 的值,则元素会交换位置
如果返回一个小于 0 的值,则元素位置不变
如果返回一个等于 0 的值,则认为两个元素相等,则不交换位置
如果只是看上面的文字,可能不太好理解,我们来看看下面的例子,你肯定就能明白。
举例:将数组中的数字按照从小到大排序
写法 1:
var arr = [5, 2, 11, 3, 4, 1];
// 自定义排序规则
var result = arr.sort(function (a, b) {
if (a > b) {
// 如果 a 大于 b,则交换 a 和 b 的位置
return 1;
} else if (a < b) {
// 如果 a 小于 b,则位置不变
return -1;
} else {
// 如果 a 等于 b,则位置不变
return 0;
}
});
console.log('arr =' + JSON.stringify(arr));
console.log('result =' + JSON.stringify(result));
打印结果:
arr = [1, 2, 3, 4, 5, 11];
result = [1, 2, 3, 4, 5, 11];
上方代码的写法太啰嗦了,其实也可以简化为如下写法:
写法 2:(冒泡排序)
let arr = [5, 2, 11, 3, 4, 1];
// 自定义排序规则
let result = arr.sort(function (a, b) {
return a - b; // 升序排列
// return b - a; // 降序排列
});
console.log('arr =' + JSON.stringify(arr));
console.log('result =' + JSON.stringify(result));
打印结果不变。
上方代码还可以写成 ES6 的形式,也就是将 function 改为箭头函数,其写法如下。
写法 3:(箭头函数)
let arr = [5, 2, 11, 3, 4, 1];
// 自定义排序规则
let result = arr.sort((a, b) => {
return a - b; // 升序排列
});
console.log('arr =' + JSON.stringify(arr));
console.log('result =' + JSON.stringify(result));
上方代码,因为函数体内只有一句话,所以可以去掉 return 语句,继续简化为如下写法。
写法 4:(推荐)
let arr = [5, 2, 11, 3, 4, 1];
// 自定义排序规则:升序排列
let result = arr.sort((a, b) => a - b);
console.log('arr =' + JSON.stringify(arr));
console.log('result =' + JSON.stringify(result));
上面的各种写法中,写法 4 是我们在实战开发中用得最多的。
为了确保代码的简洁优雅,接下来的代码中,凡是涉及到函数,我们将尽量采用 ES6 中的箭头函数来写。
sort 方法举例:将数组从小到大排序
将数组从小到大排序,这个例子很常见。但在实际开发中,总会有一些花样。
下面这段代码,在实际开发中,经常用到,一定要掌握。完整代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
let dataList = [
{
title: '品牌鞋子,高品质低价入手',
publishTime: 200,
},
{
title: '不是很贵,但是很暖',
publishTime: 100,
},
{
title: '无法拒绝的美食,跟我一起吃吃',
publishTime: 300,
},
];
console.log('qianguyihao 排序前的数组:' + JSON.stringify(dataList));
// 将dataList 数组,按照 publishTime 字段,从小到大排序。(会改变原数组)
dataList.sort((a, b) => parseInt(a.publishTime) - parseInt(b.publishTime));
console.log('qianguyihao 排序后的数组:' + JSON.stringify(dataList));
</script>
</body>
</html>
打印结果:
qianguyihao 排序前的数组:[
{"title":"品牌鞋子,高品质低价入手","publishTime":200},
{"title":"不是很贵,但是很暖","publishTime":100},
{"title":"无法拒绝的美食,跟我一起吃吃","publishTime":300}
]
qianguyihao 排序后的数组:[
{"title":"不是很贵,但是很暖","publishTime":100},
{"title":"品牌鞋子,高品质低价入手","publishTime":200},
{"title":"无法拒绝的美食,跟我一起吃吃","publishTime":300}
]
上方代码中,有人可能会问: publishTime 字段已经是 int 类型了,为啥在排序前还要做一次 parseInt() 转换?这是因为,这种数据,一般是后台接口返回给前端的,数据可能是 int 类型、也可能是字符串类型,所以还是统一先做一次 partInt() 比较保险。这是一种良好的工作习惯。
map()方法
语法:
arr.map(function (item, index, arr) {
return newItem;
});
解释:对数组中每一项运行回调函数,返回该函数的结果,组成的新数组(返回的是加工之后的新数组)。不会改变原数组。
作用:对数组中的每一项进行加工。
举例 1:(拷贝的过程中改变数组元素的值)
有一个已知的数组 arr1,我要求让 arr1 中的每个元素的值都加 10,这里就可以用到 map 方法。代码举例:
var arr1 = [1, 3, 6, 2, 5, 6];
var arr2 = arr1.map(function (item, index) {
return item + 10; //让arr1中的每个元素加10
});
console.log(arr2);
打印结果:
举例 2:【重要案例,实际开发中经常用到】
将 A 数组中某个属性的值,存储到 B 数组中。代码举例:
const arr1 = [
{ name: '千古壹号', age: '28' },
{ name: '许嵩', age: '32' },
];
// 将数组 arr1 中的 name 属性,存储到 数组 arr2 中
const arr2 = arr1.map((item) => item.name);
// 将数组 arr1 中的 name、age这两个属性,改一下“键”的名字,存储到 arr3中
const arr3 = arr1.map((item) => ({
myName: item.name,
myAge: item.age,
})); // 将数组 arr1 中的 name 属性,存储到 数组 arr2 中
console.log('arr1:' + JSON.stringify(arr1));
console.log('arr2:' + JSON.stringify(arr2));
console.log('arr3:' + JSON.stringify(arr3));
打印结果:
arr1:[{"name":"千古壹号","age":"28"},{"name":"许嵩","age":"32"}]
arr2:["千古壹号","许嵩"]
arr3:[{"myName":"千古壹号","myAge":"28"},{"myName":"许嵩","myAge":"32"}]
map 的应用场景,主要就是以上两种。
filter()
语法:
arr.filter(function (item, index, arr) {
return true;
});
解释:对数组中的每一项运行回调函数,该函数返回结果是 true 的项,将组成新的数组(返回值就是这个新的数组)。不会改变原数组。
作用:对数组进行过滤。
举例 1:找出数组 arr1 中大于 4 的元素,返回一个新的数组。代码如下:
let arr1 = [1, 3, 6, 2, 5, 6];
let arr2 = arr1.filter((item) => {
if (item > 4) {
return true; // 将arr1中大于4的元素返回,组成新的数组
}
return false;
});
console.log(JSON.stringify(arr1)); // 打印结果:[1,3,6,2,5,6]
console.log(JSON.stringify(arr2)); // 打印结果:[6,5,6]
上方代码更简洁的写法如下:
let arr1 = [1, 3, 6, 2, 5, 6];
let arr2 = arr1.filter((item) => item > 4); // 将arr1中大于4的元素返回,组成新的数组
console.log(JSON.stringify(arr1)); // 打印结果:[1,3,6,2,5,6]
console.log(JSON.stringify(arr2)); // 打印结果:[6,5,6]
举例 2:
获取数组 A 中指定类型的对象,放到数组 B 中。代码举例如下:
const arr1 = [
{ name: '许嵩', type: '一线' },
{ name: '周杰伦', type: '过气' },
{ name: '邓紫棋', type: '一线' },
];
const arr2 = arr1.filter((item) => item.type == '一线'); // 筛选出一线歌手
console.log(JSON.stringify(arr2));
打印结果:
[
{ name: '许嵩', type: '一线' },
{ name: '邓紫棋', type: '一线' },
];
reduce()方法
reduce() 语法
reduce 的发音:[rɪ’djuːs]。中文含义是减少,但这个方法跟“减少”没有任何关系。
reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。返回值是回调函数累计处理的结果。
语法:
arr.reduce(function (previousValue, currentValue, currentIndex, arr) {}, initialValue);
参数解释:
previousValue:必填,上一次调用回调函数时的返回值
currentValue:必填,当前正在处理的数组元素
currentIndex:选填,当前正在处理的数组元素下标
arr:选填,调用 reduce()方法的数组
initialValue:选填,可选的初始值(作为第一次调用回调函数时传给 previousValue 的值)
在以往的数组方法中,匿名的回调函数里是传三个参数:item、index、arr。但是在 reduce() 方法中,前面多传了一个参数previousValue
,这个参数的意思是上一次调用回调函数时的返回值。第一次执行回调函数时,previousValue 没有值怎么办?可以用 initialValue 参数传给它。
备注:绝大多数人在一开始接触 reduce() 的时候会很懵逼,但是没关系,有事没事多看几遍,自然就掌握了。如果能熟练使用 reduce() 的用法,将能替代很多其他的数组方法,并逐渐走上进阶之路,领先于他人。
为了方便理解 reduce(),我们先来看看下面的简单代码,过渡一下:
let arr1 = [1, 2, 3, 4, 5, 6];
arr1.reduce((prev, item) => {
console.log(prev);
console.log(item);
console.log('------');
return 88;
}, 0);
打印结果:
0
1
------
88
2
------
88
3
------
88
4
------
88
5
------
88
6
------
上面的代码中,由于return
的是固定值,所以 prev 打印的也是固定值(只有初始值是 0,剩下的遍历中,都是打印 88)。
现在来升级一下,实际开发中,prev 的值往往是动态变化的,这便是 reduce()的精妙之处。我们来看几个例子就明白了。
函数
1、所有的函数,都是 Fuction
的“实例”(或者说是“实例对象”)。函数本质上都是通过 new Function 得到的。
2、函数既然是实例对象,那么,函数也属于“对象”。还可以通过如下特征,来佐证函数属于对象:
(1)我们直接打印某一个函数,比如 console.log(fun2)
,发现它的里面有__proto__
。(这个是属于原型的知识,后续再讲)
(2)我们还可以打印 console.log(fun2 instanceof Object)
,发现打印结果为 true
。这说明 fun2 函数就是属于 Object。
函数的调用
方式1:普通函数的调用
函数调用的语法:
函数名();
或者:
函数名.call();
代码举例:
function fn1() {
console.log('我是函数体里面的内容1');
}
function fn2() {
console.log('我是函数体里面的内容2');
}
fn1(); // 调用函数
fn2.call(); // 调用函数
方式2:通过对象的方法来调用
var obj = {
a: 'qianguyihao',
fn2: function() {
console.log('千古壹号,永不止步!');
},
};
obj.fn2(); // 调用函数
如果一个函数是作为一个对象的属性保存,那么,我们称这个函数是这个对象的方法。
函数也可以成为对象的属性。如果一个函数是作为一个对象的属性保存,那么,我们称这个函数是这个对象的方法。
调用这个函数就说调用对象的方法(method)。函数和方法,有什么本质的区别吗?它只是名称上的区别,并没有其他的区别。
方式3:立即执行函数
代码举例:
(function() {
console.log('我是立即执行函数');
})();
立即执行函数在定义后,会自动调用。
PS:关于立即执行函数,本文的后续内容里有讲到,可以往下面翻。
上面讲到的这三种方式,是用得最多的。接下来讲到的三种方式,暂时看不懂也没关系,可以等学完其他的知识点,再回过头来看。
方式4:通过构造函数来调用
代码举例:
function Fun3() {
console.log('千古壹号,永不止步~');
}
new Fun3();
这种方式用得不多。
方式5:绑定事件函数
代码举例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="btn">我是按钮,请点击我</div>
<script>
var btn = document.getElementById('btn');
//2.绑定事件
btn.onclick = function() {
console.log('点击按钮后,要做的事情');
};
</script>
</body>
</html>
这里涉及到DOM操作和事件的知识点,后续再讲。
方式6:定时器函数
代码举例:(每间隔一秒,将 数字 加1)
let num = 1;
setInterval(function () {
num ++;
console.log(num);
}, 1000);
这里涉及到定时器的知识点。
函数的参数:形参和实参
实参的类型
函数的实参可以是任意的数据类型。
调用函数时,解析器不会检查实参的类型,所以要注意,是否有可能会接收到非法的参数,如果有可能则需要对参数进行类型的检查。
实参的数量(实参和形参的个数不匹配时)
调用函数时,解析器也不会检查实参的数量。
如果实参的数量多于形参的数量,多余实参不会被赋值。
如果实参的数量少于形参的数量,多余的形参会被定义为 undefined。表达式的运行结果为 NaN。
代码举例:
function sum(a, b) {
console.log(a + b);
}
sum(1, 2); // 3
sum(1, 2, 3); // 3
sum(1); // NaN
注意:在 JS 中,形参的默认值是 undefined。
函数名、函数体和函数加载问题(重要,请记住)
我们要记住:函数名 == 整个函数。举例:
console.log(fn) == console.log(function fn(){alert(1)});
//定义fn方法
function fn(){
alert(1)
};
我们知道,当我们在调用一个函数时,通常使用函数()
这种格式;可如果,我们是直接使用函数
这种格式,它的作用相当于整个函数。
函数的加载问题:JS加载的时候,只加载函数名,不加载函数体。所以如果想使用内部的成员变量,需要调用函数。
fn() 和 fn 的区别【重要】
fn()
:调用函数。调用之后,还获取了函数的返回值。fn
:函数对象。相当于直接获取了整个函数对象。
类数组 arguments
这部分,小白可能看不懂。所以,这一段,暂时可以忽略。
在调用函数时,浏览器每次都会传递进两个隐含的参数:
1.函数的上下文对象 this
2.封装实参的对象 arguments
例如:
function foo() {
console.log(arguments);
console.log(typeof arguments);
}
foo();
arguments 是一个类数组对象,它可以通过索引来操作数据,也可以获取长度。
arguments 代表的是实参。在调用函数时,我们所传递的实参都会在 arguments 中保存。有个讲究的地方是:arguments只在函数中使用。
1、返回函数实参的个数:arguments.length
arguments.length 可以用来获取实参的长度。
举例:
fn(2, 4);
fn(2, 4, 6);
fn(2, 4, 6, 8);
function fn(a, b) {
console.log(arguments);
console.log(fn.length); //获取形参的个数
console.log(arguments.length); //获取实参的个数
console.log('----------------');
}
打印结果:
我们即使不定义形参,也可以通过 arguments 来使用实参(只不过比较麻烦):arguments[0] 表示第一个实参、arguments[1] 表示第二个实参…
2、返回正在执行的函数:arguments.callee
arguments 里边有一个属性叫做 callee,这个属性对应一个函数对象,就是当前正在指向的函数对象。
function fun() {
console.log(arguments.callee == fun); //打印结果为true
}
fun('hello');
在使用函数递归调用时,推荐使用 arguments.callee 代替函数名本身。
3、arguments 可以修改元素
之所以说 arguments 是伪数组,是因为:arguments 可以修改元素,但不能改变数组的长短。举例:
fn(2, 4);
fn(2, 4, 6);
fn(2, 4, 6, 8);
function fn(a, b) {
arguments[0] = 99; //将实参的第一个数改为99
arguments.push(8); //此方法不通过,因为无法增加元素
}
arguments 的使用
当我们不确定有多少个参数传递的时候,可以用 arguments 来获取。在 JavaScript 中,arguments 实际上是当前函数的一个内置对象。所有函数都内置了一个 arguments 对象(只有函数才有 arguments 对象),arguments 对象中存储了传递的所有实参.
arguments的展示形式是一个伪数组。伪数组具有以下特点:
可以进行遍历;具有数组的 length 属性。
按索引方式存储数据。
不具有数组的 push()、pop() 等方法。
代码举例:利用 arguments 求函数实参中的最大值
代码实现:
function getMaxValue() {
var max = arguments[0];
// 通过 arguments 遍历实参
for (var i = 0; i < arguments.length; i++) {
if (max < arguments[i]) {
max = arguments[i];
}
}
return max;
}
console.log(getMaxValue(1, 3, 7, 5));
函数作用域
提醒1:在函数作用域中,也有声明提前的特性:
函数中,使用var关键字声明的变量,会在函数中所有的代码执行之前被声明。
函数中,没有var声明的变量都是全局变量,而且并不会提前声明。
提醒2:定义形参就相当于在函数作用域中声明了变量。
作用域链
引入:
只要是代码,就至少有一个作用域
写在函数内部的局部作用域
如果函数中还有函数,那么在这个作用域中就又可以诞生一个作用域
基于上面几条内容,我们可以得出作用域链的概念。
作用域链:内部函数访问外部函数的变量,采用的是链式查找的方式来决定取哪个值,这种结构称之为作用域链。查找时,采用的是就近原则。
代码举例:
var num = 10;
function fn() {
// 外部函数
var num = 20;
function fun() {
// 内部函数
console.log(num);
}
fun();
}
fn();
打印结果:20。
JavaScript 运行三部曲
语法分析
预编译
解释执行
预编译前奏
在讲预编译前,我们先来普及下面两个规律。
两个规律
规律1:任何变量,如果未经声明就赋值,此变量是属于 window 的属性,而且不会做变量提升。(注意,无论在哪个作用域内赋值)
比如说,如果我们直接在代码里写 console.log(a)
,这肯定会报错的,提示找不到 a
。但如果我直接写 a = 100
,这就不会报错,此时,这个 a
就是 window.a
。
规律2:一切声明的全局变量,全是window的属性。(注意,我说的是在全局作用域内声明的全局变量,不是说局部变量)
比如说,当我定义 var a = 200
时,这此时这个 a
就是 window.a
。
由此,我们可以看出:window 代表了全局作用域(是说「代表」,没说「等于」)。
举例
掌握了上面两句话之后,我们再来看看下面的例子。
function foo() {
var a = b = 100; // 连续赋值
}
foo();
console.log(window.b); // 在全局范围内访问 b
console.log(b); // 在全局范围内访问 b,但是前面没有加 window 这个关键字
console.log(window.a); // 在全局范围内访问 a
console.log(a); // 在全局范围内访问 a,但是前面没有加 window 这个关键字
上方代码的打印结果:
100
100
undefined
(会报错,提示 Uncaught ReferenceError: a is not defined)
解释:
当执行了foo()
函数之后, var a = b = 100
这行连续赋值的代码等价于 var a = (b = 100)
,其执行顺序是:
(1)先把 100 赋值给 b;
(2)再声明变量 a;
(3)再把 b 的值赋值给 a。
我们可以看到,b 是未经声明的变量就被赋值了,此时,根据规律1,这个 b 是属于 window.b
;而 a 的作用域仅限于 foo() 函数内部,不属于 window。所以也就有了这样的打印结果。
预编译
函数预编译的步骤
函数预编译,发生在函数执行的前一刻。
(1)创建AO对象。AO即 Activation Object 活跃对象,其实就是「执行期上下文」。
(2)找形参和变量声明,将形参名和变量作为 AO 的属性名,值为undefined。
(3)将实参值和形参统一,实参的值赋给形参。
(4)查找函数声明,函数名作为 AO 对象的属性名,值为整个函数体。
这个地方比较难理解。但只有了解了函数的预编译,才能理解明白函数的执行顺序。
代码举例:
function fn(a) {
console.log(a);
var a = 666;
console.log(a);
function a() {}
console.log(a);
var b = function() {};
console.log(b);
function c() {}
}
fn(1);
打印结果:
ƒ a() {}
666
666
ƒ () {}
事件简介
事件:就是文档或浏览器窗口中发生的一些特定的交互瞬间。对于 Web 应用来说,有下面这些代表性的事件:点击某个元素、将鼠标移动至某个元素上方、关闭弹窗等等。
JavaScript 是以事件驱动为核心的一门语言。JavaScript 与 HTML 之间的交互是通过事件实现的。
事件的三要素
事件的三要素:事件源、事件、事件驱动程序。
比如,我用手去按开关,灯亮了。这件事情里,事件源是:手。事件是:按开关。事件驱动程序是:灯开了或者关了。
再比如,网页上弹出一个广告,我点击右上角的X
,广告就关闭了。这件事情里,事件源是:X
。事件是:onclick。事件驱动程序是:广告关闭了。
代码书写步骤如下:(重要)
(1)获取事件源:document.getElementById(“box”); // 类似于Android里面的findViewById
(2)绑定事件: 事件源box.事件onclick = function(){ 事件驱动程序 };
(3)书写事件驱动程序:关于DOM的操作。
常见的事件如下:
onload事件
onload事件比较特殊,这里单独讲一下。
当页面加载(文本和图片)完毕的时候,触发onload事件。
举例:
<script type="text/javascript">
window.onload = function () {
console.log("smyhvae"); //等页面加载完毕时,打印字符串
}
</script>
有一点我们要知道:js的加载是和html同步加载的。因此,如果使用元素在定义元素之前,容易报错。这个时候,onload事件就能派上用场了,我们可以把使用元素的代码放在onload里,就能保证这段代码是最后执行。
建议是:整个页面上所有元素加载完毕再执行js内容。所以,window.onload可以预防使用标签在定义标签之前。
备注:关于 onLoad事件,在下一篇文章《DOM简介和DOM操作》中有更详细的讲解和示例。
JavaScript的组成
JavaScript基础分为三个部分:
ECMAScript:JavaScript的语法标准。包括变量、表达式、运算符、函数、if语句、for语句等。
DOM:文档对象模型(Document object Model),操作网页上的元素的API。比如让盒子移动、变色、轮播图等。
BOM:浏览器对象模型(Browser Object Model),操作浏览器部分功能的API。比如让浏览器自动滚动。
DOM树:(一切都是节点)
DOM的数据结构如下:
上图可知,在HTML当中,一切都是节点(非常重要)。节点的分类,在上一段中,已经讲了。
整个html文档就是一个文档节点。所有的节点都是Object。
JS动画
JS动画的主要内容如下:
1、三大家族和一个事件对象:
三大家族:offset/scroll/client。也叫三大系列。
事件对象/event(事件被触动时,鼠标和键盘的状态)(通过属性控制)。
2、动画(闪现/匀速/缓动)
3、冒泡/兼容/封装
offset 家族的组成
我们知道,JS动画的三大家族包括:offset/scroll/client。今天来讲一下offset,以及与其相关的匀速动画。
offset的中文是:偏移,补偿,位移。
js中有一套方便的获取元素尺寸的办法就是offset家族。offset家族包括:
offsetWidth
offsetHight
offsetLeft
offsetTop
offsetParent
1、offsetWidth 和 offsetHight
offsetWidth
和 offsetHight
:获取元素的宽高 + padding + border,不包括margin。如下:
offsetWidth = width + padding + border
offsetHeight = Height + padding + border
这两个属性,他们绑定在了所有的节点元素上。获取元素之后,只要调用这两个属性,我们就能够获取元素节点的宽和高。
2、offsetParent
offsetParent
:获取当前元素的定位父元素。
如果当前元素的父元素,有CSS定位(position为absolute、relative、fixed),那么
offsetParent
获取的是最近的那个父元素。如果当前元素的父元素,没有CSS定位(position为absolute、relative、fixed),那么
offsetParent
获取的是body。
3、offsetLeft 和 offsetTop(不太懂)
offsetLeft
:当前元素相对于其定位父元素的水平偏移量。
offsetTop
:当前元素相对于其定位父元素的垂直偏移量。
备注:从父亲的 padding 开始算起,父亲的 border 不算在内。
举例:
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
<style>
.box1 {
width: 300px;
height: 300px;
padding: 100px;
margin: 100px;
position: relative;
border: 100px solid #000;
background-color: pink;
}
.box2 {
width: 100px;
height: 100px;
background-color: red;
/*position: absolute;*/
/*left: 10px;*/
/*top: 10px;*/
}
</style>
</head>
<body>
<div class="box1">
<div class="box2" style="left: 10px"></div>
</div>
<script>
var box2 = document.getElementsByClassName("box2")[0];
//offsetTop和offsetLeft
console.log(box2.offsetLeft); //100
console.log(box2.style.left); //10px
</script>
</body>
</html>
在父盒子有定位的情况下,offsetLeft == style.left(去掉px之后)。注意,后者只识别行内样式。但区别不仅仅于此,下面会讲。
offsetLeft 和 style.left 区别
(1)最大区别在于:
offsetLeft 可以返回无定位父元素的偏移量。如果父元素中都没有定位,则body为准。
style.left 只能获取行内样式,如果父元素中都没有设置定位,则返回””(意思是,返回空字符串);
(2)offsetLeft 返回的是数字,而 style.left 返回的是字符串,而且还带有单位:px。
比如:
div.offsetLeft = 100;
div.style.left = "100px";
(3)offsetLeft 和 offsetTop 只读,而 style.left 和 style.top 可读写(只读是获取值,可写是修改值)
总结:我们一般的做法是:用offsetLeft 和 offsetTop 获取值,用style.left 和 style.top 赋值(比较方便)。理由如下:
style.left:只能获取行内式,获取的值可能为空,容易出现NaN。
offsetLeft:获取值特别方便,而且是现成的number,方便计算。它是只读的,不能赋值。
scroll 相关属性
window.onscroll() 方法
当我们用鼠标滚轮,滚动网页的时候,会触发 window.onscroll() 方法。
1、ScrollWidth 和 scrollHeight
ScrollWidth
和 scrollHeight
:获取元素整个滚动区域的宽、高。包括 width 和 padding,不包括 border和margin。
注意:
scrollHeight
的特点是:如果内容超出了盒子,scrollHeight
为内容的高(包括超出的内容);如果不超出,scrollHeight
为盒子本身的高度。ScrollWidth
同理。
2、scrollTop 和 scrollLeft
scrollLeft
:获取水平滚动条滚动的距离。scrollTop
:获取垂直滚动条滚动的距离。
实战经验:
当某个元素满足scrollHeight - scrollTop == clientHeight
时,说明垂直滚动条滚动到底了。
当某个元素满足scrollWidth - scrollLeft == clientWidth
时,说明水平滚动条滚动到底了。
这个实战经验非常有用,可以用来判断用户是否已经将内容滑动到底了。比如说,有些场景下,希望用户能够看完“长长的活动规则”,才允许触发接下来的表单操作。
scrollTop 的兼容性
如果要获取页面滚动的距离,scrollTop 这个属性的写法要注意兼容性。
为了兼容,不管有没有DTD,最终版的兼容性写法:
window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop;
判断是否已经 DTD 声明
方法如下:
document.compatMode === "CSS1Compat" // 已声明
document.compatMode === "BackCompat" // 未声明
获取 html 文档的方法
获取title、body、head、html标签的方法如下:
document.title
文档标题;document.head
文档的头标签document.body
文档的body标签;document.documentElement
(这个很重要)。
document.documentElement
表示文档的html标签。也就是说,基本结构当中的 html 标签
而是通过document.documentElement
访问的,并不是通过 document.html 去访问的。
缓动动画
三个函数
缓慢动画里,我们要用到三个函数,这里先列出来:
Math.ceil() 向上取整
Math.floor() 向下取整
Math.round(); 四舍五入
缓动动画的原理
缓动动画的原理就是:在移动的过程中,步长越来越小。
设置步长为:目标位置和盒子当前位置的十分之一。用公式表达,即:
盒子位置 = 盒子本身位置 + (目标位置 - 盒子本身位置)/ 10;
client 家族的组成
clientWidth 和 clientHeight
元素调用时:
clientWidth:获取元素的可见宽度(width + padding)。
clientHeight:获取元素的可见高度(height + padding)。
body/html 调用时:
clientWidth:获取网页可视区域宽度。
clientHeight:获取网页可视区域高度。
声明:
clientWidth
和clientHeight
属性是只读的,不可修改。clientWidth
和clientHeight
的值都是不带 px 的,返回的都是一个数字,可以直接进行计算。
clientX 和 clientY
event调用:
clientX:鼠标距离可视区域左侧距离。
clientY:鼠标距离可视区域上侧距离。
clientTop 和 clientLeft
clientTop:盒子的上border。
clientLeft:盒子的左border。
三大家族 offset/scroll/client 的区别
区别1:宽高
offsetWidth = width + padding + border
offsetHeight = height + padding + border
scrollWidth = 内容宽度(不包含border)
scrollHeight = 内容高度(不包含border)
clientWidth = width + padding
clientHeight = height + padding
区别2:上左
offsetTop/offsetLeft:
- 调用者:任意元素。(盒子为主)
- 作用:距离父系盒子中带有定位的距离。
scrollTop/scrollLeft:
- 调用者:document.body.scrollTop(window调用)(盒子也可以调用,但必须有滚动条)
- 作用:浏览器无法显示的部分(被卷去的部分)。
clientY/clientX:
- 调用者:event
- 作用:鼠标距离浏览器可视区域的距离(左、上)。
函数封装:获取浏览器的宽高(可视区域)
函数封装如下:
//函数封装:获取屏幕可视区域的宽高
function client() {
if (window.innerHeight !== undefined) {
//ie9及其以上的版本的写法
return {
"width": window.innerWidth,
"height": window.innerHeight
}
} else if (document.compatMode === "CSS1Compat") {
//标准模式的写法(有DTD时)
return {
"width": document.documentElement.clientWidth,
"height": document.documentElement.clientHeight
}
} else {
//没有DTD时的写法
return {
"width": document.body.clientWidth,
"height": document.body.clientHeight
}
}
}
案例:根据浏览器的可视宽度,给定不同的背景的色。
PS:这个可以用来做响应式。
代码如下:(需要用到上面的封装好的方法)
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
</head>
<body>
<script src="tools.js"></script>
<script>
//需求:浏览器每次更改大小,判断是否符合某一标准然后给背景上色。
// // >960红色,大于640小于960蓝色,小于640绿色。
window.onresize = fn; //页面大小发生变化时,执行该函数。
//页面加载的时候直接执行一次函数,确定浏览器可视区域的宽,给背景上色
fn();
//封装成函数,然后指定的时候去调用和绑定函数名
function fn() {
if (client().width > 960) {
document.body.style.backgroundColor = "red";
} else if (client().width > 640) {
document.body.style.backgroundColor = "blue";
} else {
document.body.style.backgroundColor = "green";
}
}
</script>
</body>
</html>
上当代码中,window.onresize
事件指的是:在窗口或框架被调整大小时发生。各个事件的解释如下:
window.onscroll 屏幕滑动
window.onresize 浏览器大小变化
window.onload 页面加载完毕
div.onmousemove 鼠标在盒子上移动(注意:不是盒子移动)
获取显示器的分辨率
比如,我的电脑的显示器分辨率是:1920*1080。
获取显示器的分辨率:
window.onresize = function () {
document.title = window.screen.width + " " + window.screen.height;
}
显示效果:
上图中,不管我如何改变浏览器的窗口大小,title栏显示的值永远都是我的显示器分辨率:1920*1080。