前端js


变量的数据类型

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;">&#9860;</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。


评论
评论
  目录