前端知识系统学习之JavaScript篇

616 阅读21分钟

本文是作者系统学习《Javascript 高级程序设计》第4版时所记录的笔记,内容主要偏向于总结,适合有一点点基础又想系统学习的同学阅读。若笔记中有什么理解或这是叙述不正确的地方,欢迎大家的批评指正。后续的想法是继续补充适当给每节内容增加一两道面试题,大家有什么经典的面试题欢迎提出来共同讨论,共同进步

1、JavaScript实现

image.png

  • 核心(ECMAScript): 由 ECMA-262 定义并提供核心功能。

    1.  语法
    2.  类型
    3. 语句
    4. 关键字
    5. 保留字
    6. 操作符
    7. 全局对象
  • 文档对象模型(DOM):提供与网页内容交互的方法和接口

    1. DOM 视图:描述追踪文档不同视图(如应用 CSS 样式前后的文档)的接口。
    2. DOM 事件:描述事件及事件处理的接口。
    3. DOM 样式:描述处理元素 CSS 样式的接口。
    4. DOM 遍历和范围:描述遍历和操作 DOM 树的接口。
  • 浏览器对象模型(BOM):提供与浏览器交互的方法和接口。

    1. 弹出新浏览器窗口的能力;
    2. 移动、缩放和关闭浏览器窗口的能力;
    3. navigator 对象,提供关于浏览器的详尽信息;
    4. location 对象,提供浏览器加载页面的详尽信息;
    5. screen 对象,提供关于用户屏幕分辨率的详尽信息;
    6. performance 对象,提供浏览器内存占用、导航行为和时间统计的详尽信息;
    7. 对 cookie 的支持;
    8. 其他自定义对象,如 XMLHttpRequest 和 IE 的 ActiveXObject。

1.1、JavaScript特点

1、js是一种解释性脚本语言(代码不进行预编译

2、跨平台特性,在绝大多数浏览器的支持下,可以在多种平台下运行(如Windows、Linux、Mac、Android、iOS等)。

3、弱类型脚本语言

对使用的数据类型未做出严格的要求,可以进行类型转换,简单又灵活。

4、单线程,事件驱动

JavaScript对用户的响应,是以事件驱动的方式进行的。在网页(Web Page)中执行了某种操作所产生的动作,被称为“事件”(Event)。例如按下鼠标、移动窗口、选择菜单等都可以被视为事件。当事件发生后,可能会引起相应的事件响应,执行某些对应的脚本,这种机制被称为“事件驱动”。

5、面向对象

一种基于对象的脚本语言,这意味着JavaScript能运用其已经创建的对象。因此,许多功能可以来自于脚本环境中对象的方法与脚本的相互作用。

6、安全性

JavaScript是一种安全性语言,它不允许访问本地的硬盘,并不能将数据存入到服务器上,不允许对网络文档进行修改和删除,只能通过浏览器实现信息浏览或动态交互。从而有效地防止数据的丢失。

2、<script>元素

JavaScript 是通过<script>元素插入到 HTML 页面中的。这个元素可用于把 JavaScript 代码嵌入到HTML 页面中,跟其他标记混合在一起,也可用于引入保存在外部文件中的 JavaScript。本章的重点可以总结如下。

  • 要包含外部 JavaScript 文件,必须将 src 属性设置为要包含文件的 URL。文件可以跟网页在同一台服务器上,也可以位于完全不同的域。
  • 所有<script>元素会依照它们在网页中出现的次序被解释。在不使用 defer 和 async 属性的情况下,包含在<script>元素中的代码必须严格按次序解释。
  • 对不推迟执行的脚本,浏览器必须解释完位于<script>元素中的代码,然后才能继续渲染页面的剩余部分。为此,通常应该把<script>元素放到页面末尾,介于主内容之后及</body>标签之前。
  • 可以使用 defer属性把脚本推迟到文档渲染完毕后再执行。推迟的脚本原则上按照它们被列出的次序执行。
  • 可以使用 async属性表示脚本不需要等待其他脚本,同时也不阻塞文档渲染,即异步加载。异步脚本不能保证按照它们在页面中出现的次序执行。
  • 通过使用<noscript>元素,可以指定在浏览器不支持脚本时显示的内容。如果浏览器支持并启 用脚本,则<noscript>元素中的任何内容都不会被渲染。

3、语言基础

任何语言的核心所描述的都是这门语言在最基本的层面上如何工作,涉及语法、操作符、数据类型 以及内置功能,在此基础之上才可以构建复杂的解决方案。如前所述,ECMA-262 以一个名为ECMAScript的伪语言的形式,定义了 JavaScript 的所有这些方面。

3.1、变量

ECMAScript 变量是松散类型的,意思是变量可以用于保存任何类型的数据。每个变量只不过是一个用于保存任意值的命名占位符。有 3 个关键字可以声明变量:var、const 和 let。其中,var 在ECMAScript 的所有版本中都可以使用,而 const 和 let 只能在 ECMAScript 6 及更晚的版本中使用。

3.1.1、var 关键字

var 声明作用域

使用 var 操作符在函数内部定义局部变量

function test() {
    var message = "hi"// 局部变量
}
test();
console.log(message); // 出错!message变量将在函数退出时被销毁

函数内定义变量时省略 var 操作符

function test() {
    message="hi"; //全局变量
}
test();
console.log(message); // "hi"
// 去掉之前的 var 操作符之后,message 就变成了全局变量。只要调用一次函数 test(),就会定义这个变量,并且可以在函数外部访问到。并不建议这样操作

var 变量提升

使用var这个关键字声明的变量会自动提升到函数作用域顶部

function foo() { 
    console.log(age); 
    var age = 26;
}

foo(); // undefined
// ECMAScript 运行时把它看成等价于如下代码
function foo() {
    var age;
    console.log(age); 
    age = 26;
}

foo(); // undefined

// 这就是所谓的“提升”(hoist),也就是把所有变量声明都拉到函数作用域的顶部。
// 此外,反复多次使用 var 声明同一个变量也没有问题。

3.1.2、let 关键字

使用规则(与var的区别)

  • let 声明的范围是块作用域, 而 var 声明的范围是函数作用域。
  • 不允许同一个块作用域中出现冗余声明
  • let 声明的变量不会在作用域中被提升

暂时性死区

// name 会被提升
console.log(name); // undefined 
var name = 'Matt';

// age 不会被提升
console.log(age); // ReferenceError:age 没有定义
let age = 26;

解释:在解析代码时,JavaScript 引擎也会注意出现在块后面的 let 声明,只不过在此之前不能以任何方式来引用未声明的变量。在 let 声明之前的执行瞬间被称为“暂时性死区”(temporal dead zone),在此阶段引用任何后面才声明的变量都会抛出 ReferenceError。

全局声明

与 var 关键字不同,使用 let 在全局作用域中声明的变量不会成为 window 对象的属性(var 声明的变量则会)。不过,let 声明仍然是在全局作用域中发生的。

var name = 'Matt'console.log(window.name); // 'Matt'

let age = 26;
console.log(window.age); // undefined

 for循环中的let声明

在 let 出现之前,for 循环定义的迭代变量会渗透到循环体外部,而使用let过后迭代变量的作用域仅限于 for 循环块内部

for (var i = 0; i < 5; ++i) {
// 循环逻辑
}
console.log(i); // 5

for (let i = 0; i < 5; ++i) {
// 循环逻辑
}

console.log(i); // ReferenceError: i 没有定义


在使用 var  的时候,最常见的问题就是对迭代变量的奇特声明和修改:

for (var i = 0; i < 5; ++i) { 
    setTimeout(() => console.log(i), 0)
}

//  5、5、5、5、5

之所以会这样,是因为在退出循环时,迭代变量保存的是导致循环退出的值:5。在之后执行超时  逻辑时,所有的 i 都是同一个变量,因而输出的都是同一个最终值。

而在使用 let 声明迭代变量时,JavaScript 引擎在后台会为每个迭代循环声明一个新的迭代变量。每个 setTimeout 引用的都是不同的变量实例,所以 console.log 输出的是我们期望的值,也就是循环执行过程中每个迭代变量的值。

for (let i = 0; i < 5; ++i) { 
    setTimeout(() => console.log(i), 0)
}

// 会输出 0、1、2、3、4

这种每次迭代声明一个独立变量实例的行为适用于所有风格的 for 循环,包括 for-in 和 for-of循环。

3.1.2、const 声明

const 的行为与 let 基本相同,唯一一个重要的区别是用它声明变量时必须同时初始化变量,且尝试修改 const 声明的变量会导致运行时错误。

const age = 26;
age = 36// TypeError: 给常量赋值
// const 也不允许重复声明

const name = 'Matt';
const name = 'Nicholas'; // SyntaxError

// const 声明的作用域也是块
const name = 'Matt'; 
if (true) {
    const name = 'Nicholas';
}
console.log(name); // Matt

const 声明的限制只适用于它指向的变量的引用。换句话说,如果 const 变量引用的是一个对象, 那么修改这个对象内部的属性并不违反 const 的限制。

const person = {};

person.name = 'Matt'; // ok

JavaScript 引擎会为 for 循环中的 let 声明分别创建独立的变量实例,虽然 const 变量跟 let 变量很相似,但是不能用 const 来声明迭代变量(因为迭代变量会自增):

for (const i = 0; i < 10; ++i) {
} 
// TypeError:给常量赋值

不过,如果你只想用 const 声明一个不会被修改的 for 循环变量,那也是可以的。也就是说,每次迭代只是创建一个新变量。这对 for-of 和 for-in 循环特别有意义:

let i = 0;
for (const j = 7; i < 5; ++i) { 
    console.log(j);    // 7, 7, 7, 7, 7
}

for (const key in {a: 1, b: 2}) { 
    console.log(key); // a, b
}

for (const value of [1,2,3,4,5]) { 
    console.log(value);  // 1, 2, 3, 4, 5
}

4、数据类型

简单数据类型(也称为原始类型):

  • Undefined--只有一个值,就是特殊值 undefined。当使用 var 或 let 声明了变量但没有初始化时,就相当于给变量赋予了 undefined 值
  • Null--只有一个值,即特殊值 null。逻辑上讲,null 值表示一个空对象指针,这也是给typeof 传一个 null 会返回"object"的原因
  • Boolean--是 ECMAScript 中使用最频繁的类型之一,有两个字面值:true 和 false。
  • Number--ECMAScript 中最有意思的数据类型或许就是 Number 了。Number 类型使用 IEEE 754 格式表示整数和浮点值(在某些语言中也叫双精度值)。有 3 个函数可以将非数值转换为数值:Number()、parseInt()和 parseFloat()。
  • String--数据类型表示零或多个 16 位 Unicode 字符序列。字符串可以使用双引号(")、单引号(')或反引号标示
  • Symbol--Symbol(符号)是 ECMAScript 6 新增的数据类型。符号是原始值,且符号实例是唯一、不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。

复杂数据类型(也称为引用类型):

  • Object(对象)--ECMAScript 中的对象其实就是一组数据和功能的集合。对象通过 new 操作符后跟对象类型的名称来创建。开发者可以通过创建 Object 类型的实例来创建自己的对象,然后再给对象添加属性和方法
    1. constructor:用于创建当前对象的函数。在前面的例子中,这个属性的值就是Object() 函数。
    2. hasOwnProperty(propertyName):用于判断当前对象实例(不是原型)上是否存在给定的属性。要检查的属性名必须是字符串(如 o.hasOwnProperty("name"))或符号。
    3. isPrototypeOf(object):用于判断当前对象是否为另一个对象的原型。(第 8 章将详细介绍原型。)
    4. propertyIsEnumerable(propertyName):用于判断给定的属性是否可以使用(本章稍后讨论的)for-in 语句枚举。与 hasOwnProperty()一样,属性名必须是字符串。
    5. toLocaleString():返回对象的字符串表示,该字符串反映对象所在的本地化执行环境。
    6. toString():返回对象的字符串表示。
    7. valueOf():返回对象对应的字符串、数值或布尔值表示。通常与 toString()的返回值相同。

4.1、 nullundefined有什么区别么(面试题)

undefined 只有一个值,就是特殊值 undefined。当使用 var 或 let 声明了变量但没有初始化时,就相当于给变量赋予了 undefined 值,undefined是一个假值

let message; // 这个变量被声明了,只是值为 undefined
// age 没有声明
if (message) {
    // 这个块不会执行
}
if (!message) {
    // 这个块会执行
}
if (age) {
    // 这里会报错
}

Null 类型同样只有一个值,即特殊值 null。逻辑上讲,null 值表示一个空对象指针,这也是给typeof 传一个 null 会返回"object"的原因 undefined 值是由 null 值派生而来的,因此 ECMA-262 将它们定义为表面上相等,如下面的例子所示:

console.log(null == undefined); // true
console.log(null === undefined); // false

4.2、typeof 操作符

let message = "some string"console.log(typeof message); // "string"
console.log(typeof(message)); // "string" 
console.log(typeof 95); // "number"
  •  "undefined"表示值未定义;
  •  "boolean"表示值为布尔值;
  •  "string"表示值为字符串;
  • "number"表示值为数值;
  •  "object"表示值为对象(而不是函数)或 null;
  •  "function"表示值为函数;
  •  "symbol"表示值为符号。

5、变量、作用域与内存

5.1、 原始值与引用值

ECMAScript 变量可以包含两种不同类型的数据:原始值和引用值。原始值(primitive value)就是最简单的数据,引用值(reference value)则是由多个值构成的对象。

在把一个值赋给变量时,JavaScript 引擎必须确定这个值是原始值还是引用值。上一章讨论了 6 种原始值:Undefined、Null、Boolean、Number、String 和 Symbol。保存原始值的变量是按值(by value)访问的,因为我们操作的就是存储在变量中的实际值。

假如有以下几个基本类型的变量:

var name = 'jozo';\
var city = 'guangzhou';\
var age = 22;\

那么它的存储结构如下图: 

 引用值是保存在内存中的对象。与其他语言不同,JavaScript不允许直接访问内存位置,因此也就不能直接操作对象所在的内存空间。在操作对象时,实际上操作的是对该对象的引用(reference)而非实际的对象本身。为此,保存引用值的变量是按引用(by reference)访问的。

假如有以下几个对象:

var person1 = {name:'jozo'};
var person2 = {name:'xiaom'};
var person3 = {name:'xiaoq'};

那么它的存储结构如下图: 



5.2、复制值

在通过变量把一个原始值赋值到另一个变量时,原始值会被复制到新变量的位置。请看下面的例子:

let num1 = 5let num2 = num1;

原始值赋值.png

在把引用值从一个变量赋给另一个变量时,存储在变量中的值也会被复制到新变量所在的位置。区别在于,这里复制的值实际上是一个指针,它指向存储在堆内存中的对象。操作完成后,两个变量实际上指向同一个对象,因此一个对象上面的变化会在另一个对象上反映出来,如下面的例子所示:

let obj1 = new Object();

let obj2 = obj1;

obj1.name = "Nicholas";

console.log(obj2.name); // "Nicholas"

引用值赋值.png

5.2.1、赋值、深拷贝与浅拷贝

赋值 当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。

let  obj1 = {name:’张三’,age:12}
let  obj2 = obj1
Obj2.name = ‘李四’
Console.log(obj1 ) // {name:’李四’,age:12}
var a = [1,2,3,4];
var b = a; a[0] = 0;
console.log(a,b); // [0,2,3,4],[0,2,3,4]

浅拷贝  重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。

  • Object.assign()*

Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。

let obj1 = { person: {name: "张三", age: 41},sports:'basketball' };
let obj2 = Object.assign({}, obj1);
obj2.person.name = "李四";
obj2.sports = 'football'
console.log(obj1); // { person: { name: "李四", age: 41 }, sports: 'basketball' }
  • 展开运算符...*

展开运算符是一个 es6 / es2015特性,它提供了一种非常方便的方式来执行浅拷贝,这与 Object.assign ()的功能相同。

let obj1 = { name: 张三, address:{x:100,y:100}}
let obj2= {... obj1}
obj1.address.x = 200;
obj1.name = '李四'
console.log('obj2',obj2) // obj2 { name: '张三', address: { x: 200, y: 100 } }

  • Array.prototype.concat()*
let arr = [1, 3, {username: '张三'}];
let arr2 = arr.concat();    
arr2[2].username = '李四';
console.log(arr);

//[ 1, 3, { username: '李四' } ]

  • Array.prototype.slice()*
let arr = [1, 3, {username: ' 张三' }];

let arr3 = arr.slice();

arr3[2].username = '李四'

console.log(arr); // [ 1, 3, { username: '李四' } ]

深拷贝

从堆内存中开辟一个新的区域存放新对象,对对象中的子对象进行递归拷贝,拷贝前后的两个对象互不影响。

  • JSON.parse(JSON.stringify())* 这也是利用JSON.stringify将对象转成JSON字符串,再用JSON.parse把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。
let arr = [1, 3, {username: ' 张三'}];

let arr4 = JSON.parse(JSON.stringify(arr));

arr4[2].username = 'duncan';

这种方法虽然可以实现数组或对象深拷贝,但不能处理函数和正则 因为这两者基于JSON.stringify和JSON.parse处理后,得到的正则就不再是正则(变为空对象),得到的函数就不再是函数(变为null)了。

  • 递归 递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。
function deepClone(obj, hash = new WeakMap()) {

    // 如果是null或者undefined我就不进行拷贝操作

    if (obj === null) return obj;  

    if (obj instanceof Date) return new Date(obj);

    // 可能是对象或者普通的值 如果是函数的话是不需要深拷贝

    if (obj instanceof RegExp) return new RegExp(obj);

    // 是对象的话就要进行深拷贝

    if (typeof obj !== "object") return obj;

    if (hash.get(obj)) return hash.get(obj);

    //找到的是所属类原型上的constructor,而原型上的constructor指向的是当前类本身

    let cloneObj = new obj.constructor();

    hash.set(obj, cloneObj);

    for (let key in obj) {

        if (obj.hasOwnProperty(key)) {

         // 实现一个递归拷贝

        cloneObj[key] = deepClone(obj[key], hash);

        }

    }

    return cloneObj;

}

let obj = { name: 1, address: { x: 100 } };

obj.o = obj; // 对象存在循环引用的情况

let d = deepClone(obj);

obj.address.x = 200; 
console.log(d); // { name: 1, address: { x: 200 } }

5.3、确定类型

前一章提到的 typeof 操作符最适合用来判断一个变量是否为原始类型。更确切地说,它是判断一个变量是否为字符串、数值、布尔值或 undefined 的最好方式。如果值是对象或 null,那么 typeof返回"object",如下面的例子所示:

let s = "Nicholas"let b = true;

let i = 22let u;

let n = null;

let o = new Object(); 
console.log(typeof s); // string 
console.log(typeof i); // number 
console.log(typeof b); // boolean 
console.log(typeof u); // undefined 
console.log(typeof n); // object 
console.log(typeof o); // object

typeof虽然对原始值很有用,但它对引用值的用处不大。我们通常不关心一个值是不是对象, 而是想知道它是什么类型的对象。为了解决这个问题,ECMAScript 提供了 instanceof 操作符,语法如下:

result = variable instanceof constructor

如果变量是给定引用类型(由其原型链决定,将在第 8 章详细介绍)的实例,则 instanceof 操作符返回 true。来看下面的例子:

console.log(person instanceof Object); 
// 变量 person 是 Object 吗?
console.log(colors instanceof Array); 
// 变量 colors 是 Array 吗? 
console.log(pattern instanceof RegExp); 
// 变量 pattern 是 RegExp 吗?

按照定义,所有引用值都是 Object 的实例,因此通过instanceof 操作符检测任何引用值和Object 构造函数都会返回 true。类似地,如果用 instanceof 检测原始值,则始终会返回 false, 因为原始值不是对象。

5.4、作用域

任何变量(不管包含的是原始值还是引用值)都存在于某个执行上下文中(也称为作用域)。这个上下文(作用域)决定了变量的生命周期,以及它们可以访问代码的哪些部分。执行上下文可以总结如下。

  • 执行上下文分全局上下文、函数上下文和块级上下文。
  • 代码执行流每进入一个新上下文,都会创建一个作用域链,用于搜索变量和函数。
  • 函数或块的局部上下文不仅可以访问自己作用域内的变量,而且也可以访问任何包含上下文乃至全局上下文中的变量。
  • 全局上下文只能访问全局上下文中的变量和函数,不能直接访问局部上下文中的任何数据。
  • 变量的执行上下文用于确定什么时候释放内存。

详解一

var color = "blue";

function changeColor() { 
    if (color === "blue") {
        color = "red";

        } else {

        color = "blue";
    }
}
changeColor();

对这个例子而言,函数 changeColor()的作用域链包含两个对象:一个是它自己的变量对象(就是定义 arguments 对象的那个),另一个是全局上下文的变量对象。这个函数内部之所以能够访问变量color,就是因为可以在作用域链中找到它。

详解二

var color = "blue";
function changeColor() { 
    let anotherColor = "red";
    function swapColors() {
        let tempColor = anotherColor; 
        anotherColor = color;
        color = tempColor;
        // 这里可以访问 color、anotherColor 和 tempColor

    }
    // 这里可以访问 color 和 anotherColor,但访问不到 tempColor swapColors();

}
// 这里只能访问 color 
changeColor();

以上代码涉及 3 个上下文:全局上下文、changeColor()的局部上下文和 swapColors()的局部上下文。全局上下文中有一个变量 color 和一个函数 changeColor()。changeColor()的局部上下文中有一个变量 anotherColor 和一个函数 swapColors(),但在这里可以访问全局上下文中的变量 color。swapColors()的局部上下文中有一个变量 tempColor,只能在这个上下文中访问到。全局上下文和changeColor()的局部上下文都无法访问到 tempColor。而在 swapColors()中则可以访问另外两个上下文中的变量,因为它们都是父上下文。

5.4.1、作用域(变量提升、函数提升和暂时性死区)

那么,变量提升是怎样的呢?

console.log(a);
var a = 10;

在这段脚本中,会输出 undefined,为什么?

var a;
console.log(10);
a = 10;

看上面代码,在 JavaScript 解析的过程中,会将 a 提取到上面进行声明。 而 console.log(a) 打印的时候,因为 a 声明了但是没有填写值,所以是 undefined。 而这段代码换成 let 或者 const 语句呢?就会出现 暂时性死区

console.log(a);
let a = 10;

输出:

VM196:1 Uncaught ReferenceError: Cannot access 'a' before initialization

OK,下面进行 变量提升和暂时性死区 小总结:

  • 在 var 定义的变量前面进行打印,因为变量提升缘故,所以可以打印 undefined
  • 在 let、const 定义的变量前面进行打印,因为暂时性死区问题,所以会报错。

再看看函数提升:

var foo = 3;
function getFoo() {
  var foo = foo || 5;
  console.log(foo); // 输出 5
}

getFoo();

记住一句话:

  • 函数是一等公民

所以,上面的代码,JavaScript 解析会变成:

function getFoo() {
  var foo;
  foo = foo || 5;
  console.log(foo);
}
var foo;
foo = 3;
getFoo();

看,这就是 函数提升

5.5、变量提升相关面试题

题目 1

function getFoo() {
  foo();
  var foo = function() {
    console.log(1);
  }
  foo();
  function foo() {
    console.log(2);
  }

  foo();
}
getFoo();
// 2
// 1
// 1

我们将其换成变量和函数提升后的效果便可得知:

function getFoo() {
  var foo;

  function foo() {
    console.log(2);
  }

  foo(); // 2

  foo = function() {
    console.log(1);
  }

  foo(); // 1

  foo(); // 1
}
getFoo();

题目 2

console.log(a);

var a = 10;
var a = 100;

console.log(a);

function a() {
  console.log('a');
}
function a() {
  console.log('aa');
}

a();

// f a() { console.log('aa')}
// 100
// a is not a function

题目 3

var a = 1;
function b() {
  a = 10;
  return;
  function a() {}
}
b();
console.log(a);
// 1

题目 4

function foo() {
  function bar() {
    return 3;
  }
  return bar();
  function bar() {
    return 8;
  }
}
console.log(foo());

// 8

题目 5

var foo = { n: 1 };

(function(foo) {
  console.log(foo.n);
  foo.n = 3;
  foo = { n: 2 };
  console.log(foo.n);
})(foo)

console.log(foo.n);

答案:

var foo = { n: 1 };
(function(foo){            // 形参foo同实参foo一样,指向同一片内存空间,这个空间里的 n 的值为 1
    var foo;               // 优先级低于形参,无效。
    console.log(foo.n);    // 输出 1
    foo.n = 3;             // 形参与实参 foo 指向的内存空间里的 n 的值被改为 3
    foo = { n: 2 };        // 形参 foo 指向了新的内存空间,里面 n 的值为 2.
    console.log(foo.n);    // 输出新的内存空间的 n 的值
})(foo);
console.log(foo.n);        // 实参 foo 的指向还是原来的内存空间,里面的 n 的值为 3.

题目 6.谈谈对作用域链的理解

答:当上下文中的代码在执行的时候,会创建变量对象的一个作用域链。这个作用域链决定了各级上下文中的代码在访问变量和函数的顺序。代码正在执行的上下文的变量对象始终位于作用域链的最前端。如果上下文是函数,则其活动对象用作变量对象。活动对象最初只有一个定义变量:arguments。作用域链的下一个变量对象来自于包含上下文,在下一个对象来自于在包含上下文,以此类推直至全局上下文;全局上下文的变量对象始终是作用域的最后一个变量对象。

在《JavaScript深入之变量对象》中讲到,当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

6、对象、类与面向对象编程

6.1、创建对象

虽然使用 Object 构造函数或对象字面量可以方便地创建对象,但这些方式也有明显不足:创建具有同样接口的多个对象需要重复编写很多代码。

6.1.1、工厂模式

工厂模式是一种众所周知的设计模式,广泛应用于软件工程领域,用于抽象创建特定对象的过程。

function createPerson(name, age, job) {
    let o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function() {
        console.log(this.name);
    };
    return o;
}

let person1 = createPerson("Nicholas", 29, "Software Engineer");

let person2 = createPerson("Greg", 27, "Doctor");

这里,函数createPerson()接收3个参数,根据这几个参数构建了一个包含Person信息的对象。可以用不同的参数多次调用这个函数,每次都会返回包含3个属性和1个方法的对象。

6.1.2、构造函数模式

像 Object 和 Array 这样的原生构造函数,运行时可以直接在执行环境中使用。当然也可以自定义构造函数,以函数的形式为自己的对象类型定义属性和方法。

function Person(name, age, job){ 
    this.name = name;
    this.age = age; 
    this.job = job;
    this.sayName = function() { 
        console.log(this.name);
    };
} 

let person1 = new Person("Nicholas"29"Software Engineer"); 
let person2 = new Person("Greg"27, "Doctor");

person1.sayName(); // Nicholas 
person2.sayName(); // Greg

在这个例子中,Person()构造函数代替了 createPerson()工厂函数。实际上,Person()内部的代码跟 createPerson()基本是一样的,只是有如下区别。

  • 没有显式地创建对象。
  • 属性和方法直接赋值给了 this。
  • 没有 return。

6.1.3、原型模式

每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。实际上,这个对象就是通过调用构造函数创建的对象的原型。使用原型对象的好处是,在它上面定义的属性和方法可以被对象实例共享。原来在构造函数中直接赋给对象实例的值,可以直接赋值给它们的原型,如下所示:

function Person() {}

Person.prototype.name = "Nicholas"Person.prototype.age = 29Person.prototype.job = "Software Engineer"Person.prototype.sayName = function() {
    console.log(this.name);
};

let person1 = new Person(); 
person1.sayName(); // "Nicholas"
let person2 = new Person(); 
person2.sayName(); // "Nicholas"

console.log(person1.sayName == person2.sayName); // true

这里,所有属性和 sayName()方法都直接添加到了 Person 的  prototype 属性上,构造函数体中什么也没有。但这样定义之后,调用构造函数创建的新对象仍然拥有相应的属性和方法。与构造函数模式不同,使用这种原型模式定义的属性和方法是由所有实例共享的。因此 person1 和 person2 访问的都是相同的属性和相同的 sayName()函数。

6.2、原型链

把原型链定义为 ECMAScript 的主要继承方式,其基本思想就是通过原型继承多个引用类型的属性和方法。重温一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。如果原型是另一个类型的实例呢?那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。这就是原型链的基本构想。

实现原型链涉及如下代码模式:

function SuperType() { 
    this.property = true;
}

SuperType.prototype.getSuperValue = function() { 
    return this.property;

};

function SubType() { 
    this.subproperty = false;

}


// 继承 SuperType

SubType.prototype = new SuperType(); 
SubType.prototype.getSubValue = function () {
    return this.subproperty;
};

 
let instance = new SubType(); 
console.log(instance.getSuperValue()); // true

7、函数

7.1、 函数表达式、函数声明及箭头函数

函数是ECMAScript 中最有意思的部分之一,这主要是因为函数实际上是对象。每个函数都是Function 类型的实例,而 Function也有属性和方法,跟其他引用类型一样。因为函数是对象,所以函数名就是指向函数对象的指针,而且不一定与函数本身紧密绑定

函数声明的方式定义

function sum (num1, num2) { 
    return num1 + num2;
}

JavaScript 引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义。而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。来看 下面的例子:

// 没问题console.log(sum(10,10));

 function sum(num1, num2) {

    return num1 + num2;

}

以上代码可以正常运行,因为函数声明会在任何代码执行之前先被读取并添加到执行上下文。这个过程叫作函数声明提升(function declaration hoisting)。

函数表达式


sum(1,2) //// Error! function doesn't exist yet
let sum = function(num1, num2) { 
    return num1 + num2;
};

sum(1,2) // 3

sum2(1,2) //3 因为JavaScript引擎会先读取函数声明,然后再执行代码
function sum2(num1, num2) {
    return num1 + num2;
}

箭头函数

let sum = (num1, num2) => { 
    return num1 + num2;
};

箭头函数不能使用arguments、new.target和

super,也不能用作构造函数。此外,箭头函数也没有 prototype 属性。

箭头函数的this指向规则:

  • 箭头函数没有prototype(原型),所以箭头函数本身没有this
  • 箭头函数的this指向在定义的时候继承自外层第一个普通函数的this。
  • 不能直接修改箭头函数的this指向
  • 箭头函数外层没有普通函数,严格模式和非严格模式下它的this都会指向window(全局对象)

7.2、this

另一个特殊的对象是this,它在标准函数和箭头函数中有不同的行为。

在标准函数中,this 引用的是把函数当成方法调用的上下文对象,这时候通常称其为 this 值(在网页的全局上下文中调用函数时,this 指向 windows)。来看下面的例子:

window.color = 'red'let o = {
    color'blue'
};

function sayColor() { 
    console.log(this.color);
}

sayColor(); // 'red'


o.sayColor = sayColor; 
o.sayColor(); // 'blue'

在箭头函数中,this 引用的是定义箭头函数的上下文。下面的例子演示了这一点。在对sayColor()的两次调用中,this 引用的都是 window 对象,因为这个箭头函数是在 window 上下文中定义的:

window.color = 'red'let o = {
    color'blue'
};

let sayColor = () => console.log(this.color); 
sayColor(); // 'red'

o.sayColor = sayColor; o.sayColor(); // 'red'

7.3、递归

递归函数通常的形式是一个函数通过名称调用自己,如下面的例子所示:

    function factorial(num) { 
        if (num <= 1) {
            return 1;
        } else {
            return num * factorial(num - 1);
        }
    }

这是经典的递归阶乘函数。虽然这样写是可以的,但如果把这个函数赋值给其他变量,就会出问题:

let anotherFactorial = factorial; 
factorial = null;
console.log(anotherFactorial(4)); // 报错

在严格模式下运行的代码是不能访问 arguments.callee的,因为访问会出错。此时,可以使用命名函数表达式(named function expression)达到目的。比如:

const factorial = (function f(num) { 
    if (num <= 1) {
        return 1;
    } else {
        return num * f(num - 1);
    }
});

这里创建了一个命名函数表达式 f(),然后将它赋值给了变量 factorial。即使把函数赋值给另一个变量,函数表达式的名称 f 也不变,因此递归调用不会有问题。这个模式在严格模式和非严格模式下都可以使用。

8、异步编程

8.1、同步

同步行为对应内存中顺序执行的处理器指令。每条指令都会严格按照它们出现的顺序来执行,而每条指令执行后也能立即获得存储在系统本地(如寄存器或系统内存)的信息。

8.2、异步

异步行为类似于系统中断,即当前进程外部的实体可以触发代码执行。异步操作经常是必 要的,因为强制进程等待一个长时间的操作通常是不可行的(同步操作则必须要等)。如果代码要访问 一些高延迟的资源,比如向远程服务器发送请求并等待响应,那么就会出现长时间的等待。

8.2.1、 以往的异步编程模式

在早期的 JavaScript 中,只支持定义回调函数来表明异步操作完成。串联多个异步操作是一个常见的问题,通常需要深度嵌套的回调函数(俗称“回调地狱”)来解决。

1、异步返回值

function double(value,callback) {
    setTimeout(()=> callback(value * 2),100)
}

double(3,(x) => console.log(x)) // 6 (大约 1000 毫秒之后)

2、嵌套异步回调

如果异步返值又依赖另一个异步返回值,那么回调的情况还会进一步变复杂。在实际的代码中,这 就要求嵌套回调:

function  double(value, success, failure) {
    setTimeout(()=>{
        try{
            if(typeof !== 'number') {
                throw 'Must provide number as first argument'
            }
            sucess(value * 2)
        } catch (e) {
            failure(e)
        }
    },1000)
}

const successCallback = (x) => {

    double(x, (y) => console.log(`Success: ${y}`));

};

const failureCallback = (e) => console.log(`Failure: ${e}`);

double(3, successCallback, failureCallback);

// Success: 12(大约 1000 毫秒之后)

显然,随着代码越来越复杂,回调策略是不具有扩展性的。“回调地狱”这个称呼可谓名至实归。 嵌套回调的代码维护起来就是噩梦。

8.2.2、 期约

一、期约基础

1、期约的三种状态

待定(pending) 是期约的最初始状态。在待定状态下,期约可以 落定(settled)为代表成功的 兑现(fulfilled也称为resolved)状态,或者代表失败的拒绝(rejected) 状态。无论落定为哪种状态都是不可逆 的。

2、通过执行函数控制期约状态

由于期约的状态是私有的,所以只能在内部进行操作。内部操作在期约的执行器函数中完成。执行器函数主要有两项职责:初始化期约的异步行为和控制状态的最终转换。

例一(执行顺序)
new Promise(() => setTimeout(console.log0'executor')); 
setTimeout(console.log0'promise initialized');
// executor
// promise initialized

例二(推迟切换状态)
let p = new Promise((resolve, reject) => setTimeout(resolve, 1000));
// 在console.log 打印期约实例的时候,还不会执行超时回调(即 resolve()) 
setTimeout(console.log0, p); // Promise <pending>

例三(状态不可逆)
let p = new Promise((resolve, reject) => {
    resolve();
    reject(); // 没有效果
});

setTimeout(console.log0, p);  
// Promise <resolved>

3、Promise.resolve()

期约并非一开始就必须处于待定状态,然后通过执行器函数才能转换为落定状态。通过调用Promise.resolve()静态方法,可以实例化一个解决的期约。

例一(期约转换)
setTimeout(console.log0Promise.resolve());
// Promise <resolved>: undefined 
setTimeout(console.log0Promise.resolve(3));
// Promise <resolved>: 3

// 多余的参数会忽略
setTimeout(console.log0Promise.resolve(456));
// Promise <resolved>: 4

例二(幂等方法)
let p = Promise.resolve(7);
setTimeout(console.log0, p === Promise.resolve(p));
// true
setTimeout(console.log0, p === Promise.resolve(Promise.resolve(p)));
// true

二、期约的实例方法

1、Promise.prototype.then()

Promise.prototype.then()是为期约实例添加处理程序的主要方法。这个 then()方法接收最多两个参数:onResolved 处理程序和 onRejected 处理程序。这两个参数都是可选的,如果提供的话, 则会在期约分别进入“兑现”和“拒绝”状态时执行。

function onResolved(id) { 
    setTimeout(console.log0, id, 'resolved');
}

function onRejected(id) { 
    setTimeout(console.log0, id, 'rejected');
}

let p1 = new Promise((resolve, reject) => setTimeout(resolve, 3000)); 
let p2 = new Promise((resolve, reject) => setTimeout(reject, 3000));

p1.then(()=>onResolved('p1'),()=>onRejected('p1'));
p2.then(()=>onResolved('p2'),()=>onRejected('p2'));

//(3 秒后)
// p1 resolved
// p2 rejected

2、Promise.prototype.catch()

Promise.prototype.catch()方法用于给期约添加拒绝处理程序。这个方法只接收一个参数:onRejected处理程序。事实上,这个方法就是一个语法糖,调用它就相当于调用Promise.prototype. then(null, onRejected)

let p = Promise.reject();
let onRejected = function(e) { 
    setTimeout(console.log0'rejected');
};

// 这两种添加拒绝处理程序的方式是一样的: 
p.then(null, onRejected); // rejected 
p.catch(onRejected);//rejected
Promise.prototype.catch()返回一个新的期约实例:
let p1 = new Promise(() => {});
let p2=p1.catch

setTimeout(console.log, 0, p1); // Promise <pending> 
setTimeout(console.log, 0, p2); // Promise <pending>
setTimeout(console.log,0,p1 ===p2); //  false

3、Promise.prototype.finally()

Promise.prototype.finally()方法用于给期约添加 onFinally处理程序,这个处理程序在期约转换为解决或拒绝状态时都会执行。这个方法可以避免 onResolved 和 onRejected 处理程序中出现冗余代码。但 onFinally 处理程序没有办法知道期约的状态是解决还是拒绝,所以这个方法主要用于添加清理代码

4、传递解决值和拒绝理由

let p1 = new Promise((resolve, reject) => resolve('foo')); 
p1.then((value) => console.log(value)); // foo
let p2 = new Promise((resolve, reject) => reject('bar')); 
p2.catch((reason) => console.log(reason)); // bar


let p3 = Promise.resolve('foo');
p3.then((value) => console.log(value)); // foo
let p4 = Promise.reject('bar');
p4.catch((reason) => console.log(reason)); // bar

三、期约连锁与期约合成

一、 期约连锁

把期约逐个地串联起来是一种非常有用的编程模式。之所以可以这样做,是因为每个期约实例的方 法(then()、catch()和 finally())都会返回一个新的期约对象,而这个新期约又有自己的实例方法。这样连缀方法调用就可以构成所谓的“期约连锁”。

例一(串行同步任务)
let p = new Promise((resolve, reject) => { console.log('first');
    resolve();
});
p.then(() => console.log('second'))
.then(() => console.log('third'))
.then(() => console.log('fourth'));
// first
// second
// third
// fourth
//这个实现最终执行了一连串同步任务

例二(串行化异步任务)

let p1 = new Promise((resolve, reject) => { 
        console.log('p1 executor');
        setTimeout(resolve,1000)
    })
    
    p1.then(()=>new Promise((resolve, reject) => { 
        console.log('p2 executor');
        setTimeout(resolve,1000)
    })))
    .then(()=>new Promise((resolve, reject) => { 
        console.log('p3 executor');
        setTimeout(resolve,1000)
    }))
    .then(()=>new Promise((resolve, reject) => { 
        console.log('p4 executor');
        setTimeout(resolve,1000)
    }))
    
// p1 executor(1 秒后)
// p2 executor(2 秒后)
// p3 executor(3 秒后)
// p4 executor(4 秒后)

2、期约图

因为一个期约可以有任意多个处理程序,所以期约连锁可以构建有向非循环图的结构。这样,每个期约都是图中的一个节点,而使用实例方法添加的处理程序则是有向顶点。因为图中的每个节点都会等待前一个节点落定,所以图的方向就是期约的解决或拒绝顺序。

// A

// / \

// B C

// /\  /\

// D E F G

 

let A = new Promise((resolve, reject) => { console.log('A');
    resolve();
});
let B = A.then(() => console.log('B'));
let C = A.then(() => console.log('C'));
B.then(() => console.log('D'));
B.then(() => console.log('E'));
C.then(() => console.log('F'));
C.then(() => console.log('G'));
// A
// B
// C
// D
// E
// F
// G

注意,日志的输出语句是对二叉树的层序遍历。如前所述,期约的处理程序是按照它们添加的顺序 执行的。由于期约的处理程序是先添加到消息队列,然后才逐个执行,因此构成了层序遍历。

8.2.2、 异步函数

ES8 的 async/await 旨在解决利用异步结构组织代码的问题。为此,ECMAScript 对函数进行了扩展,为其增加了两个新关键字:async 和 await。异步函数是将期约应用于 JavaScript 函数的结果。异步函数可以暂停执行,而不阻塞主线程。无论是编写基于期约的代码,还是组织串行或平行执行的异步代码,使用异步函数都非常得心应手。异步函数可以说是现代 JavaScript 工具箱中最重要的工具之一。

一、async

例一(声明异步函数)
async function foo() {}              //函数声明
const bar = async function () {}     // 函数表达式
const baz = async () => {}         //  箭头函数

例二(执行顺序)
async function foo() { 
    console.log(1);
}
foo();
console.log(2);
// 1
// 2
// 使用 async 关键字可以让函数具有异步特征,但总体上其代码仍然是同步求值的

例三(异步函数的返回)
async function foo() { 
    console.log(1);
    return 3
}
// 给返回的期约添加一个解决处理程序
foo().then(console.log);
console.log(2);
// 1
// 2
// 3


当然,直接返回一个期约对象也是一样的:
async function foo() { 
    console.log(1);
    return Promise.resolve(3);
}
// 给返回的期约添加一个解决处理程序
foo().then(console.log); 
console.log(2);
// 1
// 2
// 3

2、await

因为异步函数主要针对不会马上完成的任务,所以自然需要一种暂停和恢复执行的能力。使用await关键字可以暂停异步函数代码的执行,等待期约解决。

例一(await的使用)
// 异步打印"foo"

async function foo() {
    console.log( await Promise.resolve('foo'));
}
foo();
// foo

// 异步打印"bar"
async function bar() {
    return await Promise.resolve('bar');
}
bar().then(console.log);
// bar

// 1000 毫秒后异步打印"baz" 
async function baz() {
    await new Promise((resolve, reject) => setTimeout(resolve, 1000)); 
    console.log('baz');
}
baz();
// baz(1000 毫秒后)

3、停止和恢复执行

要完全理解 await 关键字,必须知道它并非只是等待一个值可用那么简单。JavaScript 运行时在碰到 await 关键字时,会记录在哪里暂停执行。等到 await 右边的值可用了,JavaScript 运行时会向消息队列中推送一个任务,这个任务会恢复异步函数的执行。因此,即使 await 后面跟着一个立即可用的值,函数的其余部分也会被异步求值。

async function foo() {
    console.log(2)
    await 1
    console.log(4)
}
console.log(1)
foo()
console.log(3)

解释
(1) 打印 1;
(2) 调用异步函数 foo();
(3)(在 foo()中)打印 2;
(4)(在 foo()中)await 关键字暂停执行,为立即可用的值 null 向消息队列中添加一个任务;
(5foo()退出;
(6) 打印 3;
(7) 同步线程的代码执行完毕;
(8JavaScript   运行时从消息队列中取出任务,恢复异步函数执行; 
(9)(在 foo()中)恢复执行,await 取得 1 值(这里并没有使用); 
(10)(在 foo()中)打印 4;
(11foo()返回。

9、浅谈事件循环(event loop)

9.1、Micro-Task 与 Macro-Task

  • 常见的 macro-task 比如:setTimeout、setInterval、 setImmediate、script(整体代码)、 I/O 操作、UI 渲染等。
  • 常见的 micro-task 比如: process.nextTick、new Promise().then(回调)、

9.1、Event Loop 执行过程如下:

  1. 一开始整个脚本 script 作为一个宏任务执行
  2. 执行过程中,同步代码 直接执行,在执行的过程中,会判断是同步任务还是异步任务,通过对一些接口的调用,可以产生新的 macro-task 与 micro-task,宏任务 进入宏任务队列,微任务 进入微任务队列。
  3. 当前宏任务执行完出队,检查微任务列表,有则依次执行,直到全部执行完毕(当 macro-task 出队时,任务是一个一个执行的;而 micro-task 出队时,任务是一队一队执行的)。
  4. 执行浏览器 UI 线程的渲染工作。
  5. 检查是否有 Web Worker 任务,有则执行。
  6. 执行完本轮的宏任务,回到步骤 2,依次循环,直到宏任务和微任务队列为空。

本来准备放上大量面试题,但我想了想,想要完全了解,这里我强烈推荐这篇文章!!!

Tasks, microtasks, queues and schedules(宏任务、微任务、队列)

10、 BOM

  •  理解 BOM 的核心——window 对象
  •  控制窗口及弹窗
  •  通过 location 对象获取页面信息
  •  使用 navigator 对象了解浏览器
  •  通过 history 对象操作浏览器历史

10.1、window对象

window对象的所有属性方法都是全局的window对象的所有属性和方法,在使用时都可以省略 window.所有的全局变量都是window对象的属性 所有的全局函数都是window对象的方法

var age = 29;
var sayAge = () => alert(this.age); 
alert(window.age); // 29
sayAge(); // 29
window.sayAge(); // 29

三个对话框

  • alert() 警告框
  • prompt() 弹出输入框 点击确定返回输入的内容点击取消返回null
  • confirm() 弹出确认框 点击确定 返回true 点击取消返回false

一个弹出新窗口(页面跳转)

  • open( "url" , "name" , "外观") 该方法返回弹出的子窗口

  • 外观 :width height top left(位置、大小)

  • location 是否有地址栏;resizable是否可调整窗口大小;scrollbars是否有滚动条;status 状态栏;titlebar 标题栏; toolbar工具栏(兼容性)

    yes | 1 表示有 no | 0 表示没有

两个定时器方法 :

  • setInterval( 要执行的任务 ,间隔时间 )连续执行的定时器使用clearInterval() 停止
  • setTimeout( 要执行的任务 ,间隔时间 )延时器只执行一次的定器 clearTimeout() 停止延时器

定时器特点 : 定时器都是异步执行的 事件也是异步执行

// 例一(页面打开后,弹出的广告窗口在5秒后自动关闭)
//给opener 一个点击关闭时间
$id("btn2").onclick = function (){
    opener.close();
}
//延时5秒执行关闭
setTimeout( function(){
    opener.close();
},5000)


// 例二(手写函数防抖与节流)

// 防抖:指在事件触发n秒后再执行[回调]。如果在n秒内再次被触发,则重新计算时间。合并执行
 function debounce(fn,delay) {
     let timer = null //创建一个标记用来存放定时器的返回值
     return function () {
         let _this = this 
         let args = arguments
         if(timer) {
           clearTimeout(timer) // 在delay内,清除上一个setTimeout
           timer = null
         }
         timer = setTimeout(function(){ // 创建一个新的setTimeout
             fn.apply(_this,args) // 绑定函数
         },delay)
     }
 }
 
// 节流:指定时间间隔内只会执行一次任务。

function throttle(fn,delay) {
    let isRun = false // 定义一个状态
    let timer 
    return function(){
        if(!isRun) {
            clearTimeout(timer) // 间隔时间内清除setTimeout
            return
        }
        isRun = false
        
        timer = setTimeout(function(){ // 创建一个新的setTimeout
             fn.apply(_this,args) // 绑定函数
             isRun = true
         },delay)
    }
}




10.2、location 地址栏对象

  • location.href = "url"页面跳转把当前页面替换成指定的url页面
  • location.href 获取当前打开页面的路径
  • location.assign("url") 页面跳转
  • location.replace("url")页面跳转
  • location = "url" 页面跳转
  • window.open() 页面跳转
  • location.reload() 页面刷新
  • history.go(0) 刷新
例一(浏览器每三秒自动刷新)
setIntervalfunction(){
    location.reload()
    ducument.write(Math.random())
},3000

10.3、history 历史记录对象

  • history.go(1) 前进 == history.forward()
  • history.go(-1) 后退 == history.back()
  • history.go(0) 刷新

10.4、document对象 页面对象(重点)

document.write() 浏览器打印

查找页面元素的方式 :

  • document.getElementById() 根据id查找页面元素 结果是唯一的
  • document.getElementsByTagName()根据给定的标签名称查找页面元素 结果是一个集合,使用的时候需要用下标
  • document.getElementsByClassName() 根据给定的类名 查找页面元素 结果是一个集合 兼容性
  • document.getElementsByName() 根据给定的name值查找页面元素 一般适用于表单元素 结果是集合
  • document.querySelector() 根据给定的css选择器查找元素 结果是唯一的,一般用于id选择器 兼容性
  • 用法 : document.querySelector("#id") .classname html标记
  • document.querySelectorAll() 根据给定的css选择器查找元素 结果是集合 兼容性
  • document.html 错误的

总结 : 查找的结果是一个类数组(伪数组)的方法有:

  • document.getElementsByTagName()
  • document.getElementsByName()
  • document.getElementsByClassName()
  • document.querySelectorAll()

10.5、对页面元素的操作(核心操作)

10.5.1、属性:

  • 设置属性 : obj.属性 = 值
  • 获取属性 : obj.属性
  • 获取自定义(非自定义)属性值 : getAttribute()
  • 设置属性 : setAttribute()
// 例一
var oImg = document.getElementsByTagName('img')[0]
alert(oImg.src)
// 使用js为html元素添加自定义属性
oImg.setAttribute('level',8)
console.log(oImg.getAttribute('level')) // 8

10.5.2、内容

innerHTML 操作标签;识别html标记;扩展:outerHTML改变了元素本身innerText 操作标签 ;文本内容操作:不识别html标记;value操作表单

**设置内容 **:

  • obj.innerHTML = "内容"
  • obj.innerText = "内容"
  • obj.value = "内容"

获取内容 :

  • obj.innerHTML
  • obj.innerText
  • obj.value
例一(向ul中写人10个li标签)
var oUl = document.querySelector('ul')
var str = ''
for(var i = 1; i < 10; i++){
    str += '<li>多个兄弟元素</li>'
}
oUl.innerHTML = str

10.5.2、样式

设置样式:

  • obj.style.样式 = 值;一次只能操作一个样式oDiv.style.color = "red"
  • obj.style.cssText = "css编写格式"(了解)
  • oDiv.style.cssText = "color:red;"
  • obj.className = "类名" 复用性好
var oDiv = document.querySelector('div')
// 获取样式
console.log(oDiv.style.color) //行内
console.log(window.getComputedStyle(oDiv)['width'])
// 设置样式
oDiv.style.cssText = 'color: red;font-size:16px'
oDiv.className = 'box'  // 添加样式类名

11、 网络请求与远程资源

11.1 、 axios

 Axios 是一个基于 promise 网络请求库,作用于node.js 和浏览器中。 它是 isomorphic 的(即同一套代码可以运行在浏览器和node.js中)。在服务端它使用原生 node.js http 模块, 而在客户端 (浏览端) 则使用 XMLHttpRequests。

// 我将直接用代码展示axios使用方法和基本配置

// axios.js文件

/***
 * 添加axios的全局配置,创建请求或新的实例可以引用该文件
****/

import axios from 'axios' // 引入

axios.defaults.timeout = 10000 //设置了在10000毫秒内请求数据,如果没有请求成功就执行错误函数
axios.defaults.headers.post['Content-Type'] = 'application/json;charset=UTF-8' // post方法设置媒体格式类型
axios.defaults.baseURL = '/realwarehouse/v1' //proxy代理地址

const CancelSourceMap = new Map() // 终止请求

// 全局请求拦截器
axios.interceptors.request.use(config => {
  // 发起请求时,取消掉当前正在进行的相同请求
  const source = axios.CancelToken.source()
  config.cancelToken = source.token
  if (CancelSourceMap.has(config.url)) {
    CancelSourceMap.get(config.url).cancel('取消重复请求')
    CancelSourceMap.set(config.url, source)
  } else {
    CancelSourceMap.set(config.url, source)
  }

  return config
}, error => {
  return Promise.reject(error)
})

// 全局响应拦截器
axios.interceptors.response.use(
  function (res) {
    // console.log(res, 'aaaaaaaaaaaaaaaaaaaaaa 全局响应拦截器res')
    // 请求本地文件
    if (res.config && res.config.baseURL === '/static') {
      return {
        statusCode: 200,
        data: res.data,
        message: '请求成功!',
      }
    }

    // 在这里对返回的数据进行处理
    if (res.data && res.data.statusCode === 200) {
      return res.data
    } else {
      if (res.data.statusCode === 401) {
        window.location.href = '/login'
      }
      return Promise.reject(res.data)
    }
  },
  function (err) {
    // console.log(err, err.response, 'aaaaaaaaaaaaaaaaaaaaaa 全局响应拦截器err')
    // 处理请求错误。根据与后端约定的code值进行不同的操作处理
    if (err && err.response) {
      switch (err.response.status) {
      case 400:
        err.message = '请求错误'
        break
      case 401:
        err.message = '未授权,请登录'
        break
      case 404:
        err.message = `请求地址出错: ${err.response.config.url}`
        break
      default:
      }
    }
    return Promise.reject(err)
  }
)
export default axios

// 页面api处理文件

/****
 * 描述:api-首页
 * 创建者:zj
****/

import axios from '@axios'
/**
 * 获取首页统计数据
 **/
export const getHomeOverviewData = () => {
  return axios({
    method: 'get', // 请求方式
    url: '/home/tab', // 请求地址
  })
}

/**
 * 获取帮助文档列表
 **/
export const getHelpFileList = (data) => {
  return axios({
    method: 'get',
    url: '/home/list',
    params: data  // params传参
  })
}

/**
 * 获取帮助文档内容
 * @param {object} data<可选>  // 请求传递的数据
 * @param {string} data.id<必须> // 帮助文档id
 * @param {string} data.type<必须>  // 帮助文档类型 sql-flinksql帮助文档 template-常用SQL模板
 **/
export const getHelpFile = (data) => {
  return axios({
    method: 'get',
    url: '/home/help',
    data              // // body传参
  })
}


上文只是简单介绍了axios常用方法,不太熟悉的,建议浏览  Axios官方文档

11.2 、替代性跨源技术

1、图片探测

图片探测是利用<img>标签实现跨域通信的最早的一种技术。任何页面都可以跨域加载图片而不必担心限制,因此这也是在线广告跟踪的主要方式。可以动态创建图片,然后通过它们的 onload 和onerror 事件处理程序得知何时收到响应。

let img = new Image();

img.onload = img.onerror = function() { 
    alert("Done!");
};

img.src = "http://www.example.com/test?name=Nicholas";

2、JSONP

JSONP 调用是通过动态创建<script>元素并为 src 属性指定跨域 URL 实现的。此时的<script> 与<img>元素类似,能够不受限制地从其他域加载资源。因为 JSONP 是有效的 JavaScript,所以 JSONP 响应在被加载完成之后会立即执行。

function handleResponse(response) { 
    console.log(`You're at IP address ${response.ip}, 
        which is in ${response.city}${response.region_name}`
     );
}
let script = document.createElement("script");
script.src = "http://freegeoip.net/json/?callback=handleResponse";
document.body.insertBefore(script, document.body.firstChild);

11.3、 web Soket

Web Socket(套接字)的目标是通过一个长时连接实现与服务器全双工、双向的通信。

11.3.1、 API

要创建一个新的 Web Socket,就要实例化一个 WebSocket 对象并传入提供连接的 URL:

let socket = new WebSocket("ws://www.example.com/server.php");
// 注意,必须给 WebSocket 构造函数传入一个绝对 URL。

WebSocket 也有一个readyState 属性表示当前状态。

  • WebSocket.OPENING(0):连接正在建立。
  • WebSocket.OPEN(1):连接已经建立。
  • WebSocket.CLOSING(2):连接正在关闭。
  • WebSocket.CLOSE(3):连接已经关闭。

11.3.2、 发送和接收数据

发送数据使用 send()方法并传入一个字符串、ArrayBuffer 或 Blob,如下所示:

let socket = new WebSocket("ws://www.example.com/server.php");

let stringData = "Hello world!";
let arrayBufferData = Uint8Array.from(['f''o''o']); 
let blobData = new Blob(['f''o', 'o']);

socket.send(stringData);
socket.send(arrayBufferData.buffer);
socket.send(blobData);

服务器向客户端发送消息时,WebSocket 对象上会触发 message 事件。这个 message 事件与其他消息协议类似,可以通过 event.data 属性访问到有效载荷:

socket.onmessage = function(event) { 

    let data = event.data;

    // 对数据执行某些操作
};