整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

一文详解JavaScript的变量,超详细,建议收藏!

文首发自「慕课网」,想了解更多IT干货内容,程序员圈内热闻,欢迎关注!

作者|慕课网精英讲师 然冬

变量就是存放一些内容容器

对于初学者,理解变量是重要的一环。

从分析变量这个名词,可以知道他是一个可以改变的量,这里的量就是代表某一种

在 JavaScript 中,变量就是一个用来存放值的容器,并且可以对容器中的值做修改。

每个变量都有唯一的变量名,使用变量名来区分变量。

1. 声明变量

在 JavaScript 中使用var关键字来声明变量。

var 存放数字用的变量 = 996;

console.log(存放数字用的变量); // 输出:996
代码块123

上述这段代码就是申明了一个名为存放数字用的变量的变量,并且将它的值设为996

使用 console.log,括号内放置变量名,即可将变量的值输出在控制台。

其中 // 后面的内容为注释,代码执行过程中会被忽略。

虽然使用中文作为变量名在 chrome 浏览器下没有报错,但是还是不建议使用。

常规场景中不会有使用中文名作为变量的情况

所以上述例子中的变量名不可取。

var number = 996;

console.log(number); // 输出:996
代码块123

存放数字用的变量修改成 number ,执行结果是一样的。

2. 赋值

给变量设置值的操作称为赋值操作。

2.1 最简单的赋值操作

var result = 0;

console.log(result); // 输出:0
代码块123

这是一个最简单的赋值操作,直接将值赋给变量。

通常只有一个等号出现的情况下就存在赋值操作。

2.2 将计算结果赋值给变量

var result = 2 + 3;

console.log(result); // 输出:5
代码块123

这也是一个赋值操作,只不过等号右边的 2 + 3 会被计算出结果(计算的方式和小学开始学习的自然数学一样),再赋给变量 result

将上面这个例子做一个简单的改写:

2.3 让变量也参与计算

var number1 = 2;
var number2 = 3;

var result = number1 + number2; // 2 + 3

console.log(result); // 输出:5
代码块123456

原本 2 + 3 这部分也可以被变量所代替,参与计算的就是变量中的值。

2.4 改变变量的值

var string = '今天加班?';

console.log(string); // 输出:今天加班?

string = '福报!';

console.log(string); // 输出:福报!
代码块1234567

注意:

这里赋给变量的值和之前有点不一样,是中文文字。

当需要用变量存放一些“字”的时候,就需要用单引号'或者双引号"将需要存放的字包裹。

通常单个字会称之为字符,多个字的时候称为字符串

这里做个了解,具体的会在后续数据类型章节详细展开讨论。

这段代码运行后可以在控制台观察到有两个输出,分别对应变量的值。

代码很简单,先声明了一个叫 string 的变量,并赋值字符串今天加班?并输出,随后修改了他的值,重新赋值了字符串福报!

这是变量最重要的一个特性:可变

3. 变量的命名规范

在 JavaScript 中变量名存在一定规范,所有变量名必须符合这些规范,否则程序无法执行。

3.1 变量名必须使用字母、下划线(_)、美元符号($)开头

尽管之前的例子有用到中文作为变量名,但是是不推荐的。

// 不会报错但是不推荐
var 数字 = 1;
// 错误
var 1number = 1;
// 错误
var number@a = 1;
// 错误
var num+aa = 2;

//下面是正确的方式
var number1 = 1;
var _number = 1;
var $number = 1;
代码块12345678910111213

以上是一些简单的示例,可以根据规则自己在控制台尝试寻找规则。

3.2 变量对大小写敏感

// 这是两个不同的变量
var firstName = 'Hello';
var firstname = 'hello';
代码块123

以上是两个不同的变量,在 JavaScript 中变量是对大小写敏感的。

两个变量名即便字母是相同的,但是大小写不同,就不能算做一个变量。

3.3 无法使用关键字作为变量名

关键字就是指一些已经被 JavaScript 预定义或者保留下来的内容,如声明变量用的关键字 var 就不能作为变量名。

var var = 1; // Uncaught SyntaxError: Unexpected token 'var'
代码块1

上面这段代码尝试着将 var 作为变量,到控制台运行是会报错的。

4. 合理规范的变量名

刚开始学习的读者,现在去深究如何命名一个变量还有些尚早,因为结合了具体的需求场景才能体会到一个好的变量名的重要性。可以先在此做个了解。

对于变量名,除了上面提到的变量命名的规范,最需要注意的就是给变量起一个有意义的名字。

如求和:

var num1 = 1;
var num2 = 2;
var num3 = 3;
var num4 = 4;

var count = num1 + num2 + num3 + num4;
代码块123456

其中numnumber的缩写,是很常用的一种缩写。

count则是总数,表示求和的结果。

如果将上述例子做如下修改:

var a = 1;
var b = 2;
var c = 3;
var d = 4;

var e = a + b + c + d;
代码块123456

缺少了有意义的变量名就比较难看出代码具体在做什么。当然这段代码本身意义就不大,场景太过简单。

刚才提到的缩写,其实也是要注意的一点,缩写上一定要使用通用的缩写,如含有fn表示一个功能或者函数,avg 表示平均值,pwd 表示密码,i18n 为国际化。

这些缩写比较通用,大部分开发者都可以看得懂。随着编码经验的增加,会在他人代码里见到大量的缩写,从而累积到自己的大脑的缩写库中。

最后需要注意的一点是业务中尽量不要含有中文拼音或中文拼音的缩写,排开鄙视链的原因,最大的问题是会让变量名变得冗长难懂。

以上内容在写 demo 或者测试功能的时候可以不需要考虑,写 demo 等大部分情况是为了验证自己的猜想。

// 不合理的变量名
var ln = 'World'; // last name
var zs = 0; // 总数
var jinNianDeNianShouRu = 1999999999; // 今年的年收入
代码块1234

以上是针对变量名的意义展开的讨论。

还有需要注意的是变量命名的格式,大部分前端程序员会使用驼峰命名法,也就是第一个字母小写,后续如果有新的单词来进行构成,单词的第一个字符都大写。

如:

var firstName = 'Hello';

var lastName = 'world';

var createAt = 1577895179196;

var userInfo = '用户信息'; // Info => Information

var isPaidUser = '是否付费用户';
代码块123456789

可以见到上面的变量,从构成变量名的第二个单词开始,首字母都是大写,这就是驼峰命名的格式,本 Wiki 所有变量名使用的就是这种格式。

当然还有大驼峰,就是第一个字母也大写。

除此之外最常用的还有使用下划线分隔变量,如 user_info,还有按功能来划分的变量名,如使用匈牙利命名法,这里不再做展开。

5. 有关变量的其他知识

5.1 变量的默认值

变量在声明的时候,如果没有赋值,则变量就会有一个默认值 undefined

var total;

console.log(total); // 输出:undefined
代码块123

undefined 是一种是数据类型,具体内容可以参考数据类型章节。

5.2 同时声明多个变量

使用一个 var 关键字就可以直接声明多个变量。

var num1 = 0, num2 = 1;

// 通常会换行,方便阅读代码
var num3 = 2,
    num4 = 3,
    num5 = 4,
    num6,
    num7 = 6;
代码块12345678

在一个变量声明后,使用逗号分隔,紧接着声明下一个变量即可。

通常使用一个 var 声明多个变量的时候也会换行,方便后续阅读,并保持代码格式上的整洁清晰,防止一行过长。

5.3 变量在 window 上

在最外层声明的变量(不包括 ES6 模块的情况),实际上是变成了 window 对象的一个属性。

var number = 996;

console.log(number); // 输出:996
console.log(window.number); // 输出:996
代码块1234

上述代码执行后输出的两个内容是一样的,都为 996。有关 window 对象的内容可以参考 BOM 章节。

细心的读者应该会注意到最外层这个条件,因为变量还有可能声明在函数之中,函数有自己独立的作用域,通常在函数中使用 var 关键字声明的变量,只在函数中有效。

至于为什么可以省略 window 直接访问到变量,可以参考作用域链章节。

5.4 不使用 var 关键字声明的变量

假如不使用 var 关键字,直接创建变量并赋值:

total = 10;

console.log(total); // 输出:10
代码块123

在控制台运行后会发现其实并没有报错,输出的结果也正常。

在非ES6模块中,这样创建的变量和使用 var 创建的变量除了不能提前使用之外,没有其他大的区别,会被直接作为 window 对象的属性,成为全局变量。

即便是在函数或者其他存在块级作用域的环境中,这样声明的变量也会作为全局变量。

5.5 连续赋值

var a = b = 1;
代码块1

假如把上面这行代码拆开来可以理解成是这样的:

b = 1;
var a = b;
代码块12

看似没什么问题,许多开发者也会用这种方式同时声明多个变量,但如果在函数或者独立的作用域中,b 就会成为全局变量,造成全局命名空间的污染。

5.6 重复声明变量

按照之前说的,变量在声明的时候如果没有赋值,则会是 undefined,这个规则在重复声明的情况下不适用。

var num = 1;
var num;

console.log(num); // 输出:1
代码块1234

观察上面这个例子输出的结果,可以发现变量 num 的值并没有改变。

但是如果重新声明的同时做赋值操作,值就会改变。

var num = 1;
var num = 3;

console.log(num); // 输出:3
代码块1234

这个例子输出的结果,就是再次声明并赋值后的值。

5.7 提前使用变量

console.log(number); // 输出:undefined

var number = 1;
代码块123

这个例子先输出了 number 的值,再声明并对其进行赋值。

代码并没有报错,但如果没有第二行声明,只输出 number

console.log(number); // Uncaught ReferenceError: number is not defined
代码块1

这样子会爆出变量未定义的错误,说明变量是可以被提前使用,只是没有值,或者说是 undefined 默认值。

具体原因可以参考执行上下文章节。

这里简单的解释可以理解成,在浏览器执行的时候,会把代码调整成如下样子:

var number;

console.log(number); // 这个时候 number 还没有被赋值,所以输出 undefined

number = 1;
代码块12345

5.8 常量

常量就是定义并赋值后再也不能修改的量,通常一些不会改变的量,如配置、物理值等会声明为常量,在 ES6 之前是没有提供常量这一特性的。

但是根据常量自身的特性,定义赋值后不能被修改,就可以通过一些方式来模拟常量。

第一种就是采用约定的形式,通常常量都是大写,不同单词之间用下划线分隔。

var PI = 3.1415926535;

var DB_ACCOUNT = 'root';
var DB_PASSWORD = 'root';
代码块1234

这种方式定义的常量本质上还是变量,值还是可以修改的,但因为命名格式采用国际惯例,一眼就能看出是常量,不会对其修改。

这种方式是最简单的方式,但不安全。

第二种方式就是利用对象下属性的描述来控制可写性,将对象的属性设置为只读。

var CONFIG = {};

Object.defineProperty(CONFIG, 'DB_ACCOUNT', {
  value: 'root',
  writable: false,
});

console.log(CONFIG.DB_ACCOUNT); // 输出:root

CONFIG.DB_ACCOUNT = 'guest';

console.log(CONFIG.DB_ACCOUNT); // 因为不可被改写,所以输出:root
代码块123456789101112

这种方式将常量都放在一个对象下,通过Object.defineProperty定义属性,设定其writablefalse,就可以防止被改写。

但有一个问题,CONFIG自身这个对象可能被修改。

换一个思路,既然在最外层声明的变量是放在window上的,那可以用这个方式往 window上挂不可改写的属性。

Object.defineProperty(window, 'DB_ACCOUNT', {
  value: 'root',
  writable: false,
});

console.log(DB_ACCOUNT); // 输出:root

DB_ACCOUNT = 'guest';

console.log(DB_ACCOUNT); // 因为不可被改写,所以输出:root
代码块12345678910

通常情况下 window 对象是不可被修改的,这样常量的安全系数就变得非常高,但缺点是可能性较差,通过一点修改可以提升可读性。

var define = function(name, value) {
  Object.defineProperty(window, name, {
    value: value,
    writable: false,
  });
};

define('DB_ACCOUNT', 'root');
define('DB_PASSWORD', 'root');
代码块123456789

只要约定好使用 define 函数定义的都为常量即可。

还有两种方式,就是结合Object.sealObject.freeze的特性来声明常量。

前者可以使对象不能再被扩充,但是所有属性还需要再手动设置不可写,后者可以让对象不能扩充,属性也不能修改。

这里对这两个原生方法不再做过多描述,有兴趣可以查阅相关 API 资料。

6. 小结

变量就是存放值的容器。

变量名存在一些命名规则:

  • 变量名必须使用字母下划线(_)美元符号($)开头;
  • 变量对大小写敏感;
  • 无法使用关键字作为变量名。

同时起变量名的时候需要有意义,靠近上下文场景。

欢迎关注「慕课网」,发现更多IT圈优质内容,分享干货知识,帮助你成为更好的程序员!

接:https://juejin.im/post/5c6ad9fde51d453c356e37d1

一、变量类型

1.JS 的数据类型分类

根据 JavaScript 中的变量类型传递方式,分为基本数据类型和引用数据类型。其中基本数据类型包括Undefined、Null、Boolean、Number、String、Symbol (ES6新增,表示独一无二的值),而引用数据类型统称为Object对象,主要包括对象、数组和函数。在参数传递方式上,基本类型是按值传递,引用类型是按共享传递

题目:基本类型和引用类型的区别

基本类型和引用类型存储于内存的位置不同,基本类型直接存储在栈中,而引用类型的对象存储在堆中,与此同时,在栈中存储了指针,而这个指针指向正是堆中实体的起始位置。下面通过一个小题目,来看下两者的主要区别:

// 基本类型
var a = 10
var b = a
b = 20
console.log(a) // 10
console.log(b) // 20

上述代码中,a b都是值类型,两者分别修改赋值,相互之间没有任何影响。再看引用类型的例子:

// 引用类型
var a = {x: 10, y: 20}
var b = a
b.x = 100
b.y = 200
console.log(a) // {x: 100, y: 200}
console.log(b) // {x: 100, y: 200}

上述代码中,a b都是引用类型。在执行了b = a之后,修改b的属性值,a的也跟着变化。因为a和b都是引用类型,指向了同一个内存地址,即两者引用的是同一个值,因此b修改属性时,a的值随之改动

2.数据类型的判断

1)typeof

typeof返回一个表示数据类型的字符串,返回结果包括:number、boolean、string、symbol、object、undefined、function等7种数据类型,但不能判断null、array等

2)instanceof

instanceof 是用来判断A是否为B的实例,表达式为:A instanceof B,如果A是B的实例,则返回true,否则返回false。instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性,但它不能检测null 和 undefined

3)严格运算符===

只能用于判断null和undefined,因为这两种类型的值都是唯一的

4)constructor

constructor作用和instanceof非常相似。但constructor检测 Object与instanceof不一样,还可以处理基本数据类型的检测。 不过函数的 constructor 是不稳定的,这个主要体现在把类的原型进行重写,在重写的过程中很有可能出现把之前的constructor给覆盖了,这样检测出来的结果就是不准确的。

5)Object.prototype.toString.call()

Object.prototype.toString.call() 是最准确最常用的方式

3.浅拷贝与深拷贝

浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存

浅拷贝的实现方式(详见浅拷贝与深拷贝):

  • Object.assign():需注意的是目标对象只有一层的时候,是深拷贝
  • Array.prototype.concat()
  • Array.prototype.slice()

深拷贝就是在拷贝数据的时候,将数据的所有引用结构都拷贝一份。简单的说就是,在内存中存在两个数据结构完全相同又相互独立的数据,将引用型类型进行复制,而不是只复制其引用关系。

深拷贝的实现方式:

  • 热门的函数库lodash,也有提供_.cloneDeep用来做深拷贝
  • jquery 提供一个$.extend可以用来做深拷贝
  • JSON.parse(JSON.stringify())
  • 手写递归方法

递归实现深拷贝的原理:要拷贝一个数据,我们肯定要去遍历它的属性,如果这个对象的属性仍是对象,继续使用这个方法,如此往复。

二、作用域和闭包

1.执行上下文和执行栈

执行上下文就是当前 JavaScript 代码被解析和执行时所在环境的抽象概念, JavaScript 中运行任何的代码都是在执行上下文中运行。 执行上下文的生命周期包括三个阶段:创建阶段→执行阶段→回收阶段,我们重点介绍创建阶段。

创建阶段(当函数被调用,但未执行任何其内部代码之前)会做以下三件事:

  • 创建变量对象:首先初始化函数的参数arguments,提升函数声明和变量声明。
  • 创建作用域链:下文会介绍
  • 确定this指向:下文会介绍

这是因为当函数执行的时候,首先会形成一个新的私有的作用域,然后依次按照如下的步骤执行:

  • 如果有形参,先给形参赋值
  • 进行私有作用域中的预解释,函数声明优先级比变量声明高,最后后者会被前者所覆盖,但是可以重新赋值
  • 私有作用域中的代码从上到下执行

函数多了,就有多个函数执行上下文,每次调用函数创建一个新的执行上下文,那如何管理创建的那么多执行上下文呢?

JavaScript 引擎创建了执行栈来管理执行上下文。可以把执行栈认为是一个存储函数调用的栈结构,遵循先进后出的原则

  • 从上面的流程图,我们需要记住几个关键点:JavaScript执行在单线程上,所有的代码都是排队执行。
  • 一开始浏览器执行全局的代码时,首先创建全局的执行上下文,压入执行栈的顶部。
  • 每当进入一个函数的执行就会创建函数的执行上下文,并且把它压入执行栈的顶部。当前函数执行完成后,当前函数的执行上下文出栈,并等待垃圾回收。
  • 浏览器的JS执行引擎总是访问栈顶的执行上下文。
  • 全局上下文只有唯一的一个,它在浏览器关闭时出栈。

2.作用域与作用域链

ES6 到来JavaScript 有全局作用域、函数作用域和块级作用域(ES6新增)。我们可以这样理解:作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。 在介绍作用域链之前,先要了解下自由变量,如下代码中,console.log(a)要得到a变量,但是在当前的作用域中没有定义a(可对比一下b)。当前作用域没有定义的变量,这成为 自由变量。

var a = 100
function fn() {
 var b = 200
 console.log(a) // 这里的a在这里就是一个自由变量
 console.log(b)
}
fn()

自由变量的值如何得到 —— 向父级作用域(创建该函数的那个父级作用域)寻找。如果父级也没呢?再一层一层向上寻找,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是作用域链 。

function F1() {
 var a = 100
 return function () {
 console.log(a)
 }
}
function F2(f1) {
 var a = 200
 console.log(f1())
}
var f1 = F1()
F2(f1) // 100 

上述代码中,自由变量a的值,从函数F1中查找而不是F2,这是因为当自由变量从作用域链中去寻找,依据的是函数定义时的作用域链,而不是函数执行时。

3.闭包是什么

闭包这个概念也是JavaScript中比较抽象的概念,我个人理解,闭包是就是函数中的函数(其他语言不能这样),里面的函数可以访问外面函数的变量,外面的变量的是这个内部函数的一部分。

闭包的作用:

  • 使用闭包可以访问函数中的变量。
  • 可以使变量长期保存在内存中,生命周期比较长

闭包不能滥用,否则会导致内存泄露,影响网页的性能。闭包使用完了后,要立即释放资源,将引用变量指向null。

闭包主要有两个应用场景:

  • 函数作为参数传递(见作用域部分例子)
  • 函数作为返回值(如下例)
function outer() {
 var num = 0 //内部变量
 return function add() {
 //通过return返回add函数,就可以在outer函数外访问了。
 num++ //内部函数有引用,作为add函数的一部分了
 console.log(num)
 }
}
var func1 = outer() //
func1() //实际上是调用add函数, 输出1
func1() //输出2
var func2 = outer()
func2() // 输出1
func2() // 输出2

4.this全面解析

先搞明白一个很重要的概念 —— this的值是在执行的时候才能确认,定义的时候不能确认! 为什么呢 —— 因为this是执行上下文环境的一部分,而执行上下文需要在代码执行之前确定,而不是定义的时候。看如下例子:

// 情况1
function foo() {
 console.log(this.a) //1
}
var a = 1
foo()
// 情况2
function fn(){
 console.log(this);
}
var obj={fn:fn};
obj.fn(); //this->obj
// 情况3
function CreateJsPerson(name,age){
//this是当前类的一个实例p1
this.name=name; //=>p1.name=name
this.age=age; //=>p1.age=age
}
var p1=new CreateJsPerson("尹华芝",48);
// 情况4
function add(c, d){
 return this.a + this.b + c + d;
}
var o = {a:1, b:3};
add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16
add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34
// 情况5
<button id="btn1">箭头函数this</button>
<script type="text/javascript"> 
 let btn1 = document.getElementById('btn1');
 let obj = {
 name: 'kobe',
 age: 39,
 getName: function () {
 btn1.onclick = () => {
 console.log(this);//obj
 };
 }
 };
 obj.getName();
</script>

接下来我们逐一解释上面几种情况

  • 对于直接调用 foo 来说,不管 foo 函数被放在了什么地方,this 一定是 window
  • 对于 obj.foo() 来说,我们只需要记住,谁调用了函数,谁就是 this,所以在这个场景下 foo 函数中的 this 就是 obj 对象
  • 在构造函数模式中,类中(函数体中)出现的this.xxx=xxx中的this是当前类的一个实例
  • call、apply和bind:this 是第一个参数
  • 箭头函数this指向:箭头函数没有自己的this,看其外层的是否有函数,如果有,外层函数的this就是内部箭头函数的this,如果没有,则this是window。

三、异步

1.同步 vs 异步

同步,我的理解是一种线性执行的方式,执行的流程不能跨越。比如说话后在吃饭,吃完饭后在看手机,必须等待上一件事完了,才执行后面的事情。

异步,是一种并行处理的方式,不必等待一个程序执行完,可以执行其它的任务。比方说一个人边吃饭,边看手机,边说话,就是异步处理的方式。在程序中异步处理的结果通常使用回调函数来处理结果。

// 同步
console.log(100)
alert(200);
console.log(300) //100 200 300

// 异步
console.log(100) 
setTimeout(function(){ 
 console.log(200) 
}) 
console.log(300) //100 300 200 

2.异步和单线程

JS 需要异步的根本原因是 JS 是单线程运行的,即在同一时间只能做一件事,不能“一心二用”。为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

一个 Ajax 请求由于网络比较慢,请求需要 5 秒钟。如果是同步,这 5 秒钟页面就卡死在这里啥也干不了了。异步的话,就好很多了,5 秒等待就等待了,其他事情不耽误做,至于那 5 秒钟等待是网速太慢,不是因为 JS 的原因。

3.前端异步的场景

前端使用异步的场景

  • 定时任务:setTimeout,setInterval
  • 网络请求:ajax请求,动态加载
  • 事件绑定

4.Event Loop

一个完整的 Event Loop 过程,可以概括为以下阶段:

  • 一开始执行栈空,我们可以把执行栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。micro 队列空,macro 队列里有且只有一个 script 脚本(整体代码)。
  • 全局上下文(script 标签)被推入执行栈,同步代码执行。在执行的过程中,会判断是同步任务还是异步任务,通过对一些接口的调用,可以产生新的 macro-task 与 micro-task,它们会分别被推入各自的任务队列里。同步代码执行完了,script 脚本会被移出 macro 队列,这个过程本质上是队列的 macro-task 的执行和出队的过程。
  • 上一步我们出队的是一个 macro-task,这一步我们处理的是 micro-task。但需要注意的是:当 macro-task 出队时,任务是一个一个执行的;而 micro-task 出队时,任务是一队一队执行的。因此,我们处理 micro 队列这一步,会逐个执行队列中的任务并把它出队,直到队列被清空。
  • 执行渲染操作,更新界面
  • 检查是否存在 Web worker 任务,如果有,则对其进行处理
  • 上述过程循环往复,直到两个队列都清空

接下来我们看道例子来介绍上面流程:

Promise.resolve().then(()=>{
 console.log('Promise1') 
 setTimeout(()=>{
 console.log('setTimeout2')
 },0)
})
setTimeout(()=>{
 console.log('setTimeout1')
 Promise.resolve().then(()=>{
 console.log('Promise2') 
 })
},0)

最后输出结果是Promise1,setTimeout1,Promise2,setTimeout2

  • 一开始执行栈的同步任务(这属于宏任务)执行完毕,会去查看是否有微任务队列,上题中存在(有且只有一个),然后执行微任务队列中的所有任务输出Promise1,同时会生成一个宏任务 setTimeout2
  • 然后去查看宏任务队列,宏任务 setTimeout1 在 setTimeout2 之前,先执行宏任务 setTimeout1,输出 setTimeout1
  • 在执行宏任务setTimeout1时会生成微任务Promise2 ,放入微任务队列中,接着先去清空微任务队列中的所有任务,输出 Promise2
  • 清空完微任务队列中的所有任务后,就又会去宏任务队列取一个,这回执行的是 setTimeout2

四、原型链与继承

1.原型和原型链

原型:在JavaScript中原型是一个prototype对象,用于表示类型之间的关系。

原型链:JavaScript万物都是对象,对象和对象之间也有关系,并不是孤立存在的。对象之间的继承关系,在JavaScript中是通过prototype对象指向父类对象,直到指向Object对象为止,这样就形成了一个原型指向的链条,专业术语称之为原型链。

var Person = function() {
 this.age = 18
 this.name = '匿名'
}
var Student = function() {}
//创建继承关系,父类实例作为子类原型
Student.prototype = new Person()
var s1 = new Student()
console.log(s1)

原型关系图:

当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的__proto__(即它的构造函数的prototype)中寻找。如果一直找到最上层都没有找到,那么就宣告失败,返回undefined。最上层是什么 —— Object.prototype.__proto__ === null

2.继承

介绍几种常见继承方式(如需了解更多,请点击JavaScript常见的六种继承方式):

  • 原型链+借用构造函数的组合继承
function Parent(value) {
 this.val = value
}
Parent.prototype.getValue = function() {
 console.log(this.val)
}
function Child(value) {
 Parent.call(this, value)
}
Child.prototype = new Parent()
const child = new Child(1)
child.getValue() // 1
child instanceof Parent // true

以上继承的方式核心是在子类的构造函数中通过 Parent.call(this) 继承父类的属性,然后改变子类的原型为 new Parent() 来继承父类的函数。

这种继承方式优点在于构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数,但是也存在一个缺点就是在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费。

  • 寄生组合继承:这种继承方式对上一种组合继承进行了优化
function Parent(value) {
 this.val = value
}
Parent.prototype.getValue = function() {
 console.log(this.val)
}
function Child(value) {
 Parent.call(this, value)
}
Child.prototype = Object.create(Parent.prototype, {
 constructor: {
 value: Child,
 enumerable: false,
 writable: true,
 configurable: true
 }
})
const child = new Child(1)
child.getValue() // 1
child instanceof Parent // true

以上继承实现的核心就是将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数。

  • ES6中class 的继承

ES6中引入了class关键字,class可以通过extends关键字实现继承,还可以通过static关键字定义类的静态方法,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。需要注意的是,class关键字只是原型的语法糖,JavaScript继承仍然是基于原型实现的

class Parent {
 constructor(value) {
 this.val = value
 }
 getValue() {
 console.log(this.val)
 }
}
class Child extends Parent {
 constructor(value) {
 super(value)
 this.val = value
 }
}
let child = new Child(1)
child.getValue() // 1
child instanceof Parent // true

class 实现继承的核心在于使用 extends 表明继承自哪个父类,并且在子类构造函数中必须调用 super,因为这段代码可以看成 Parent.call(this, value)。

五、DOM操作与BOM操作

1.DOM操作

当网页被加载时,浏览器会创建页面的文档对象模型(DOM),我们可以认为 DOM 就是 JS 能识别的 HTML 结构,一个普通的 JS 对象或者数组。接下来我们介绍常见DOM操作:

2.DOM事件模型和事件流

DOM事件模型分为捕获和冒泡。一个事件发生后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段。

(1)捕获阶段:事件从window对象自上而下向目标节点传播的阶段;

(2)目标阶段:真正的目标节点正在处理事件的阶段;

(3)冒泡阶段:事件从目标节点自下而上向window对象传播的阶段。

DOM事件捕获的具体流程

捕获是从上到下,事件先从window对象,然后再到document(对象),然后是html标签(通过document.documentElement获取html标签),然后是body标签(通过document.body获取body标签),然后按照普通的html结构一层一层往下传,最后到达目标元素。

接下来我们看个事件冒泡的例子:

如何阻止冒泡?

通过event.stopPropagation() 方法阻止事件冒泡到父元素,阻止任何父事件处理程序被执行。 我们可以在上例中inner元素的click事件上,添加event.stopPropagation()这句话后,就阻止了父事件的执行,最后只打印了'inner'。

 inner.onclick = function(ev) {
 console.log('inner')
 ev.stopPropagation()
}

3.事件代理(事件委托)

由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理。

我们设定一种场景,如下代码,一个<div>中包含了若干个<a>,而且还能继续增加。那如何快捷方便地为所有<a>绑定事件呢?

<div id="div1">
 <a href="#">a1</a>
 <a href="#">a2</a>
 <a href="#">a3</a>
 <a href="#">a4</a>
</div>
<button>点击增加一个 a 标签</button>

如果给每个<a>标签一一都绑定一个事件,那对于内存消耗是非常大的。借助事件代理,我们只需要给父容器div绑定方法即可,这样不管点击的是哪一个后代元素,都会根据冒泡传播的传递机制,把父容器的click行为触发,然后把对应的方法执行,根据事件源,我们可以知道点击的是谁,从而完成不同的事。

最后,使用代理的优点如下:

  • 使代码简洁
  • 减少浏览器的内存占用

4.BOM 操作

BOM(浏览器对象模型)是浏览器本身的一些信息的设置和获取,例如获取浏览器的宽度、高度,设置让浏览器跳转到哪个地址。

  • window.screen对象:包含有关用户屏幕的信息
  • window.location对象:用于获得当前页面的地址(URL),并把浏览器重定向到新的页面
  • window.history对象:浏览历史的前进后退等
  • window.navigator对象:常常用来获取浏览器信息、是否移动端访问等等

获取屏幕的宽度和高度

console.log(screen.width)
console.log(screen.height)

获取网址、协议、path、参数、hash 等

// 例如当前网址是 https://juejin.im/timeline/frontend?a=10&b=10#some
console.log(location.href) // https://juejin.im/timeline/frontend?a=10&b=10#some
console.log(location.protocol) // https:
console.log(location.pathname) // /timeline/frontend
console.log(location.search) // ?a=10&b=10
console.log(location.hash) // #some

另外,还有调用浏览器的前进、后退功能等

history.back()
history.forward()

获取浏览器特性(即俗称的UA)然后识别客户端,例如判断是不是 Chrome 浏览器

var ua = navigator.userAgent
var isChrome = ua.indexOf('Chrome')
console.log(isChrome)

5.Ajax与跨域

Ajax 是一种异步请求数据的一种技术,对于改善用户的体验和程序的性能很有帮助。 简单地说,在不需要重新刷新页面的情况下,Ajax 通过异步请求加载后台数据,并在网页上呈现出来。常见运用场景有表单验证是否登入成功、百度搜索下拉框提示和快递单号查询等等。Ajax的目的是提高用户体验,较少网络数据的传输量

如何手写 XMLHttpRequest 不借助任何库

因为浏览器出于安全考虑,有同源策略。也就是说,如果协议、域名或者端口有一个不同就是跨域,Ajax 请求会失败。

那么是出于什么安全考虑才会引入这种机制呢? 其实主要是用来防止 CSRF 攻击的。简单点说,CSRF 攻击是利用用户的登录态发起恶意请求。

然后我们来考虑一个问题,请求跨域了,那么请求到底发出去没有? 请求必然是发出去了,但是浏览器拦截了响应。

常见的几种跨域解决方案:

  • JSONP:利用同源策略对<script>标签不受限制,不过只支持GET请求
  • CORS:实现 CORS 通信的关键是后端,服务端设置 Access-Control-Allow-Origin 就可以开启,备受推崇的跨域解决方案,比 JSONP 简单许多
  • Node中间件代理或nginx反向代理:主要是通过同源策略对服务器不加限制

6.存储

sessionStorage 、localStorage 和 cookie 之间的区别

  • 共同点:都是保存在浏览器端,且都遵循同源策略。
  • 不同点:在于生命周期与作用域的不同

作用域:localStorage只要在相同的协议、相同的主机名、相同的端口下,就能读取/修改到同一份localStorage数据。sessionStorage比localStorage更严苛一点,除了协议、主机名、端口外,还要求在同一窗口(也就是浏览器的标签页)下

生命周期:localStorage 是持久化的本地存储,存储在其中的数据是永远不会过期的,使其消失的唯一办法是手动删除;而 sessionStorage 是临时性的本地存储,它是会话级别的存储,当会话结束(页面被关闭)时,存储内容也随之被释放。

六、模块化

几种常见模块化规范的简介:

CommonJS规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD CMD解决方案

AMD规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅。

CMD规范与AMD规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在Node.js中运行。不过,依赖SPM 打包,模块的加载逻辑偏重

ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案

复杂的网站都会有大量的CSS代码,通常也会有许多重复的值。

举个例子,同样一个颜色值可能在成千上百个地方被使用到,如果这个值发生了变化,需要全局搜索并且一个一个替换,效率不高且容易出错。

自定义属性在某个地方存储一个值,然后在其他许多地方引用它。另一个好处是语义化的标识。比如,--main-text-color 会比 #00ff00 更易理解,尤其是这个颜色值在其他上下文中也被使用到。

概念

自定义属性(有时候也被称作CSS变量或者级联变量)是由CSS作者定义的,它包含的值可以在整个文档中重复使用。

由自定义属性标记设定值(比如: --main-color: black;),由 var() 函数来获取值(比如: color: **var(--main-color)**;)。

优势

在构建大型站点时,作者通常会面对可维护性的挑战。在这些网页中,所使用的CSS 的数量是非常庞大的,并且在许多场合大量的信息会重复使用。

例如,在网页中维护一个配色方案,意味着一些颜色在 CSS 文件中多次出现,并被重复使用。当你修改配色方案时,不论是调整某个颜色或完全修改整个配色,都会成为一个复杂的问题,不容出错,而单纯查找替换是远远不够的。

如果使用了CSS 框架,这种情况会变得尤其糟糕,此时如果要修改颜色,则需要对框架本身进行修改。

在这些场合使用 LESS 或 Sass 类似的预处理器是非常有帮助的,但是这种通过添加额外步骤的方式,可能会增加系统的复杂性。

CSS变量为我们带来一些预处理器的便利,并且不需要额外的编译。

这些变量的第二个优势就是名称本身就包含了语义的信息。CSS 文件变得易读和理解。main-text-color比文档中的#00ff00更容易理解,特别是同样的颜色出现在不同的文件中的时候。

用法

下面是 CSS 变量的使用方法和步骤。

CSS 中声明变量

我们都知道,在 JS 中要使用一个变量前,必须声明这个表变量。在 CSS 中也是一样的道理。

声明一个自定义属性,属性名需要以两个减号(--)开始,属性值则可以是任何有效的CSS值。和其他属性一样,自定义属性也是写在规则集之内的,如下:

body {
    --bg-color: #7F583F;
    --color: #F7EFD2;
}

上面代码中,body选择器里面声明了两个变量:--bg-color和--color。

它们与color、font-size等正式属性没有什么不同,只是没有默认含义。所以 CSS 变量(CSS variable)又叫做**"CSS 自定义属性"**(CSS custom properties)。

规则集所指定的选择器定义了自定义属性的可见作用域。通常的最佳实践是定义在根伪类 :root下,这样就可以在HTML文档的任何地方访问到它了:

:root {
    --main-bg-color: #eee;
}

自定义属性名是大小写敏感的,--my-color 和 --My-color 会被认为是两个不同的自定义属性。

CSS 中使用变量

通过var()函数来读取变量。语法如下:

var(custom-property-name, value)
  • name (必需) 变量名(以两条破折号开头)。
  • value (可选) 表示变量的默认值。如果该变量不存在,就会使用这个默认值。

变量名称必须以两个破折号(--)开头,且区分大小写!

使用方法:

element {
  background-color: var(--main-bg-color);
}

变量也可以使用在变量声明中:

:root {
  --primary-color: #eee;
  --primary-bg-color: var(--main-bg-color);
}

变量值只能用作属性值,不能用作属性名。

JS 操作 CSS 变量

在 JS 代码中,我们可能需要读取 CSS 变量的值,其方法如下:

const root = document.querySelector(":root");
// 设置 CSS 变量
root.style.setProperty("--main-bg-color", "red");
// 读取 CSS 变量
const computedStyle = getComputedStyle(root);
const mainBgColor = computedStyle.getPropertyValue("--main-bg-color");
console.log(mainBgColor);
// 删除 CSS 变量
root.style.removeProperty("--main-bg-color");

总结

以上就是关于 CSS 变量的一些基本概念及使用方法,更多详情待后续!

灵活使用 CSS 变量,不仅可以提高生产力,也能够提高代码的可阅读性和维护性。

~

~

~ 本文完

学习有趣的知识,结识有趣的朋友,塑造有趣的灵魂!

大家好!我是〖编程三昧〗的作者 隐逸王,我的公众号是『编程三昧』,欢迎关注,希望大家多多指教!

知识与技能并重,内力和外功兼修,理论和实践两手都要抓、两手都要硬!