前端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'6true 等任意数据类型的值,也可以是表达式。注意,在这里,字符串'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。比如consoledocument

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

offsetWidthoffsetHight:获取元素的宽高 + 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

ScrollWidthscrollHeight:获取元素整个滚动区域的宽、高。包括 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:获取网页可视区域高度。

声明

  • clientWidthclientHeight 属性是只读的,不可修改。

  • clientWidthclientHeight 的值都是不带 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。


评论
评论
  目录