JavaScript百炼成仙-(记录笔记)

在JavaScript中,数据可分为两类,分别为原生数据类型和对象数据类型

对象数据类型,是一种复合型的数据类型,
它可以把多个数据放到一起,就好像一个篮子,这个篮子里面的每一个数据都可以看作是一个单元,它们都有自己的名字和值。


对象

var container = {
  caoyao: "草药",
  renshen: "人参"
};

var container = {};
container.caoyao = "草药";
container.renshen = "人参";

console.log(container.hyq); // 这个属性不存在,返回undefined

// 获取对象的属性的值
console.log(container.caoyao);

console.log(container['caoyao']);

var prop = "caoyao";
console.log(container[prop]);

typeof() 返回数据类型

var a = "hyq";
	var fun = function(){

}
console.log(typeof(a));
console.log(typeof(fun));
    if(typeof(fun) == "function"){
        console.log(true);
}

对象内容的遍历

        var LiBai = {
            name : "李白",
            age : 18,
            eat : function(){
                console.log("黄鹤楼");
            }
        }

        for(var i in LiBai){
            console.log(i);
            console.log(LiBai[i]);
        }

数组的4种定义方式

直接量定义数组
var arr = ["first", "second", "third"];
console.log(arr);
构造函数的方式创建的一个数组对象

在JavaScript中, 每个类型其实都有一个函数作为支撑。

在这个例子 中,Array也叫作构造函数。

var a = new Array();
console.log(a.length);  // 0
其他两种
var b = new Array(8);
console.log(b.length);  // 8  
console.log(b);  // 这个数组的内部就是8个空元素,没有东西,但是占据了内存空间

var c = new Array("first", "second", "third");

数组方法

push() 元素追加到数组末尾
var b = new Array(8);
b.push("苹果");
b.push( "香蕉");
b.push( "牛油果");
b[0] = "火龙果";
console.log(b);
pop() 删除数组尾端元素
var b = new Array(8);
console.log(b.length);  // 8  
console.log(b);  // 这个数组的内部就是8个空元素,没有东西,但是占据了内存空间
b.push("苹果");
b.push( "香蕉");
b.push( "牛油果");
b[0] = "火龙果";
b.pop();
console.log(b);
splice() 插入、删除或替换数组元素

splice方法用前两个参数进行定位,余下的参数表示插入部分。

数组的下标位置默认 从0开始

第一个参数 代表 需要操作的数组的起始位置

第二个参数代表 要删除元素的个数

余下的参数表示插入部分

var a = [1,2,3,4,5];
a.splice(2,1);  // 从数组下标为2的元素开始,删除1个元素。
console.log(a);  // [1,2,4,5 ]

var a = [1,2,3,4,5];
a.splice(2,1,38,66); // 从数组下标为2的元素开始,先删除1个元素,后插入38 和 66 两个元素
console.log(a); //  [1, 2, 38, 66, 4, 5]
jion() 数组转换字符串

join方法可以把数组中的所有元素放入一个字符串。

元素是通过指定的分隔符进行分隔的,而这指定的分隔符就是join方法的参数。

var arr = [1,2,3];
var str = arr.join(",");
console.log(str);   //  1,2,3

函数七重关

函数七重关之一(函数定义)

两种定义方式

function myFun(){
    document.write("我一定要成为一名前端工程师!")
}

var a = function(){
    document.write("我一定要成为一名全栈工程师!")
}

区别:

第一种方法定义的函数,把调用语句放在前面,则可以 成功调用。

第二种方法定义的函数,把调用语句放在前面,会报错。Uncaught TypeError: a is not a function

原因:

第一种方法定义的函数,它会被提前加载,因此调用语句可以写在函数的定义之前,因为那个时候函数已经被加载完毕了。

第二种方式定义的函数是不会被提前加载的。必须要执行到函数定义的语句才会加载这个函数。

因为在调用a函数的时候,a函数还没有加载,强行调用一个不存在的函数自然是不被允许的!

myFun();
function myFun(){
    document.write("我一定要成为一名前端工程师!")
}

a();	//	报错
var a = function(){
    document.write("我一定要成为一名全栈工程师!")
}

疑问:

为什么第一个a打印出来是undefined,而不是直接报错 呢?

我之前遇到过这种 情况,就是引用一个从来没有被定义过的变量apple,得到的结果是直接报错。ReferenceError: apple is not defined

因为我从来没有在任何地方定义过一个apple变量。你刚才调用了一个还未被加载的函数,为什么会打印出undefined而不是报错呢?

console.log(a);  // undefined
var a = function(){
    alert("天大寒,砚冰坚。");
}
console.log(a);
console.log(apple); // 报错

回答:

函数有没有被加载 与 变量有没有被定义是不同的事情。

函数有没有被加载,可以看成function有没有被赋值给变量a。从代码上来看,自然是没有的。

也就是说,当调用变量a的时候,变量a并没有被赋值。但是不管变量a有没有被赋予一个function函数,我就问你一 个问题,a有没有定义?

疑问:

定义是定义了,可是它并没有被运行到啊!

回答:

这就要说到 JavaScript代码的运行机制

JavaScript编译原理

JavaScript代码在运行之前会经过一个编译的过程,而编译有三个步骤。

var a = 10;

第一个步骤是分词

JavaScript代码其实就是由一句句话组成的, 分词的目的是把这些代码分解为一个个有意义的代码块。如果经过分词的步骤,那么得到的结果就是 ‘ var、a、=、10、;’

第二个步骤是解析

由JavaScript编译器对刚才分词得到的一个个代码块进行解析,生成一棵抽象的语法树(AST)。

JavaScript代码是没有办法直接运行的,要想运行JavaScript代码,就需要通过JavaScript编译器对其进行编译,只有编译之后的代码才可以被识别,然后通过JavaScript引擎执行代码逻辑。

抽象语法树是什么啊?

抽象语法树定义了代码本身,通过操作这棵树可以精准地定位到 赋值语句、声明语句和运算语句。

再来说说刚才的代码,很明显,这是一个赋值语句,当然,这也是一个定义的语句。我们通过JavaScript的解析器把它解析为一棵抽象树。

  • type: Program
  • -body
    • -#1
      • type: VariableDeclaration
      • -declarations
        • -#1
          • type: VariableDeclarator
          • -id
            • type: Identifier
            • name: a
          • -init
            • type: Literal
            • value: 10
            • raw: 10
      • kind: var
  • sourceType: script

Program :程序

VariableDeclaration:变量声明

‘var a = 10;’这句话是一个程序,程序的目的是进行一个变量的声明。

declarations[1]是声明数组,中括号里面写了一个1,表示这个语句只声明了一个变量。kind代表种类,表示用 var关键字声明一个变量,

VariableDeclarator:变量声明

id:变量名

identifier:标识符

init:初始化操作,将10赋 给变量a

不把10赋值给a,看看会怎样?

如果没有给变量a赋值,那么JavaScript的解释器也会给变量a赋一 个初始值,null代表空。

注意:这里的null不要理解为JavaScript里面的数据类型null,而是语义上的空。实际上,在代码执行的时候,变量a的值是undefined。

最后一个步骤,就是代码生成。

JavaScript引擎会把在第二个步骤中生成的抽象语法树进行转换。也许最终生成出来的就是一些机器指令,创建了一个叫作a的变量并放在变量区,然后分配一些内存以存放这个变量,最后将数字10存储在了变量a所在的地方。

if(false) {
    var b = 20;
}
console.log(b); // undefined

在if判断中,而if判断的条件是false,所以这句话的确不会执行。

但是,执行代码是在运行阶段,在代码的 分词阶段 和 解析阶段,变量a依然会被获取,并且系统会默认给它一个 undefined。

又因为变量a不是在某一个函数的函数体中,而是在全局作用域里面,所以console.log方法依然可以访问这个变量,因此获取变量a 的值就是undefined。

console.log(a);  // undefined
var a = function(){
    alert("天大寒,砚冰坚。");
}
console.log(a);
console.log(apple); // 报错

接下来可以解释之前的那个问题了:

第一次执行console.log方法的时候,变量a还没有被赋值为一个函数,但是JavaScript引擎还是会把它提取出来并放入全局作用域,并且默认给它一个undefined。

所以,第一次打印出来的就是undefined。接下来就是一个赋值语句了。

这个赋值语句把一个匿名函数赋给了变量a,那么从此变量a就指 向了这个函数,换句话说,一个名字叫作a的函数就已经产生了。

这句话一旦执行,a就不再是undefined了,而是一个函数。

接下来执行第二个console.log方法,这个时候a自然已经有值了,所以打印出来的是一个函数。

函数七重关之二(作用域)

在JavaScript中,作用域分为两种,一种是全局作用域,另一种是函数作用域。

不管是全局作用域还是函数作用域,都被定义在 词法阶段。

词法阶段就是刚才所说的JavaScript编译代码的第一个步骤 ——分词。所以,词法阶段也叫作分词阶段。

var a = 1;
function test(){    
    var a;
    var inner = function(){
        console.log(a);    
    }    
    inner();
}
test(); //undefined

函数作用域里面嵌套了函数作用域,那 么在最里面的inner函数中访问一个变量,就会优先在inner函数里面寻找,结果却发现找不到。

既然在当前函数作用域里面找不到,那么就往上翻一层,在它的父级作用域,也就是test函数的作用域里面寻找,结果发现找到了。

test函数里面定义了一个变量a,但是没有赋值,那么a 就是undefined。

既然已经找到了,那么就不会去全局作用域里面寻找变 量a了。所以,全局作用域里面的变量a其实就是一个摆设。

函数七重关之三(参数传递)
function add(a,b,c){
    var sum = a + b + c;    
    console.log(sum);
}

如果我在调用函数的时候就传了一个参数咋办?

function test(a){
	console.log(a);
}
test(); //   undefined

因为test函数明明是要求填写一个参数的,那就是a。

可是在调用函数的时候,却偏偏没有参数传递进来。

这 按理说是不被允许的,可是当这种情况真的发生了会怎样呢?

也就是说,如果没有参数传进来,那么函数中已经设置好的参数会等于什么 呢?

结果显示:undefined。

其实,对于函数传参到底是怎么回事,可以把这个例子再次细分。

刚才的函数中有一个参数a,那么这个参数自然也属于函数作用域,就相当于下面这样。

function test(){
    var a;
    console.log(a);
}

函数的参数可以简单地 看成是在函数体,也就是花括号扩起来的地方,即里面的第一行定义了 一个变量。

因为我们并没有给这个变量赋值,所以这个局部变量就是 undefined。

可以这么说,任何变量在被赋予真正的值之前,其在编译阶段都是undefined。

或者说,任何变量不管其最终的值是什么,它都曾经是undefined。

这些函数的参数可以被理解为一种预备变量。接下来说说正常的情况,比如我调用fun函数,传递一个参数18。传参的过程就相当于是给预备变量赋值的过程。如果没有传参,那么预备变量自然还是 undefined。

function add(a,b,c){
    var sum = a + b + c;
    console.log(sum);
}
add(1); //  NaN

这种情况下,a的值是1,b和c的值就是undefined,

那么数字1 和 2个undefined相加会是多少呢?

结果是NaN,代表无法计算。

如果我多传一个参数又会怎样呢?

好比我定义了一个 函数fun,但没有参数,如果我在调用fun函数的时候故意给它加了一个参数,会发生什么?

结果是可想而知的,自然是什么都不会发生啦。

再回到刚才的例子中,就算你强行加了第四个参数,对结果也不会有什么影响。

function fun(){
}
fun(10);

function add(a,b,c){
    var sum = a + b + c;
    console.log(sum);
}
add(1,2,3,4); //    6

如果我一定要在函数里面访问额外的参数需要咋办?

其实所有的参数都会被装载到函数内部一个叫 作arguments的数组里面。

在函数的内部还维护了一个arguments数组。

function add(a,b,c){
    console.log(arguments);	//	 [1, 2, 3, 4, callee: ƒ, Symbol(Symbol.iterator): ƒ]
    var sum = a + b + c;
    console.log(sum);
}
add(1,2,3,4); //    6

传过来的四个参数,其实都放进了这个默认的 arguments数组里面。

换句话说,参数列表里面的a、b、c也是根据这个数组赋值的。

function add(a,b,c){
    console.log(arguments);
    a = arguments[0];
    b = arguments[1];
    c = arguments[2];
    var sum = a + b + c;
    console.log(sum);
}
add(1,2,3,4); //    6

根据这个特性,可以完成一些有趣的功能,比如我可以编写 一个函数,参数个数任意,实现数字的累加。

function add(){
    if(!arguments[0]){
        return 0;
    }
    for(var i = 1; i<arguments.length; i++){
        arguments[0] = arguments[0] + arguments[i];
    }
    return arguments[0];
}
console.log(add(0,100,200,300)); //第一种累加器有bug, 第一个元素为0时,返回值都是0.

function add(){
    var sum = 0;
    for(var i = 0; i<arguments.length; i++){
        sum = sum + arguments[i];
    }
    return sum;
}
console.log(add(0,100,200,300));
函数七重关之四(闭包)
function test(){
    return function(){
    }
}

在一个函数里面嵌套了另外一个函数,因为我只是想要把一个函数返回出去,而并不在乎这个内部函数叫啥名字, 所以干脆就不给它取名字了,那么里面的这个函数就是一个匿名的函数。

这种写法虽然有点奇怪,但它依然是正确的。

因为函数作用域可以嵌套,所以里面的函数作用域 就可以访问外面函数作用域中的变量了。

function test(){
    var a = 0;
    return function(){
        console.log(a);
    }
}
test()();

产生闭包的条件:

第一点,在函数内部也有一个函数。

第二点,函数内部的函数里面用到了外部函数的局部变量。

第三点,外部函数把内部函数作为返回值return出去 了。

闭包的好处:

正常情况下,我们调用一个函数, 其里面的局部变量会在函数调用结束后销毁,这也是我们在全局作用域里面无法访问函数局部变量的原因。

但是,如果你使用了闭包,那么就会让这个局部变量不随着原函数的销毁而销毁,而是继续存在。

function test(){

    var a = 0;

    return function(increment){
        a = a +increment;
        console.log(a);
    }
}
var inner = test();
inner(1);	//	1
inner(1);	//	2
inner(1); 	//	3

证明在每次调用内部函数的时候,里面访问的都是同 一个变量a了。

这种写法就相当于在全局作用域里面定义了一个变量a, 然后在函数中操作全局变量。

这样的形式操作,也就是利用闭包 操作可以减少很多不必要的全局变量。

全局作用域是一块公共区域,如果为了某个单一的功能而定义一个全局变量,则会导致全局变量过多, 代码就变得一团糟了。

函数七重关之五(自执行函数)

所谓自执行函数,顾名思义, 就是在定义之后就立刻执行的函数,它一般是没有名字的。

也正因为自执行函数没有名字,所以它虽然会被立刻执行,但是它只会被执行一 次。

(
    function(){
        console.log(123);
    }
)
();

自执行函数一般可以和闭包配合使用

var innner = (function(){
    var a = 0;
    return function(increment){
        a = a + increment;
        console.log(a);
    }
})();
innner(2);  //  2
innner(2);  //  4
innner(2);  //  6
函数七重关之六(“new”一个函数)
function hello(){
    console.log(this);
}
hello();
window.hello();
window['hello']();
var p = 'hello';
window[p]();

打印出this对象

this也是JavaScript中的一个关键字,

this永远指向当前函数的调用者。

this要么不出现,一旦出现,就一定出现在函数中。

this指向函数的调用者,换句话说,这个函数是谁调用的,那么this 就是谁。

JavaScript里面分为全局作用域和函数作用域,在全局 作用域里面定义的任何东西,不管是一个变量还是一个函数,其实都是 属于window对象的。

function hello(){
	console.log(this);
}
new hello();	// 产生一个新的对象

结果是 hello函数内部产生了一个新的对象 hello {},

也就是 hello函数的真实调用者——this关键字指向的那个对象。

说得简单些,就是函数内部产生了一个新的对象,并且this指向了这个对象, 然后函数默认返回了这个新的对象。

function hello(){
    console.log(this);
}
new hello;
var newObject = new hello();
console.log(newObject);

newObject就是函数里面的this,也就是函数内部新产生的那个对象了。

这种函数还有一个别称,叫作 构造函数

通过构造函数构建一个对象模板

指用一个函数的形式设计一种对象的种类。说得简单些,比如苹果、香蕉、板栗这些食物,它们都是食物,那么我就可以设计一个对象模板描述食物的一些共同特点。比如,食物有名字、味道、颜色等属性,那么我就可以在函数中用this关键字设计这些属性。

function Fruit(name, smell, color){
    this.name = name;
    this.smell = smell;
    this.color = color;
}

一般来说,如果这是一个构造函数,那么首字母就需要大写。

因为函数在使用了new关键字以后会从内部新产生一个对象出来, 而this就指向了这个对象。

基于这样的一个缘由,可以直接在函数里面给未来即将生成的那个对象设置属性。

function Fruit(name, smell, color){
    this.name = name;
    this.smell = smell;
    this.color = color;
};
var apple = new Fruit('苹果', '酸甜', '红色');

如果需要两个苹果,就直接调用两次new函数就行了。

var apple1 = new Fruit('苹果', '酸甜', '红色');
var apple2 = new Fruit('苹果', '酸甜', '红色');

使用 对象大括号写法,则需要写两次。

var apple3 = {
    name: "苹果",
    smell: "酸甜",
    color: "红色"
};
var apple4 = {
    name: "苹果",
    smell: "酸甜",
    color: "红色"
}

错误的示范

apple3和apple4其实都是指向同一个苹果的

var apple3 = {
    name: "苹果",
    smell: "酸甜",
    color: "红色"
};
var apple4 = apple3;

验证

添加一个 是否被吃的属性,然后让变量apple4等于apple3,修改apple4的eat属性,把没有被吃掉的状态改成已经被吃掉。

结果 apple3 显示也被吃了。

var apple3 = {
    name: "苹果",
    smell: "酸甜",
    color: "红色",
    isEat: false
};
var apple4 = apple3;
apple4.isEat = true; // 吃了apple4
console.log(apple3.isEat) // true

原因

除了基本数据类型之外,其他都属于引用数据类型。

比如对象就属于引用数据类型。

如果将基本数据类型赋值给某一个变量,然后将这个变量赋值给另外一个变量,就可以看成是数据值的复制。

var a1 = 10; 
var a2 = a1;

a1和a2还是不同的数据,虽然都是10,但是在内存上却处于不同的空间。

而引用数据类型则不同,如果简单地分出一个变量区和内存区,

apple3和apple4就都属于变量区的两个不同的变量了,但是却指向同一块内存地址,也就是真实的对象地址。

不管是apple3还是apple4,它们都拥有操作这一块内存区域的权限,也就是说,它们都可以修改真实对象的属性值。

函数七重关之七(回调函数)

所谓回调函数,就是指把一个函数的定义当作参数传递给另一个函数。

JavaScript提供了一种强大的特性,这个特性就是: 函数也可以作为另一个函数的参数。

function eat(food, callback){
    callback(food);
}
eat ('羊肉串', function(food){
    alert("坐在家里,自己动手烤着吃" + food);
})

变量和简单数据类型

简单数据类型,分为字符串、数值型、布尔型、null和undefined。

判断为假的几种情况:

字符串为空

数字 0

null

undefined

console.log('穷且益坚,不坠青云之志。' ? '真' : '假');  // 真

console.log('' ? '真' : '假');  //假

console.log(10 ? '真' : '假');  //真
console.log(-10 ? '真' : '假');  //真

console.log(0 ? '真' : '假');  //假

console.log('0' ? '真' : '假');  //真
console.log(null ? '真' : '假');  //假
console.log(undefined ? '真' : '假');  //假

精度问题

在JavaScript中整数和浮点数都属于 Number 数据类型,

所有数字都是以 64 位浮点数形式储存,即便整数也是如此。

console.log(0.1+0.2); //    0.30000000000000004

计算过程

首先,十进制的0.10.2都会被转换成二进制,但由于浮点数用二进制表达时是无穷的,例如。

0.1 -> 0.0001100110011001...(无限)
0.2 -> 0.0011001100110011...(无限)

IEEE 754 标准的 64 位双精度浮点数的小数部分最多支持 53 位二进制位,所以两者相加之后得到二进制为:

0.0100110011001100110011001100110011001100110011001100

因浮点数小数位的限制而截断的二进制数字,再转换为十进制,就成了 0.30000000000000004。所以在进行算术计算时会产生误差。

var num01 = 0.1;
var num02 = 0.2;
num01 = num01 * 10;
num02 = num02 * 10;
console.log((num01 + num02) / 10); // 0.3

封装成函数,此函数只能适用与 两个数 都是小数。

function add(num1, num2) {
    // 将数字转换为字符串
    num1 = num1.toString();
    num2 = num2.toString();
    // 获取数字小数点后的位数
    var a = num1.split(".")[1].length;
    var b = num2.split(".")[1].length;
    var max = a; // 取最大的 小数位
    if (a < b) { max = b; } // 根据位数 获取对应的10次幂数
    var beishu = 1;
    for (var i = 0; i < max; i++) {
         beishu = beishu * 10; 
    } 
    // 小数 乘 这个倍数
    num1 = num1 * beishu;
    num2 = num2 * beishu;
    // 最后再除于 这个倍数
    var sum = (num1 + num2) / beishu;
    console.log(sum);
}
add(0.01, 0.2); //  0.21

这个函数现在虽然可以正确地计算0.1+0.2,但不能计算1+0.2。

下面是错误示例

在获取小数位数之前判断一下,如果是小数,就按照原先的方法进行,否则就默认小数位数是0!

function add(num1, num2) {
    // 将数字转换为字符串
    num1 = num1.toString();
    num2 = num2.toString();
    
    var a, b;
    
    // 判断是否为小数
    if(num1.indexOf('.') == -1){
        a = 0;
    }else{
        a = num1.split(".")[1].length;
    }

    // 判断是否为小数
    if(num2.indexOf('.') == -1){
        b = 0;
    }else{
        b = num2.split(".")[1].length;
    }

    // 取最大的 小数位 
    var max = a;
    if(a < b){
        max = b;
    }
    
    // 根据位数 获取对应的10次幂数
    var beishu = 1;
    for(var i = 0; i < max; i++){
        beishu = beishu * 10;
    }

    // 小数 乘 这个倍数
    num1 = num1 * beishu;
    num2 = num2 * beishu;

    // 最后再除于 这个倍数
    var sum = (num1 + num2) /beishu;
    console.log(sum);  
}
add(1.01, 2.02); // 3.03
add(1.01, 2.002); //  3.012
add(1.001, 2.002); //  3.0029999999999997

第三次执行 ,放大的倍数是1000,也就是说,1.001 乘以1000本身就有问题。

console.log(1.001 * 1000); // 1000.9999999999999

看来不仅是加减法,小数乘除法的计算依然会有精度丢失的问题!

解决方法:

那就不作乘法,而是利用替换。

function add(num1, num2){
    // 先将数字转换为字符串
    num1 = num1.toString();
    num2 = num2.toString();

    var ws1 = 0;
    var ws2 = 0;

    // 如果小数点存在,则获取小数位数
    if(num1.indexOf('.') != -1){
        ws1 = num1.split('.')[1].length;
    }

    if(num2.indexOf('.') != -1){
        ws2 = num2.split('.')[1].length;
    }

    // 看谁的小数位 大,谁的小
    var bigger = (ws1>ws2) ? ws1 : ws2;
    var smaller = (ws1<ws2) ? ws1 : ws2;
    
    // 计算 需要补齐几个 0的个数。
    var zerosCount = bigger - smaller;

    // 先全部去除小数点
    num1 = num1.replace(".", "");
    num2 = num2.replace(".", "");

    // 谁的尾数 最小,就给谁补0;
    if(ws1 == smaller){
        for(var i = 0; i < zerosCount; i++){
            num1 += "0";
        }
    }else{
        for(var i = 0; i < zerosCount; i++){
            num2 += "0";
        }
    }

    // 开始计算,字符串转换为整数
    var sum = parseInt(num1) + parseInt(num2);

    // 按照最大的小数位,计算倍数
    var beishu = 1;
    for(var i = 0; i < bigger; i++){
        beishu = beishu * 10;
    }

    return  sum = sum / beishu;
}
console.log(add(1, 2.009));  //  3.009

字符串方法


split() 分隔字符串

split函数,可以把一个字符串通过某种规则和标记符号进行分隔,并返回一个数组。

var num01 = "0.01";
var num01_arr = num01.split(".");
console.log(num01_arr); //  ['0', '01']

indexOf() 搜索字符串

indexOf的意思是在原字符串中搜索一个特定的字符串,

比如从“123456”中搜索“2”,如果存在“2”,就把这个字符串在原字符串中的位置返回。

字符串的下标也是从0开始的。因此,用indexOf方法从“123456”中搜索“2”,返回的结果是1,而不是2。

如果搜索不到则 返回 -1

if('1.002'.indexOf('.') == -1){
    a = 0;
}else{
    a = num1.split(".")[1].length;
}

replace() 替换单个字符串

把里面的c替换成大写的C

"abcd".replace("c", "C");

第一个参数是需要替换的内容,第二个参数是替换后的内容。
但是这个方法有一个缺憾,那就是它只能匹配到字符串中的第一个匹配项,
也就是说,如果原字符串里面有多个匹配项,那么就只有第一个匹配项会生效,剩余的匹配项则享受不到替换的待遇。
比如,现在需要对字符串“abcdabcd”进行替换,将里面所有的a替换为A,如果单纯用replace方法,效果是不尽人意的。

"abcdabcd".replace("a", "A");	\\ "Abcdabcd"

jQuery


引用jQuery

<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>

$是什么?

console.log(typeof($));  //function

第一步是用jQuery选择器获取这个按钮;第二步是给这个按钮添加一个单击事件。

<body>
    <button id="btn">点我</button>
</body>
<script>
    console.log(typeof($));  //function
    $('#btn').click(function func(){
        alert('现在时间:2021/11/24 23:48,明天取爬梅林咯!');
    });
</script>

疑问?

​ 为什么要把用jQuery添加单击事件的代码放在input标签下面呢?如果放在上面行不行?

因为浏览器在解析代码时是一行一行地往下执行的,当它执行到这一段代码的时候。

$('#btn').click(function func(){
 alert('现在时间:2021/11/24 23:48,明天取爬梅林咯!');
});

如果JavaScript代码下方的按钮还没有被加载,那么$('#btn')就没有办法获取对应的按钮对象。

解决办法

写一个DOM加载完毕后的监听函数

<script>
    $(document).ready(function() {
        // 这里的代码会在所有元素加载完毕后再执行
        $('#btn').click(function func(){
            alert('不积跬步,无以至千里;不积小流,无以成江海。 -- 荀子');
        });
    });
</script>
<body>
    <button id="btn">点我</button>
</body>

$(document).ready方法接收一个回调函数,它会等页面上所有的DOM资源(不包括图片这种占用带宽的资源)全部加载完毕后,再调用这个回调函数。这样一来,就不用考虑在绑定事件的时候某个HTML元素还没有被加载的情况了。


jQuery操作DOM

查找元素

find() 查找元素

eq() 从元素集中查询元素,指定元素位置(最小为0),如果是负数则从集合中的最后一个元素往回计数

text() 返回元素的文本

<body>    <h2>道德经</h2>    <ul>        <li>道可道,非常道</li>        <li>名可名,非常名</li>        <li>无名天地之始,有名万物之母</li>        <li>故常无欲,以观其妙</li>        <li>常有欲,以观其徼(jiào)</li>        <li>此两者同出而异名,同谓之玄,玄之又玄,众妙之门</li>    </ul></body><script>    var ul = $('ul');    var lis = ul.find('li');    var li2 = lis.eq(2);    //  指定元素位置(最小为0),如果是负数则从集合中的最后一个元素往回计数    var li2_text = li2.text();    console.log(li2_text);  //  无名天地之始,有名万物之母</script>

第二种写法

<script>    var text = $('ul li:eq(1)').text();    console.log(text);  //  名可名,非常名</script>

查询属性

attr() 设置或返回被选元素的属性值。

寻找最后一个li元素的id属性值

<body>
    <h2>道德经</h2>
    <ul>
        <li id="a1">道可道,非常道</li>
        <li id="a2">名可名,非常名</li>
        <li id="a3">无名天地之始,有名万物之母</li>
        <li id="a4">故常无欲,以观其妙</li>
        <li id="a5">常有欲,以观其徼(jiào)</li>
        <li id="a6">此两者同出而异名,同谓之玄,玄之又玄,众妙之门</li>
    </ul>
</body>
var $li = $('ul li');
var len = $li.length;
var id = $li.eq(len-1).attr('id');
alert(id);	//	a6

第二种方式

用jQuery的一种特殊选择器直接获取最后一个元素

var id = $('ul li:last').attr('id');
alert(id);  //a6

链式调用

刚才的代码中频繁出现对象在调用函数之后,又立刻调用其他函数或者属性的情况,你可知道这是怎么回事,为什么能够这么写?

如果我要调用func01函数和func02函数,一般是这么写。

这是正常的函数调用,不属于链式调用。

var  myFunction = function() {
    return{
        func01 : function() {
            console.log("func01");
        },
        func02 : function() {
            console.log("func02");
        }
    }
}
var obj = myFunction();
obj.func01();
obj.func02();

如果是链式调用,就需要写这样的代码。

obj.func01().func02();

既然能够在调用func01函数之后立刻调用func02函数,就需要func01函数的返回值是一个对象,

func01函数和func02函数的宿主对象都是obj,也就是说,我只要让func01的返回值变成obj就行了。

一个this关键字就可以解决这个问题,因为在函数中,this 关键字永远指向当前函数的调用者。

var  myFunction = function() {
    return{
        func01 : function() {
            console.log("func01");
            return(this);
        },
        func02 : function() {
            console.log("func02");
            return(this);
        }
    }
}
var obj = myFunction();
obj.func01().func02();

创造新的元素

appendTo()

append()

jQuery还可以动态地创造新的元素并添加到页面上

如果我想添加一个新的li 到ul列表中

<body>
    <h2>道德经</h2>
    <ul>
        <li id="a1">道可道,非常道</li>
        <li id="a2">名可名,非常名</li>
        <li id="a3">无名天地之始,有名万物之母</li>
        <li id="a4">故常无欲,以观其妙</li>
        <li id="a5">常有欲,以观其徼(jiào)</li>
    </ul>
</body>

首先, 创建一个新的元素。

var newLi = $("<li id=\"a6\">此两者同出而异名,同谓之玄,玄之又玄,众妙之门</li>");

用到了 jQuery的工厂函数$(),当向这个函数传入一段HTML代码时,jQuery会自动解析这一段HTML,然后创建对应的DOM节点,最后将这个DOM节点的jQuery对象返回出去。

接下来我只需要把它添加到ul元素中即可。可以使用appendTo方法或者append方法。

用appendTo方法

newLi.appendTo($('ul'));// 简写

$("<li id=\"a6\">此两者同出而异名,同谓之玄,玄之又玄,众妙之门</li>").appendTo($('ul'));

用append方法

$('ul').append($("<li id=\"a6\">此两者同出而异名,同谓之玄,玄之又玄,众妙之门</li>"));

还有一种html方法,它的意义和添加方法有所不同,是直接替换目标元素里面的所有HTML代码。

$('ul').html($("<li id=\"a6\">来自老子</li> "));

这样一来,ul 里面就只剩下一个 li 元素了。


拓展:四个插入函数

insertAfter() 把匹配的元素插入另一个指定元素集合的后面

insertBefore() 把匹配的元素插入另一个指定元素集合的前面

prepend() 向匹配元素集合中的每个元素的开头插入由参数指定的内容

prependTo() 向目标的开头插入匹配元素集合中的每个元素

<body>
    <p>老骥伏枥,志在千里。</p>
    <div>
        志在千里
    </div>
</body>
<script>
    $('<h4>来自--曹操</h4>').insertAfter('p');
    $('<h3>龟虽寿</h3>').insertBefore('p');
    $('div').prepend('<span>老骥伏枥,</span>'); //在div内部
    $('<h3>龟虽寿</h3>').prependTo('div');  //在div内部
</script>

最后DOM结构变成下面这样子:

<body>
    <h3>龟虽寿</h3>
    <p>老骥伏枥,志在千里。</p>
    <h4>来自--曹操</h4>
    <div>
        <h3>龟虽寿</h3>
        <span>老骥伏枥,</span>
        志在千里    
    </div>
</body>

删除和隐藏节点

remove() 删除

hide() 隐藏

show() 显示

<body>
    <h2 id="a1">我一定一定要学会前端!</h2>
</body>
<script>
    $("#a1").remove();  //  删除
    $("#a1").hide();    // 隐藏
    $("#a1").show();    // 显示
</script>

jQuery操作属性

attr() 返回或设置元素的属性

<h1 id="d1">道可道,非常道。</h1>

给attr方法传入一个参数,那么就获取这个元素的某个属性的值,属性名就是传进来的参数。

var id = $("#d1").attr('id'); //	d1

给attr方法传入两个个参数, 可以设置元素的属性和属性值。

$("#d1").attr('id', 'd2');
var id = $("#d2").attr('id'); //  d2

给元素设置多个属性,就需要给attr方法传入一个JavaScript对象。

$('#d1').attr({name:'ddj', title:'道德经'});
var title = $("#d1").attr('title'); //  道德经

删除属性

removeAttr() 删除属性

删除上面设置的name属性

$('#d1').removeAttr('name');

内容操作

如何用jQuery设置和获取HTML、文本和值?

html() 获取或设置 元素内部的html代码

text() 获取或设置 元素内部的文本内容

val() 获取或设置 元素的值

如果不传入参数, 那么就获取元素内部的html代码或者文本内容。

如果传入参数,则是替换 或 赋值 的意思。

<script>
    $(document).ready(function(){
        var html_text = $("#d1").html();
        alert(html_text);   //   道可道,非常道。<span>--道德经</span>
        var text = $("#d1").text();
        alert(text);   //   道可道,非常道。--道德经
    });
</script>
<body>
    <h1 id="d1">道可道,非常道。<span>--道德经</span></h1>
</body>

传入参数

$("#d1").html('<strong>老子</strong>');	//页面只显示 老子 两字
$("#d1").text('<strong>老子</strong>');	//页面显示 <strong>老子</strong>

获取和设置value 值的相关操作

<label for="">姓名:<input type="text"></label>
<input type="button" onclick="get_name()" value="获取名字">

<script>
function get_name(){
    var name = $('input:eq(0)').val();
    alert(name);

}
</script>

val方法不仅可以操作input元素,还可以操作下拉框(select)、多选框(checkbox)和单选按钮(radiobox)。

<select id="fruits">
    <option>西瓜</option>
    <option>香蕉</option>
    <option>苹果</option>
</select>

设置默认显示为 香蕉

 $("select:eq(0)").val('香蕉');

如果option 设置了value,那就要设置为value值。

设置默认显示为 苹果

<select id="fruits">
    <option value="01">西瓜</option>
    <option value="02">香蕉</option>
    <option value="03">苹果</option>
</select>

<script>
  $("select:eq(0)").val('03');
</script>

如果希望同时选择香蕉和苹果,那么就是多选下拉框,需要给这个select标签设置一个multiple。

<select id="fruits" multiple="multiple">
    <option value="01">西瓜</option>
    <option value="02">香蕉</option>
    <option value="03">苹果</option>
</select>

需要给val 方法传入一个数组.

<script>
    $("select:eq(0)").val(['02', '03']);
</script>

遍历和寻找节点

jQuery遍历节点的操作有哪些,如何使用?

jQuery遍历节点的操作

children()

这个方法可以获取某个元素的下一代子元素,但不包括孙子辈的元素。该方法只沿着DOM树向下遍历单一层级。

parent() 查找 父节点

prev() 查找 上一个 兄弟节点

next() 查找 下一个 兄弟节点

siblings() 返回所有 兄弟元素

希望通过ul元素获取其中的5个孩子节点

<ul id="menu">
    <li>豫章故郡,洪都新府。</li>
    <li>星分翼轸,地接衡庐。</li>
    <li>襟三江而带五湖,控蛮荆而引瓯越。</li>
    <li>物华天宝,龙光射牛斗之墟;</li>
    <li>人杰地灵,徐孺下陈蕃之榻。 <strong>出自 滕王阁序</strong></li>
</ul>
<script>
    var lis = $("#menu").children();
    console.log(lis);
</script>

现在我想找到 strong 这个标签

var lis = $("#menu").children();
var b= lis.last().find('strong');
console.log(b);

我现在想通过 li 节点 找到它的父节点

var li1 = $('li:eq(0)');
var ul = li1.parent();
console.log(ul);

通过某一个节点找到它的兄弟节点

现在给每个标签设置一个id

<ul id="menu">
    <li id="a1">豫章故郡,洪都新府。</li>
    <li id="a2">星分翼轸,地接衡庐。</li>
    <li id="a3">襟三江而带五湖,控蛮荆而引瓯越。</li>
    <li id="a4">物华天宝,龙光射牛斗之墟;</li>
    <li id="a5">人杰地灵,徐孺下陈蕃之榻。 <strong>出自 滕王阁序</strong></li>
</ul>
var a3 = $("#a3");
var a2 = a3.prev();
var a4 = a3.next();
console.log(a2.html()); //星分翼轸,地接衡庐。
console.log(a4.html()); //物华天宝,龙光射牛斗之墟;

根据一个元素找到和它同一级别的所有兄弟元素。

var lis = a3.siblings();
console.log(lis);   // [li#a1, li#a2, li#a4, li#a5]

Vue

引入文件

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

数据绑定

<div id="app">
    <input type="text" v-model="message">
    <p>{{message}}</p>
</div>
<script>
    new Vue({
        el: '#app',
        data: {
            message: '小贼,哪里跑!'
        }
    })
</script>

v-model通常用于表单组件的绑定,如input和select等,

v-model与v-text的区别在于它实现的是表单组件的双向绑定,

v-model用于表单控件,其他的标签是没有用的。

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<style>
    table,tr,td {
        border: 1px solid red;
        border-collapse: collapse;
    }
    tr:last-child td{
        text-align: right;
    }
    tr:last-child td span{
        color: red;
    }
</style>

<table id="app">
    <tr>
        <th colspan="2">登录界面</th>
    </tr>
    <tr>
        <td>请输入用户名:</td>
        <td><input type="text" v-model='username'></td>
    </tr>
    <tr>
        <td>请输入密码:</td>
        <td><input type="password" v-model='pwd'></td>
    </tr>
    <tr>
        <td colspan="2">
            <span v-text="errMsg"></span>
            <input type="submit" value="登录">
        </td>
    </tr>
</table>
<script>
    var vue = new Vue({
        el: '#app',
        data: {
            username: '胡大大',
            pwd: '88888888',
            errMsg: '用户名或密码错误'
        }
    });
</script>

事件绑定

v-on监听事件

语法糖

v-on:click 简写成 @click

<table id="app">
    <tr>
        <th colspan="2">登录界面</th>
    </tr>
    <tr>
        <td>请输入用户名:</td>
        <td><input type="user" v-on:focus="handleFocus" v-on:blur="handleBlur" v-bind:style="userNameStyle" v-model='username'></td>
    </tr>
    <tr>
        <td>请输入密码:</td>
        <td><input type="password" v-model='pwd'></td>
    </tr>
    <tr>
        <td colspan="2">
            <span v-text="errMsg"></span>
            <input type="submit" value="登录">
        </td>
    </tr>
</table>


<script>
    var vue = new Vue({
        el: '#app',
        data: {
            username: '胡大大',
            pwd: '88888888',
            errMsg: '',
            userNameStyle: {}
        },
        methods: {
            handleFocus: function(){
                console.log('获得焦点!');
                this.userNameStyle = {
                    width: '50px',
                    borderColor: 'pink'
                }
            },
            handleBlur: function(){
                console.log('失去焦点!');
                this.userNameStyle = {};
            }
        }
    });
    </script>

事件冒泡

.stop 停止冒泡

.prevent 阻止提交,.prevent只对form和a标签有效

.capture 优先触发

.self 只允许元素自己触发,子元素无法触发。

.once 只触发一次

事件冒泡的概念

假设现在有这样的情况:有2个div,父div嵌套了子div;父div有一个单击事件,子div也有一个单击事件。当单击子div的时候,因为子div在父div里面,页面又不知道你到底想单击子div还是父div,那么咋办呢?结果就是先触发子div的单击事件,再触发父div的单击事件。

先初始化代码

<td colspan="2" @click="modelClick">
    <span></span>
    <input type="submit" value="登录" @click="btnClick">
</td>
var vue = new Vue({
    el: '#app',
    methods: {
        modelClick: function() {
            console.log('modelClick');
        },
        btnClick: function() {
            console.log('btnClick');
        }
    }
});

单击登录按钮,将按顺序显示

btnClick
modelClick

给 **登录 **按钮加上.stop,就不会触发 父元素的click事件了。

<td colspan="2" @click="modelClick">
    <span></span>
    <input type="submit" value="登录" @click.stop="btnClick">
</td>

单击登录按钮,效果是只会打印btnClick,不会打印modelClick

btnClick

form表单组件和a链接组件都会导致页面刷新和跳转。

如果不希望页面刷新,则可以加上.prevent以阻止这种默认的刷新操作。

注意:.prevent只对form和a标签有效。

.prevent

<td colspan="2" @click="modelClick">
    <span ></span> 
    <input type="submit" value="登录" @click.stop="btnClick">
    <a href="http://baidu.com" @click.prevent>baidu</a>
</td>       

使用.prevent后单击 baidu 将不会跳转

.capture

在外层td元素上添加这个修饰符,则会优先触发modelClick,然后触发按钮自己的单击事件,这就是一个优先级的调整

<td colspan="2" @click.capture="modelClick">
    <span ></span>
    <input type="submit" value="登录" @click="btnClick">
</td>

单击登录按钮,将按顺序显示

modelClick

btnClick

.self

事件修饰符.self的作用是:当仅单击元素本身时,只允许元素自己触发,子元素无法触发。

在外层td元素上添加这个修饰符,当单击外层td的部分(不单击按钮部分)时,就会只触发modelClick,不会触发按钮自己的单击事件,

简单了解这个修饰符即可,因为这种行为本来就是默认的。

<td colspan="2" @click.self="modelClick">
    <span ></span>
    <input type="submit" value="登录" @click="btnClick">
</td>

意外发现

当我单击 登录按钮时,只会触发btnClick,相当于使用.stop

.once

事件修饰符.once表示只触发一次

给td的单击事件加上.once修饰符,那么modelClick就只会触发一次。

<td colspan="2" @click.once="modelClick">
    <span ></span>
    <input type="submit" value="登录" @click="btnClick">
</td>

无论我单击多少次 td,控制台只打印了一次modelClick。


条件语句

v-if

v-else

v-else-if

当第一次单击“登录”按钮的时候,“登录”按钮上面的“登录”二字变成“登录中…”的字样。当后台成功返回数据或者超时后,就在页面上显示相应的信息,同时让“登录”按钮再变回原样。

<td colspan="2">
    <span v-text="errMsg"></span>
    <input v-if="!isLogining" type="submit" value="登录" @click="btnClick">
    <input v-else type="submit" value="登录中...">
</td>
        var vue = new Vue({
            el: '#app',
            data: {
                isLogining: false,
                errMsg: ""
            },
            methods: {
                btnClick: function() {
                    console.log('btnClick');
                    this.isLogining = true;
                }
            }
        });

一般这个时候,前端就会采用ajax等技术向后台发起登录请求了。

后台过一会儿,就会返回一个响应,告诉前端页面是否登录成功。

下面用延时函数模拟一下即可

错误案例

        btnClick: function() {
            console.log('btnClick');
            this.isLogining = true; // 模拟发起请求,2秒后后台返回结果
            setTimeout(function() {
                alert(this); // 这里的 this 指向 window对象
                this.errMsg = "登录失败,请检查密码是否正确!";
                this.isLogining = false;
            }, 2000);
        }

结果发现,弹窗成功弹出来了,说明setTimeout起作用了,那么就可以确定回调函数的代码出了问题。

原因是这里的 this 指向的是 window对象,而非Vue对象。

setTimeout 等价于 window.setTimeout

解决办法

因为最上面定已经把Vue对象 赋值给了vue变量。

直接使用使用vue变量,但这并不是常见的解决办法

        window.setTimeout(function() {
            vue.errMsg = "登录失败,请检查密码是否正确!";
            vue.isLogining = false;
        }, 2000);

·在能获取this的地方新定义一个that变量,将Vue对象存起来,

然后在回调函数里面调用即可,这种做法是较为普遍的。

        var that = this;
        setTimeout(function() {
            that.errMsg = "登录失败,请检查密码是否正确!";
            that.isLogining = false;
        }, 2000);

循环语句

v-for

<tr>
    <td>请选择身份:</td>
    <td>
        <select v-model="role"> 
            <option v-for="(item, index) in roleList" :value="item.value" :id="index">{{item.label}}</option>
        </select>
    </td>
</tr>
var vue = new Vue({
    el: '#app',
    data: {
        role: '1', // 默认游客
        isLogining: false,
        errMsg: "",
        roleList: [
            {value:'1', label:'我是游客'},
            {value:'2', label:'我是普通用户'},
            {value:'3', label:'我是管理员'},
        ]
    }
});

属性绑定

v-bind做属性绑定

v-bind:xxx简写成:xxx

<option v-for="(item, index) in roleList" :value="item.value" :id="index">{{item.label}}</option>

绑定之后才能接收 data里面的 数据

Vue组件开发

为什么要用组件?

如果哪天又要改动按钮的颜色,咋办?甚至需要制作一个按钮组。比如,红色的按钮用来删除,蓝色的按钮用来新增,黄色的按钮用来修改。这一系列的要求是很常见的,我们可以使用Vue的相关语法制作一套按钮组件,以完成这样的需求,同时也可以大大提升程序的健壮性和可扩展性。

组件也可以成为一种模板,你只需要照着这个模板把需要的参数传进去就能得到想要的效果。

组件定义在 components 属性中,

components属性是和data与methods平级的,代表当前Vue对象的局部组件。

        var vue = new Vue({
            el: '#app',
            data: {}, // 局部组件
            components: {}
        });

按钮的代码需要放在template 模板里面,

注意:双引号需要转义

定义一个coolBtn的组件模板

        // 局部组件
        components: {
            'coolBtn': {
                template: "<input type=\"submit\" value='提交' style=\" background-color: deeppink; color: #fff;border: none;padding: 2px 20px;border-radius: 6px;margin: 2px 6px;\">"
            }
        }

如何使用 组件模板呢?

在视图里面就可以直接调用这个组件了,调用的方式就和写普通的HTML标签一样,但是标签名要和组件名称保持一致。

错误写法:

<coolBtn></coolBtn>

这是一种驼峰式的命名法。对于这种驼峰式的命名,在调用组件的时候需要格外注意,每次要换成大写字母的地方都需要额外添加一个半字线(-),然后大写字母还是转变成小写字母,像这样:

<td colspan="2">
    <span></span>
    <cool-btn></cool-btn>
    <input v-else type="submit" value="登录中...">
</td>

组件 传参

对于一个组件来说,按钮具体显示什么文字应该交给按钮的调用者决定。

因此,需要给按钮组件添加一个name 属性。

该如何给自定义组件添加属性呢?

在具体的组件配置项中添加一个props属性就可以了,props属性的值是一个数组,数组里面存放的就是当前组件可以接收的属性名称。

        components: {
            'coolBtn': {
                props: ['name'],
                template: "<input  type=\"submit\" value='提交' style=\" background-color: deeppink; color: #fff;border: none;padding: 2px 20px;border-radius: 6px;margin: 2px 6px;\">"
            }
        }

传进来的属性名为name,这个name该如何插入template中呢?

对于input来说,它也是一个组件。我们的coolBtn组件无非就是对input组件做了一次拓展开发而已。input也是组件,那么它的value就是属性,Vue绑定属性的方式就是在左边加一个冒号,于是可以这样修改template。

:value='name'

components: {
    'coolBtn':{
        props: ['name'],
            template:"<input  type=\"submit\" :value='name' style=\" background-color: deeppink; color: #fff;border: none;padding: 2px 20px;border-radius: 6px;margin: 2px 6px;\">"
    }
}

最后我们给coolBtn传递name属性值就行

<cool-btn name="登录"></cool-btn>

注意,我们给coolBtn组件添加的属性是普通的属性,而不是Vue的属性绑定。

如果在name的左侧加一对冒号,又会发生什么呢?

<cool-btn :name="登录"></cool-btn>

结果报错了!

原因:一旦我们写成了:name,那么就说明我们希望给name属性做Vue的属性绑定,它会默认在data里面寻找是不是有一个叫作 登录 的数据变量。

如果找不到,就直接给你报错。

如果我们只是想要传递一个字符串进去,就可以直接用不带冒号的name,或者这样写:

<cool-btn :name= "'登录'"> </cool-btn>

依然是用了Vue的数据绑定,但我们只是直接写了一个直接量进去而已。

组件事件

我们自定义的按钮组件也是一个Vue组件,那么就完全可以给它添加methods属性,然后把它对应的单击事件写进去就行了。

注意:这个methods是coolBtn里面的methods,不是外面Vue对象的methods,不要混淆。

最后 再给组件模板添加单击事件。

        // 局部组件
        components: {
            'coolBtn': {
                props: ['name'],
                template: "<input @click='defaultClick' type=\"submit\" :value='name' style=\" background-color: deeppink; color: #fff;border: none;padding: 2px 20px;border-radius: 6px;margin: 2px 6px;\">",
                methods: {
                    defaultClick: function() {
                        alert("我被点击了哦!");
                    }
                }
            }
        }

测试一下,单击“登录”按钮,应该能看到一个默认的弹窗。

但是这样不行啊,因为在更多的时候组件是被封装好的,根本不涉及业务逻 辑。

我们希望调用coolBtn组件制作一个登录按钮,但是登录的逻辑是外面Vue组件需要做的事情,不能写在按钮组件里面。也就是说,我们希望在子组件的defaultClick方法里面调用外面的某个方法。调用外面的什么方法呢?比如在登录的时候随便写了一个方法叫作login,

var vue = new Vue({
    el: '#app',
    data: {
        role: '1', // 默认游客
        isLogining: false,
        errMsg: "",
        roleList: [{
            value: '1',
            label: '我是游客'
        }, {
            value: '2',
            label: '我是普通用户'
        }, {
            value: '3',
            label: '我是管理员'
        }, ]
    },
    methods: {
        login: function() {
            alert("登录成功!");
        }
    },
    // 局部组件
    components: {
        'coolBtn': {
            props: ['name'],
            template: "<input @click='defaultClick' type=\"submit\" :value='name' style=\" background-color: deeppink; color: #fff;border: none;padding: 2px 20px;border-radius: 6px;margin: 2px 6px;\">",
            methods: {
                defaultClick: function() {
                    alert("我被点击了哦!");
                }
            }
        }
    }
});

子组件怎么知道将来需要调用一个叫作login的方法呢?

这就需要遵循一个原则 — 约定大于配置

就是封装好coolBtn组件,因为是按钮,所以肯定要被单击,单击就要调用单击事件。那么不妨就规定好, 当coolBtn组件触发默认的defaultClick方法时,就触发调用者的某个方法,

        defaultClick: function() {
            alert("我被点击了哦!"); // 发送 btn-click事件。
            this.$emit('btn-click');
        }

this.$emit('btn-click') 这句代码的含义是触发该组件绑定的btn- click事件所对应的来自于父组件的方法。

$emit 相当于 发送事件, 事件名则是自定义的,

这句话有点绕,再加一点代码吧,这样一来,coolBtn的调用就要这么写了

 <cool-btn name="登录" @btn-click="login"></cool-btn>

在调用coolBtn的时候新添加了一个btn-click事件,

因为对于父页面来说,cool-btn就是一个组件而已,当然可以给一个组件绑定事件。

只不过这个事件叫作btn-click,不是我们熟悉的click、blur、focus等。

btn-click是子组件约定的,只要触发btn-click事件,子组件就会相应地触发内部的defaultClick。

btn-click就是一个中转站,我们最终的目的是单击coolBtn内部的input按钮。

但input按钮在coolBtn的内部被封装起来了,我们没办法直接控制它。

一个优秀的组件必须给外部提供完整的访问权限。

一个好的组件还必须拥有良好的扩展性和选择性。

我们还可以设置按钮的颜色,传递一个color属性进去即可。但是调用者还是更喜欢直接得到几个选项自己选择。

先设置几个默认的颜色

        .primary {
            background: #400eff;
            ;
        }
        
        .danger {
            background: #f56c6c;
            ;
        }
        
        .success {
            background: #67c23a;
            ;
        }
        
        .warning {
            background: #9e9a95;
            ;
        }

然后给组件增加一个type属性

props: ['name', 'type'],

同样,在template里面也要增加这个class选项。

template: "<input @click='defaultClick' :class='type' type=\"submit\" :value='name' style=\" color: #fff;border: none;padding: 2px 20px;border-radius: 6px;margin: 2px 6px;\">",

这样一来,组件的调用者只需要传入一个type属性,即可同步更新template里面的class的值。

完整代码如下:

// 局部组件
components: {
    'coolBtn': {
        props: ['name', 'type'],
        template: "<input @click='defaultClick' :class='type' type=\"submit\" :value='name' style=\" color: #fff;border: none;padding: 2px 20px;border-radius: 6px;margin: 2px 6px;\">",
        methods: {
            defaultClick: function() {
                alert("我被点击了哦!"); // 发送 btn-click事件。
                this.$emit('btn-click');
            }
        },
        created: function() {
            // alert('按钮初始化');
            if (!this.type) {
                this.type = 'primary';
            }
        }
    }
}

调用组件按钮

<cool-btn name="登录":type="'success'" @btn-click="login"></cool-btn>

全局组件component

最后提一句,这个组件是一个局部组件,就是说这个组件因为是在当前页面的Vue对象里面定义的,所以只有在这个页面才能使用。

其他页面要想使用这个组件,只能把组件的源代码重新写一遍。很明显,这样非常不利于组件的维护和更新,万一对组件进行了修改或者扩展,就需要到每一个使用该组件的页面中修改组件对应的源代码,这是非常麻烦的。所以,我们可以将这个按钮组件升级为全局组件。

注意:定义全局过滤器或全局组件一定要写在 创建Vue对象 的前面!

// 全局组件
Vue.component('coolBtn', {
    props: ['name', 'type'],
    template: "<input @click='defaultClick' :class='type' type=\"submit\" :value='name' style=\" color: #fff;border: none;padding: 2px 20px;border-radius: 6px;margin: 2px 6px;\">",
    methods: {
        defaultClick: function() {
            alert("我被点击了哦!"); // 发送 btn-click事件。
            this.$emit('btn-click');
        }
    },
    created: function() {
        // alert('按钮初始化');
        if (!this.type) {
            this.type = 'primary';
        }
    }
});

相当于在总的Vue大对象上添加了一个组件,我们不妨新建一个coolBtn.js,然后把这些代码复制进去。

注意,因为这个全局组件需要依赖Vue的库文件,所以必须先引入Vue的核心文件,再引入coolBtn.js。

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script><script src="./js/coolBtn.js"></script>

计算属性computed

Vue的计算属性是指在某些时候需要在页面上动态计算一些值,这些计算的过程可以封装在计算属性的定义里面。

举个栗子:

计算折扣

<div id="app">
     请输入商品价格: <input type="number" v-model="price"><br>
      请输入商品折扣: 
      <select v-model="discount" style="width: 100%;">
        <option v-for="item, index in discounts" :value="item.value">{{item.label}}</option>
    </select><br>
     <span style="color: #f00;">成交价:{{price * discount}} 元</span>
</div>
<script>
    var app = new Vue({
        el: '#app',
        data: {
            price: 0,
            discount: 0.9,
            discounts: [{
                value: '0.9',
                label: '9折'
            }, {
                value: '0.8',
                label: '8折'
            }, {
                value: '0.7',
                label: '7折'
            }, {
                value: '0.65',
                label: '65折'
            }]
        }
    });
</script>

但是,如果计算过程非常复杂该怎么办呢?

如果当天是11月11号,那么就需要搞促销,规定商品满200元减50元,满400元减150元。这样一来,要想再全部写在一个双大括号里面就显得有些困难了。

这个时候,我们可以使用Vue提供的计算属性完成这个任务。

计算属性也是Vue对象里面专有的一个模块, 它和methods、data是平级的。

computed: {
    payment: function(){
        var today = new Date();
        // 先获取今天的日期
        var month = today.getMonth() + 1;
        var day = today.getDate();

        // 模拟今天是 11.11
        month = day = 11;

        // 是否为双十一,获得返利金额
        var rebate = 0;
        if(month == 11 && day ==11){
            if(this.price >= 400){
                rebate = 150;
            }else if (this.price >= 200){
                rebate = 50;
            }
        }
        // 最终的金额
        return this.price * this.discount - rebate;
    }
}

payment是计算属性的名称,页面上对应的地方也要改过来。

<span style="color: #f00;">成交价:{{payment}} 元</span>

监听属性watch

Vue可以监听属性的变化,属性既可以是data里面定义的数据变量,也可以是自己的props模块中定义的数据。

业务场景为:有一个进度条,它的旁边有一个“增加进度”按钮,当进度达到不同的百分比时,就在进度条上方显示不同的提示。

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="./js/coolBtn.js"></script>
<style>
    .progress {
        display: inline-block;
        background-color: red;
        width: 0px;
        height: 18px;
    }
    .primary{
        background: #400eff;;
    }
    .danger{
        background: #f56c6c;;
    }
    .success{
        background: #67c23a;;
    }
    .warning{
        background: #9e9a95;;
    }
</style>

<div id="app">
    <h2>{{msg}}({{progressNum}} %)</h2>
    <span class="progress" :style="spanStyle"></span>
    <br>
    <cool-btn name="增加进度":type="'warning'" @btn-click="addProgress"></cool-btn>
</div>
    var app = new Vue({
        el: '#app',
        data: {
            msg: "加油,我看好你哦!",
            progressNum: 0,
            spanStyle: {}
        },
        methods: {
            // 这个函数 实际上只是希望改变进度的数值,
            // 至于进度条的颜色和提示怎样变化,并不应该交给addProgress处理。
            addProgress: function() {
                this.progressNum += 10;
                if (this.progressNum >= 100) {
                    this.progressNum = 100;
                    this.msg = '大功告成,辛苦了!';
                    return;
                }
                var background = 'red';
                if (this.progressNum >= 80) {
                    background = 'green';
                    this.msg = '就差一点点了!';
                } else if (this.progressNum >= 50) {
                    background = 'orange';
                    this.msg = '有改善咯!';
                }
                this.spanStyle = {
                    width: this.progressNum + 'px',
                    background: background
                }
            }
        }
    });

这种做法存在一个问题,那就是addProgress方法显得太冗余了。

在加载进度条的过程中,我们可以通过点击按钮不断改变进度,但我们实际上只是希望改变进度的数值,至于进度条的颜色和提示怎样变化,并不应该交给addProgress处理。

我们应该让addProgress方法保持干净整洁,至于那些锦上添花的代码,我们可以将其放到监听属性里面,

var app = new Vue({
    el: '#app',
    data: {
        msg: "加油,我看好你哦!",
        progressNum: 0,
        spanStyle: {}
    },
    watch: {
        progressNum: function(val) {
            console.log('我在监听 progressNum 属性,现在它的值为:' + val);

            if (this.progressNum >= 100) {
                this.progressNum = 100;
                this.msg = '大功告成,辛苦了!';
                return;
            }
            var background = 'red';
            if (this.progressNum >= 80) {
                background = 'green';
                this.msg = '就差一点点了!';
            } else if (this.progressNum >= 50) {
                background = 'orange';
                this.msg = '有改善咯!';
            }
            this.spanStyle = {
                width: this.progressNum + 'px',
                background: background
            }
        }
    },
    methods: {
        // 这个函数 实际上只是希望改变进度的数值,
        // 至于进度条的颜色和提示怎样变化,并不应该交给addProgress处理。
        addProgress: function() {
            this.progressNum += 10;
        }
    }
});

Vue有一个监听模块是和data、methods平级的。progressNum是需要被监听的属性,赋值一个函数,参数val为当前监听到的新值。

过滤器 filters

在很多时候,我们拿到的数据在展示到页面上之前还需要经过一些特殊的处理。

比如,我们希望一个单词的首字母为大写,如果页面上有多个地方都有这样的需求,我们就得在每一个地方都进行数据处理,或者编写一个格式化的函数,在每个地方都调用一遍。

但是,这种有针对性的数据处理,不应该使用这种偏业务的解决方式,而应该设计一个过滤器,以专门应对这些情况。

我们用纯JavaScript的方式新建一个日期对象,我们希望得到的日期格式为yyyy-mm-dd,可以使用过滤器实现,

<div id="app">
    今天是:{{today}}
</div>
var app = new Vue({
    el: '#app',
    data: {
        today: new Date(),
    }
});

页面显示

今天是:Mon Nov 29 2021 22:09:16 GMT+0800 (中国标准时间)

过滤器就是在Vue对象中添加一个filters模块,

<script>
var app = new Vue({
    el: '#app',
    data: {
        today: new Date(),
    },
    filters:{
        dateFormat: function(val){
            return val.getFullYear() + '-' + (val.getMonth()+1) + '-' + val.getDate();
        }
    }
});
</script>

dateFormat其实就是一个函数,不过因为它被写在filters模块里面, 所以也称之为过滤器。

过滤器定义好了以后,再回到页面,哪个数据需要这个过滤器进行过滤,就在竖线后面加上该数据。

<div id="app">
   今天是:{{today|dateFormat}}
</div>

过滤器还可以叠加使用,比如希望给日期添加一些样式。

boxStyle: function(val){
    return '<span style="display: inline-block;padding: 6px 10px;background: pink;">'+ val +'</span>'
}

因为这个过滤器添加了CSS样式,所以在引用的地方就不能用双大括号了,而应该用v-html进行绑定。

双大括号是不会解析HTML文档内容的,不管你输入什么,都会被当作文本处理,显示的就是text文本。

<div id="app">今天是:{{today|dateFormat|boxStyle}}</div>

页面显示

今天是:<span style="display: inline-block;padding: 6px 10px;background: pink;">2021-11-29</span>

下面的写法已经废弃!

Vue从2.0版本开始就不再支持在v-html中使用过滤器了。

<div id="app">
    <span v-html="today|dateFormat|boxStyle"></span>
</div>

解决方法是把过滤器当成一个普通方法进行调用。在定义的Vue对象中,所有过滤器都会被挂在$options.filters对象上,

$options.filters
<span v-html="$options.filters.boxStyle($options.filters.dateFormat(today))"></span>

不过,像这种用过滤器的方法添加样式的情况毕竟少,所以一般用双大括号就行了。

全局过滤器

所谓的全局过滤器,就是直接绑定在全局Vue对象上的过滤器,任何页面只要引入了它,就都可以使用了。

注意:定义全局过滤器或全局组件一定要写在 创建Vue对象 的前面!

<script>
// 全局过滤器
Vue.filter('dateFormat',  function(val){
        return val.getFullYear() + '-' + (val.getMonth()+1) + '-' + val.getDate();
    });

Vue.filter('boxStyle', function(val){
        return '<span style="display: inline-block;padding: 6px 10px;background: pink;">'+ val +'</span>'
    });

var app = new Vue({
el: '#app',
data: {
    today: new Date(),
}
});
</script>

Vue-cli

先将 Node.js安装好

Node.js使得JavaScript脚本也可以编写后台程序代码,不 过,你想要学习Vue-cli,也无须知道那么多,你只要学会使用npm和webpack就行了

npm发布模块

定义好的组件别人想用怎么办?

所有人弄一个公共的仓库,再把这个组件放到仓库里面,谁想要用,就去仓库里面取便是了。

npm就是起到了这个作用,在npm仓库中的都叫作模块。

所有模块都发布在www.npmjs.com

在发布之前,需要到npm.js上注册一个账号,才有权限发布自己定义的模块。

新建一个文件夹 :npm- study

在这个文件夹中新建一个yeXiaoFan.js

function hello(){
    alert("大家好啊,我是叶小凡!");
}
console.log('yeXiaoFan.js successfully loaded!');
exports.hello = hello;

exports的意思是导出,我们把hello这个函数作为导出模块的一部分。

接下来,再生成一个文件— package.json

package.json就是这个组件的打包信息。

直接使用命令 npm init 就可以生成这个打包配置文件。

生成package.json的时候,需要输入一些默认值, name默认是文件夹的名字,我们这次将它改成 huyiqun 吧,其他几项都默认就好。

package.json

{
 "name": "huyiqun",	// 模块名字
 "version": "1.0.0",	//版本号
 "description": "",	//模块的描述
 "main": "yeXiaoFan.js",	//模块的启动文件
 "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1" 
 },	//npm允许在package.json文件里面使用scripts字段定义脚本命令。
 "author": "",
 "license": "ISC"
}

一般一个模块发布后,都会在scripts这边写上几个脚本。比如运行整个项目,我们会这样写。

"dev": "node yeXiaoFan"

使用 npm run dev 运行,意思就是直接运行这个入口文件

npm run dev表示让计算机运行dev命令,最终运行的脚本是node yeXiaoFan。

接下来就是发布这个模块,

用npm adduser 命令添加我们刚才注册的用户。

用户添加好了,接下来就可以发布模块了,

命令是npm publish

发布完成后,你可以在npm网站上搜索huyiqun,应该就能看到了。

npm安装模块

刚才我们成功发布了huyiqun模块,现在再来看看如何引入这个模块。

新建一个文件夹:mynpm

在这个目录执行命令 npm install huyiqun

运行结果:

自动生成了一个package-lock.json 和 node_modules文件夹。

在node_modules 文件夹里就能看到我们发布的huyiqun模块了。

在mynpm文件夹中创建一个main.js作为项目的入口文件

var hyq = require("huyiqun");
hyq.hello();

require是导入模块的命令,简单理解为函数调用即可,参数是一个字符串,就是需要引入的模块名字。

运行一下,输入 node main.js

报错了,提示alert没有被定义,这是因为现在启动js脚本直接使用了node命令,而不是在浏览器里面运行,自然是没有alert函数的。

这就需要单独搭建一个服务了,比如使用Vue-cli搭建一个服务。

注意:npm一般运行速度比较慢,实际开发中应使用国内的服务器地址,使用cnpm下载模块。安装cnpm的方法非常简单,只需要执行以下命令就可以了。

npm install -g cnpm --registry=https://registry.npm.taobao.org

安装后,下次启动时就不是npm install了,而是cnpm install。这点区别使速度快了不止一个档次。

Vue-cli搭建项目

简单来说,Vue-cli就是进行Vue组件化开发的一个脚手架。

啥是脚手架啊?

脚手架,你就理解为一个项目模板吧。一个前端项目, 肯定会有JS、HTML、CSS文件的。JS文件放哪里好呢,你可能会新建一个叫作js的文件夹,里面放的都是js文件。再比如,针对图片资源, 你可以新建一个叫pictures的文件夹。

但是,每个人有不同的想法,这种命名约定就不尽相同了。可是实际上,这些资源的归类确实是必不可少的。脚手架可以帮你生成一个项目模板,在什么文件夹里面放什么资源都是定义好的。这样就方便开发了,不用总是想着定义资源文件夹的名字。别人拿到你的项目后也方便了不少,因为大家都知道这些文件夹里面放着哪些文件,直接找便是了。并且,针对一些常见的配置,脚手架也会帮你设置好,你只要专心写业务代码就可以了。

安装Vue-cli

创建一个空的Vue-cli文件夹, 然后在这个目录中运行下面的命令

cnpm install -g @vue/cli@3.0.1

-g的意思就是全局安装,全局安装后,在任何其他地方都可以使用Vuecli脚手架。

仅有Vue-cli还不够,我们还需要安装它的一个原型工具。

cnpm install -g @vue/cli-service-global@3.0.1

版本号一定要保持一致,不然会出现一些莫名其妙的错误。

上面两个工具都安装好后,就开始正式创建一个项目吧。

下面采用Vue的方式直接创建。这个项目的名字,就叫作vue-project吧!

vue create vue-project

注意,刚开始会有一些询问,直接敲回车就行,不要在那傻等着。

项目初始文件

public里面有两个文件,一个是favicon.ico, 这是项目的图标。另一个是index.html,这是项目的初始页面。

这个页面有一个根节点,是一个div,id为app。

<div id= "app"> </div>

我们的项目可以说全部渲染在这里面。

Public文件夹是默认生成 的,这个文件夹主要用来放置一些公共资源。

接下来是src文件夹,这里面的资源可就多了。

一般而言,项目中的图片资源、js资源、css资源都没有统一的定论该放在哪里。脚手架的好处就是告诉你这些静态资源该放在哪里,如果你是用Vue脚手架生成的项目,那么就是放在assets文件夹里面。

然后是components文件夹,这里存放项目中的一些公共组件,以方便具体的页面调用。

现在,这里面有一个自动生成的HelloWorld.vue文件,这是一个扩展名为vue的文件。

vue文件是做什么的?

vue文件就是一种容纳了js、css和html的文件格式。

看一看HelloWorld.vue里面是什么吧!默认生成的代码有很多,为了方便起见,我们改写一下里面的代码。

<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  props: {
    msg: String
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<!-- 添加“scoped”属性,以限制CSS只用于此组件 -->

<style scoped>
h3 {
  margin: 40px 0 0;
}
</style>

template标签是组件的页面代码,script就是写JavaScript代码的地方,最后是style标签,很明显,这里是写CSS代码的地方。

props是定义组件的属性,有一个属性是msg,类型是string。

整个文件就是一个组件。接下来,我们看一下App.vue。

<template> 
<div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
</div>
</template>
<script>
    import HelloWorld from './components/HelloWorld.vue'
    export default {
        name: 'app',
        components: {
            HelloWorld  }
    }
</script>

<style>...省略CSS代码</style>

import 是引入

App.vue又是一个新的组件,并且引入了HelloWorld.vue,还传递了参数。

<HelloWorld msg="Welcome to Your Vue.js App"/>

再看看main.js

import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
    render: h => h(App),
}).$mount('#app')

main.js是项目的入口文件

在代码的最后,它创建了全局的Vue对象,渲染的目标是‘#app’,也就是刚才看到index.html里面的那个div。

最后三行代码运用了ES6语法中的箭头函数,改写一下就是这样。

new Vue({
    render: function(h){
        return h(App)  
    }
}).$mount('#app')

箭头函数就是普通函数的一种简便写法吧,h是render函数接收的参数,h本身是一个回调函数,所以能够打括号去执行!

用Vue-cli创建的项目默认支持ES6的语法

启动一下这个项目吧,启动的方式已经写在package.json里面了

"scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
},

启动命令叫作 serve

直接 npm run serve 就可以启动项目了。

项目默认监听的是8080号端口。

至此,一个Vue项目完全启动成功了!


ES6语法

let 和 const

JavaScript是没有块级作用域的,这会导致很多问题,比如下面的代码。

var i = 10;
for(var i=1; i<6; i++){

}
console.log(i); // 6

答案是6,因为JavaScript没有块级作用域,这就会导致for循环中定义的i变量覆盖了全局变量i。

ES6推荐使用let定义变量,这样就可以实现块级作用域的效果了,内部的变量不会影响全局。

var i = 10;
for(let i=1; i<6; i++){
};
console.log(i); //10

ES6还可以使用const声明一个只读的常量,一旦声明,该常量的值就不能再被改变,比如下面的代码。

const NAME1 = '张三';
NAME1 = '李四';
console.log(NAME1);

报错:

Uncaught TypeError: "NAME1" is read-only

因为常量是只读的,不允许修改,强行修改只会导致程序报错。可以说,const的引入提升了JavaScript的安全性。

变量的解构赋值

变量的解构赋值主要分为 数组的解构赋值对象的解构赋值

一般的用法是,假如我有一个对象, 里面有若干方法:

let Person = {
  eat: function(){
    console.log("我在吃饭");
  },
  sleep: function(){
    console.log("我在睡觉");
  }
}

假如这个对象在其他页面,或者在其他地方被引用到了,则要想获取里面的这两个方法,一般的写法如下:

let eat = Person.eat;
let sleep = Person.sleep;
// 调用
eat();
sleep();

这是常规的做法

解构赋值的作用是把获取对象中的方法以及赋值给对应变量的过程一次性做完。

let {eat, sleep} = Person;

一般来说,我们在用let定义变量的时候,是在let的右边写一个变量,然后等于某个具体的值。

这种写法的好处是直接定义了变量的集合,同时程序会智能地在等号右侧寻找匹配项。

Person里面的确有eat方法和sleep方法,左侧的大括号中也确实存在eat变量和sleep变量,真可谓情投意合。

现在,我们多了一个新的需求,就是获取Person对象的name属性。于是,我们这样写代码:

let {eat, sleep, name} = Person;
console.log(name);	//undefined

由于Person对象中并没有name属性,因此name就没有对应的匹配项赋值了。显而易见,name的值打印出来后肯定是undefined。

如果我们不希望name为空,则可以设置一个默认值,将代码变更一下,就成了这样。

let {eat, sleep, name="一个神秘的杠精"} = Person;
console.log(name);	//一个神秘的杠精

虽然看起来有点怪,但是却意外地好用,很多项目里面都会这么做。

比如一个修改页面,我们用Vue将其封装成一个组件。这个组件在两种情况下会打开,第一种情况是新增的时候,第二种情况是修改的时候。如果是修改,则会允许组件接收一个data参数,这个data里面包含所有需要修改的信息,只需要让页面加载这些信息就行了,然后保存修改即可。如果是新增,则data就是一个空,我们再去渲染它的时候,不就有问题了吗?所以,这个时候可以用解构赋值中的默认值给data赋一个值。

通过一个简单的函数说明这种业务场景:

什么参数都没有传递,但是因为在参数这里使用了默认值,于是就采用{name:'jack',sex:'1', salary:'20000'}这个默认对象了。

function initEditInfo(data = {name:'hyq',sex:'1',salary:'20000'}){
    let {name, sex, salary} = data;
    console.log(name);
    console.log(sex);
    console.log(salary);
}
initEditInfo();

在实际项目中,解构赋值可以带来方便,同时解构赋值还有很多其他高级用法,但是笔者不推荐使用过多解构赋值,因为这会导致代码过于精简和抽象,不利于后期的维护。如果后面的项目被一个初来乍到的新人接管,则会给他带来很多困扰。

字符串升级

ES6对传统字符串进行了一次大规模的改进,主要体现在两个方面。

第一个改进是允许字符串直接通过for循环的方式遍历。

注意:这里用的是of,而不是in。如果用in,则获取的是每个字符对应的下标。

let str= "咱又不是学不会!";
for(let s of str){
    console.log(s);
}

另一个改进是允许用反引号进行一些简化的字符串定义。

模板字符串相当于加强版的字符串,它除了可以作为普通字符串使用,还可以用来定义多行字符串,以及在字符串中加入变量和表达式。

let name = "张三";
let sayHi = `你好啊,${name}一起来happy啊!`;
console.log(sayHi); // 你好啊,张三// 一起来happy啊!

这一改进支持换行和变量注入,这些特性使得JavaScript字符串更加灵活。

除了这两个改进之外,ES6字符串还提供了一些非常好用的API 方法,如字符串补全。

假设现在有一个需求是依次打印0~99,但是不足2位的数字需要用0左补齐,以往的做法是用if进行判断,如果小于10, 就在左边加一个0。

for(let i = 0; i<100; i++){
    console.log(i.toString().padStart(2,'0'));
}

padStart:返回新的字符串,表示用参数字符串从头部(左侧)补全原字符串。

padEnd:返回新的字符串,表示用参数字符串从尾部(右侧)补全原字符串。

以上两个方法可以接收两个参数,第一个参数是指定生成的字符串的最小长度,第二个参数是用来补全的字符串。如果没有指定第二个参数,则默认用空格填充。

另外,值得一提的是,字符串允许被当作数组一样使用。换句话说,你可以用下标的方式获取字符串中某个位置的字符

Proxy代理

在支持Proxy的浏览器环境中,Proxy是一个全局对象,它可以被直接使用。

Proxy(target,handler)是一个构造函数,target是被代理的对象,handlder是声明了各类代理操作的对象,最终返回一个代理对象。

外界每次通过代理对象访问target对象的属性时,就会经过handler对象,

从这个流程来看,代理对象很类似middleware(中间件)。

那么,Proxy可以拦截什么操作呢?

最常见的就是get(读取)、set(修改)对象属性等操作。

简单来说,Proxy的作用就是允许我们声明一个代理,对某个对象进行一些特殊的访问拦截。

一个Proxy对象由两个部分组成:target、handler。

在通过Proxy构造函数生成实例对象时,需要提供这两个参 数。target即目标对象,handler是一个对象,声明了代理target的指定行为。

比如,我们现在有如下一个对象:

let obj = {
  name: 'hyq',
  age:  18
}

当我们希望给obj赋值时,往往会直接这样做。

obj.age = '28'

这样不是说不行,但是会出现问题,因为obj的age属性分明希望得到一个number,但是我们却赋值了一个string。

我们就希望在给对象赋值的时候限制一下类型。

思路大概是这样的:给obj生成一个代理,obj就不会直接暴露给外面了。

如果你要操作obj,就和代理说,代理会帮obj做一个简单的筛选。

let obj = {
  name: 'hyq',
  age:  18
};

let objProxy = new Proxy(obj, {
  // 对象的属性值被 修改 了会进入set
  set(target, key, value){
    if(key == "age" && typeof(value) != 'number'){
      throw new Error(`修改 ${key} 为 "${value}": 不是数字类型!`);
    }
    return target[key] = value;
  },
  // 对象的属性 被获取 了会进入get
  get(target, key, receiver){
    console.log(`当前代理的对象为:${target},某个地方正在获取对象的 ${key} 属性, `);
    return  receiver;
  }
});
objProxy.age = '22'; //Uncaught Error: 修改 age 为 "22": 不是数字类型!
objProxy.age = 22;
console.log(objProxy.name);
console.log(obj);

get(target, key, receiver)中的receiver是返回代理对象,而不是返回obj对象。

强化后的数组

ES6对诸多数据类型都进行了强化,自然不可能少得了数组。下面对一些常用的方法进行详细介绍。

快速构建新数组

Array.of 方法可以将参数中的所有值作为元素而形成数组,参数值可以是不同类型,如果参数为空时,则返回空数组。

let arr = Array.of(11,'22');
console.log(arr); // [11, '22']

Array.from方法

这个方法可以将类数组对象可迭代对象转化为数组。

类数组对象就是一种可以遍历的对象,只要对象有length属性,而且有诸如0、1、2、3这样的属性,那么它就可以被称为类数组。

下面的对象就可以称为类数组

let listDate = {
  0:'keke',
  1:'jerry',
  length:2
}

但是它毕竟不是数组,不方便进行某些操作,如push。我们可以用from方法将它转换为数组。

let listDate1 = Array.from(listDate);
console.log(listDate1); // ['keke', 'jerry']
console.log(listDate);  //{0: 'keke', 1: 'jerry', length: 2}

这样就是货真价实的数组啦,from方法还可以接收第二个参数,就是一个map回调函数,用于对每个元素进行处理,放入数组的是处理后的元素。

listDate1 = Array.from(listDate1, function(item){
  return item + '---';
});
console.log(listDate1); // ['keke---', 'jerry---']

新的数组方法

find:查找数组中符合条件的元素,若有多个符合条件的元素,则返回第一个元素。

let testArr = [2,5,6,10,20];
let arr2 = testArr.find(function(item){
  return item>8;
})
console.log(arr2); // 10

findIndex:查找数组中符合条件的元素索引,若有多个符合条件的元素,则返回第一个元素索引。

let testArr = [2,5,6,10,20];
let arr2 = testArr.findIndex(function(item){
  return item>8;
})
console.log(arr2); // 3

fill:将一定范围索引的数组元素内容填充为单个指定的值。

let testArr = [2,5,6,10,20];
testArr.fill(1);
console.log(testArr); // [1, 1, 1, 1, 1]

array.fill(value, start, end)

参数 描述
value 必需。填充的值。
start 可选。开始填充位置。
end 可选。停止填充位置 (默认为 array.length)
let testArr = [2,5,6,10,20];
testArr.fill(1,2,4);
console.log(testArr); // [2, 5, 1, 1, 20]

copyWithin:将一定范围索引的数组元素修改为此数组另一指定范围索引的元素。

let testArr = [2,5,6,10,20];
testArr.copyWithin(2);
console.log(testArr); //  [2, 5, 2, 5, 6]

array.copyWithin(target, start, end)

参数 描述
target 必需。复制到指定目标索引位置。
start 可选。元素复制的起始位置。
end 可选。停止复制的索引位置 (默认为 array.length)。如果为负值,表示倒数。
let testArr = [2,5,6,10,20,30,40,50];
testArr.copyWithin(2,0,-2)
// 0,-2 代表[2, 5, 6, 10, 20, 30],从下标为2的地方开始替换
console.log(testArr); //[2, 5, 2, 5, 6, 10, 20, 30]

ES6 提供三个新的方法 —— entries(),keys()和values() 。

它们都返回一个遍历器对象,可以用for…of循环进行遍历,区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。

entries:遍历

var fruits = ["Banana", "Orange", "Apple", "Mango"];
var x = fruits.entries();
console.log(x.next().value);  [0, 'Banana']
console.log(x.next().value);   [1, 'Orange']


var obj ={ name: '小明', age: 20, sex: '男' };

console.log(Object.entries(obj)[0]);  ['name', '小明']

console.log(Object.entries(obj)[2]);   ['sex', '男']

keys:遍历键名。

values:遍历键值。

let arr = ['a', 'b', 'c']
for (let index of arr.keys()) {
console.log(index);
}
// 0
// 1
// 2
for (let item of arr.values()) {
console.log(item);
}
// 'a'
// 'b'
// 'c'
for (let [index, item] of arr.entries()) {
console.log(index, item);
}
// 0 'a'
// 1 'b'
// 2 'c'

上面是对数组的方法

Object的keys()和values()还有entries()方法

let obj = {
    name:"张三",
    sex:"男",
    age:20
}
 
for ( let key of Object.keys(obj)){
    console.log(key)
} 
// name
// sex
// age
for ( let val of Object.values(obj)){
    console.log(val)
}
// 张三
// 男
// 20
 
for ( let val of Object.entries(obj)){
    console.log(val)
}
// (2) ["name", "张三"]
// (2) ["sex", "男"]
// (2) ["age", 20]

for ( let [key,val] of Object.entries(obj)){
    console.log(key,val)
} 
// name 张三
// sex 男
// age 20

数组复制

在以前的传统项目中,如果要复制一个数组,大多采用slice方法,现在可以用“…”的方式快速复制一个数组。

let listData = ['a', 'b', 'c'];
let listData1 = listData;
let slice_listData = listData.slice(0,2);
let newListData = [...listData];

listData[0] = 1;

console.log(slice_listData); // ['a', 'b']
console.log(newListData); // ['a', 'b', 'c']
console.log(listData1); // [1, 'b', 'c']

强化后的函数

ES6对函数也做了很多强化或者说是简化,尤其著名的就是箭头函数了。

以前的常规做法是用关键字function定义一个函数,而现在ES6的语法允许省略function关键字,直接用一个箭头声明一个函数。

let sayhi = (name)=>{
  console.log(`你好!${name}`);
}
sayhi('hyq'); //你好!hyq

原先的参数列表保留,还是放在一对圆括号里面,但是如果你只有一个参数,则可以省略圆括号。

let sayhi = name =>{
  console.log(`你好!${name}`);
}
sayhi('hyq'); //你好!hyq

如果是没有参数的函数,又该怎么办呢?当然,打一个小括号是没有任何问题的。

let sayhi = () =>{
  console.log(`你好!`);
}
sayhi('hyq'); //你好!

但是有些人喜欢直接给出一个下画线,相当于有一个参数,只是这个参数永远不会被使用罢了。于是,为了降低这个参数的存在感,就用一个下画线代替它。

let person = {
  getNmae: _=>{
    return "hyq";
  }
}
console.log(person.getNmae());  //hyq

上面的代码很好地诠释了这一点,但是一般也不会这么做,不管有没有参数,如果这个函数是在某个对象里面的,更推荐直接简写成如下形式。

let person = {
  getNmae(){
    return "hyq";
  }
}
console.log(person.getNmae());  //hyq

注意:上面的方式并非箭头函数,而是普通函数的简写。

现在再看下一个问题,person对象里面有一个getName方法,这个方法用于返回对象本身的name属性,下面用this引用一下。

let person = {
  name:'hyq',
  getName(){
    return this.name;
  }
}
console.log(person.getName()); //hyq

这样写是没有问题的,相当于如下形式。

let person = {
  name:'hyq',
  getName: function(){
    return this.name;
  }
}

但是,如果换成箭头函数会怎样呢?

window.name = 'test';

let person = {
  name:'hyq',
  getName: _=>{
    return this.name;
  }
}
console.log(person.getName()); //test

原来,箭头函数体中的 this对象 是定义函数时的对象,而不是使用函数时的对象。

也就是说,定义getName函数的时候,这个函数并不知道自己在person对象中,所以里面的this依然指向window对象。

而如果用function定义函数,则里面的this会在代码的实际运行过程中动态绑定,因此指向的就是person对象。

所以,使用箭头函数时,这一点要尤其注意。 永远记住,箭头函数中this的指向是定义时所在的作用域,而不是执行时的作用域。

在刚才的例子中, getName方法中的this就指向定义它的作用域,而不是getName方法的调用者。

可能这么讲还是比较抽象,请记住一个小窍门:

只要使用了箭头函数,就不要管这个函数本身了,在外面寻找最近的大括号,然后看这个大括号处在怎样的环境中,this就指向它!

那么,距离getName方法最近的大括号就是person对象的大括号, person对象处于全局作用域里面,那么this就指向window对象。现在, 我们把这个例子稍微改一改。

window.name = 'test';

let person = {
  name:'hyq',
  getName: _=>{
    return this.name;
  },
  sayHi:_=>{
      setTimeout(_=>{
          console.log(`你好!${this.name}`);
      }, 1000);
  }
}
person.sayHi(); //你好!test

按照刚才的技巧,距离this最近的是setTimeout的参数,也就是那个回调函数的大括号。

注意:这是一个箭头函数,还要继续往上找,于是找到了sayHi方法的大括号。

可是sayHi 方法本身又是一个箭头函数,于是这次寻找还是不算数,还要继续往上冒泡,最终又找到了person对象,它是window的,于是this指向 window。

那就干脆只要是用到this的地方通通不用箭头函数了!

window.name = 'test';

let person = {
  name:'hyq',
  getName: _=>{
    return this.name;
  },
  sayHi(){
      setTimeout(function(){
          console.log(`你好!${this.name}`);
      }, 1000);
  }
}
person.sayHi(); //你好!test

这次没有用箭头函数,那么之前寻找this的办法就是有效的。

this永远指向函数的调用者,因为这个this是在setTimeout函数里面的,它的调用者还是window,并不是person。

在执行sayHi方法的时候,可以临时保存一下this的指向,这样就可以在setTimeout中访问到person对象了。

但是这样的写法很烦琐,而且看起来很奇怪。

let person = {
  name:'hyq',
  getName: _=>{
    return this.name;
  },
  sayHi(){
      let that = this;
      setTimeout(function(){
          console.log(`你好!${that.name}`);
      }, 1000);
  }
}
person.sayHi(); //你好!hyq

结合箭头函数的特性,我们可以稍加改造。

window.name = 'test';

let person = {
  name:'hyq',
  getName: _=>{
    return this.name;
  },
  sayHi(){
      setTimeout(_=>{
          console.log(`你好!${this.name}`);
      }, 1000);
  }
}
person.sayHi(); //你好!hyq

只要你用了箭头函数,就不要管这个函数本身了,从外面寻找最近的大括号,于是我们找到了sayHi方法(这一次sayHi方法没有使用箭头函数)。

sayHi方法是在person对象里面的,所以这次this不会再往上冒泡了,而是定格在这个大括号中,于是this的指向和sayHi方法一样,都是person对象,终于可以拿到name了!

最后补充一句,如果函数体仅仅是一个简单的return语句,那么函数体的大括号和return关键字都可以省略。

let test_obj = {
    sayHi: name=>`你好!${name}`,
}
console.log(test_obj.sayHi('hyq')); //你好!hyq

更加灵活多变的对象

上面已经讲解了对象的解构赋值,下面补充一点,ES6中对象的写法还是有点不同的。

let name = "道德经";
let obj ={
    name: name,
}
console.log(obj);   //{name: '道德经'}

name: name怎么看都觉得别扭不是吗?于是,ES6允许我们将其简写成name,只要左右两边的名字是一样的,就可以简写。

let obj ={name}

我们知道JavaScript中的对象的属性值是一个字符串,比如这个name: name其实是'name': name,只是我们一般喜欢省略那个引号。

那么,如果这个属性名称是一个变量又该怎么办?

很明显,这样的写法是错误的,key会被当作一个字符串进行处理。

let key = 'name';
let obj = {
    key: '杠精'
}
console.log(obj);   //{key: '杠精'}

还记得我们是如何调用对象的属性的吗?

可以用“.”,也可以 用“[]”,这里也是一样的,用“[]”就可以解决问题。

let key = 'name';
let obj = {
    [key]: '杠精'
}
console.log(obj);   //{name: '杠精'}

promise对象和async函数

最简单的需求:

制作一个定时器,2s过后,获取一个字符串,然后在控制台输出这个字符串。

let gift = null;
setTimeout(_=>{
    gift = "吸星大法";
},2000);
console.log(`我学会了${gift}`);

代码中的问题很明显,这是一个异步操作,需要在2s后才会执行gift变量的赋值语句,所以,还没有等gift有值,语句就已经输出了。

一个最容易想到的解决办法就是把输出语句放到setTimeout里面,这样做是绝对正确的,

但是如果异步操作有很多,就会出现层层嵌套的问题。

let gift = null;
setTimeout(_=>{
    gift = "吸星大法";
    console.log(`我学会了${gift}`);
},2000);

我们也可以使用Proxy代理观察gift值的变化。

let gift = null;
let obj = {gift}

let objProxy = new Proxy(obj,{
    set(target, key, value){
        if(key == "gift"){
            target[key] = value;
            console.log(`我学会了${target[key]}`);
            return true;
        }
    },
    get(target, key){
        return target[key];
    }
});

setTimeout(_=>{
    objProxy.gift = "吸星大法";
},2000);

这样就可以通过观察者和代理模式监听gift的变化,从而完成异步监听的效果。

但是,很明显,这样写代码太复杂了。

这里,我们可以使用一个新的对象——promise。

promise是异步编程的一种解决方案。从语法上说,promise是一个对象,使用它可以获取异步操作的消息。

在ES6中,promise被列为正式规范,统一了用法,原生提供了promise对象。

let gift = null;
new Promise((resolve, reject)=>{
    setTimeout(_=>{
        gift = "吸星大法";
        resolve(gift);
    },2000);
}).then(gift=>{
    console.log(`我学会了${gift}`);
});

promise对象在创建的时候分别接收了2个内部的函数钩子: resolve(已完成)和reject(已拒绝)。

promise对象就是一种承诺,在必要的时候,它会告知外部本次异步操作已经完成或者拒绝,如果是完成,则触发后面的then方法;如果是拒绝,则触发catch方法。

比如,我们把代码改造为有20%的概率可以获得吸星大法,有80%的概率什么也得不到,即表示获取异常(reject)。

let gift = null;
new Promise((resolve, reject)=>{
    setTimeout(_=>{
        if(Math.random() < 0.2){
            gift = "吸星大法";
            resolve(gift);
        }else{
            gift = "个屁";
            reject(gift);
        }
    },2000);
}).then(gift=>{
    console.log(`我学会了${gift}`);
}).catch(gift=>{
    console.log(`我学会了${gift}`);
});

这便是用promise对象处理异步操作的思路。

看到这里,可能有的人又会纳闷,虽然用promise对象处理起来更加优雅,但是我们不是还要在对应的then方法或者catch方法里面进行操作吗?

能不能直接给我resolve里面的值,不要逼着我去then里面处理数据?

办法是有的,这就需要配合async函数和await关键字了。

let getGiftAsync = _=>{
    return new Promise((resolve, reject)=>{
                setTimeout(_=>{
                    if(Math.random() < 0.2){
                        let gift = "吸星大法";
                        resolve(gift);
                    }else{
                        let gift = "个屁";
                        reject(gift);
                    }
                },2000);
            });
};

async function executeAsyncFunc(){
    let gift = await getGiftAsync();
    console.log(`我学会了${gift}`);
};
executeAsyncFunc();

之前,代码的困境是无法脱离then和catch的回调函数,导致代码还是有些冗余,其实我们只是希望得到resolve里面的参数而已,下面简单介绍上面的代码:

getGiftAsync函数返回了一个promise对象,逻辑和刚才一样,

然后在executeAsyncFunc函数的左边加上了async,代表这是一个异步处理函数。

只有加上了async关键字的函数,内部才可以使用await关键字。

async是ES7才提供的与异步操作有关的关键字,async函数执行时,如果遇到await就会先暂停执行,等到触发的异步操作完成后,才会恢复async函数的执行并返回解析值。

posted @ 2021-11-22 21:02  牛逼如我也要多思考  阅读(992)  评论(0编辑  收藏  举报