# HTML入土指南 ## DOM类 ### DOM类 HTML #### 修改id元素 document.getElementById("id").innerHTML = "新内容"; 使用例: ```html

Hello World!

``` #### 变更属性 document.getElementById(“id”).attribute = new value(); 使用例: ```html ``` ### DOM节点 整个文档就是一个文档节点 每一个 HTML 标签都是一个元素节点 HTML 标签中的文本则是文本节点 HTML 标签的属性是属性节点 #### 获取节点 1. 通过 ID 找到 HTML 元素:使用方法 `getElementById()` 通过元素的 ID 而选取元素,比如: ```javascript document.getElementById("要获取的元素id"); ``` 2. 通过标签名找到 HTML 元素:使用方法 getElementsByTagName() 来选取元素,如果有多个同类型标签,那么我们可以通过下标来确认,比如: ```html ``` #### DOM节点之间的关系 DOM 的节点并不是孤立的,节点与节点之间存在着相对的关系,就如同一个家族一样,有父辈,有兄弟,有儿子等等: | | | | | | ---------- | ---------------------- | ----------------- | ---------- | | 父节点 | 兄弟节点 | 子节点 | 所有子节点 | | parentNode | nextSibling | firstChild | childNodes | | | nextElementSibling | firstElementChild | children | | | previousSibling | lastChild | | | | previousElementSibling | lastElementChild | | 使用例: ```html DOM 节点演示

我是h1标签

我是p标签

``` 需要注意以下几点: - `childNodes`:它是标准属性,它返回指定元素的子元素集合,包括 HTML 节点,所有属性,文本节点。 - `children`:非标准属性,它返回指定元素的子元素集合。但它只返回 HTML 节点,甚至不返回文本节点。 - `nextSibling` 和 `previousSibling` 获取的是节点,获取元素对应的属性是 `nextElementSibling` 和 `previousElementSibling`。 - `nextElementSibling` 和 `previousElementSibling` 有兼容性问题,IE9 以后才支持。 #### DOM节点的操作 1. ##### 创建节点 - 创建元素节点:使用 `createElement()` 方法。比如: ```javascript var par = document.createElement("p"); ``` - 创建属性节点:使用 `createAttribute()` 方法。 - 创建文本节点:使用 `createTextNode()` 方法。 2. ###### 插入子节点 - `appendChild ()` 方法向节点添加最后一个子节点。 - `insertBefore` (插入的新的子节点,指定的子节点) 方法在指定的子节点前面插入新的子节点。如果第二个参数没写或者为 null,则默认插入到后面。 3. ###### 删除节点:使用 `removeChild()` 方法。写法为: ```javascript 父节点.removeChild(子节点); node.parentNode.removeChild(node); // 如果不知道父节点是什么,可以这样写 ``` 1. 替换子节点:使用 `replaceChild()` 方法。语法为: ```javascript node.replaceChild(newnode, oldnode); ``` 1. 设置节点的属性: - 获取:`getAttribute(name)` - 设置:`setAttribute(name, value)` - 删除:`removeAttribute(name)` ### DOM事件 #### 常用的事件 | 事件名 | 说明 | | ----------- | ------------------------------------ | | onclick | 鼠标单击 | | ondblclick | 鼠标双击 | | onkeyup | 按下并释放键盘上的一个键时触发 | | onchange | 文本内容或下拉菜单中的选项发生改变 | | onfocus | 获得焦点,表示文本框等获得鼠标光标。 | | onblur | 失去焦点,表示文本框等失去鼠标光标。 | | onmouseover | 鼠标悬停,即鼠标停留在图片等的上方 | | onmouseout | 鼠标移出,即离开图片等所在的区域 | | onload | 网页文档加载事件 | | onunload | 关闭网页时 | | onsubmit | 表单提交事件 | | onreset | 重置表单时 | 例子 1 :鼠标单击事件: ```html

请点击该文本

``` 例子 2 :鼠标双击事件: ```html

请点击该文本

``` 例子 3 :鼠标移除悬停: ```html
把鼠标移到上面
``` ## 值类型,引用类型、异常 ### 值类型 值类型又叫基本数据类型,在 JavaScript 中值类型有以下五种: - 数值类型 - 布尔类型 - undefined - null - 字符串 值类型存储在栈(stack)中,它们的值直接存储在变量访问的位置。比如: ```js var num = 18; var flag = true; var un = undefined; var nu = null; var str = "zhangsan"; ``` 上面定义的这些值类型的数据在内存中的存储如下图所示: ![1](https://doc.shiyanlou.com/document-uid897174labid9222timestamp1547607159775.png) ### 引用类型 引用类型又叫复合数据类型,在 JavaScript 中引用类型有以下三种: - 对象 - 数组 - 函数 引用类型存储在堆中,也就是说存储在变量处的值是一个指针,指向存储对象的内存处。比如: ```js var arr = [1, 2, 3]; var p = { name: "张三", age: 18 }; ``` 上面定义的这些引用类型的数据在内存中的存储如下图所示: ![1](https://doc.shiyanlou.com/document-uid897174labid9222timestamp1547609082688.png) #### 值类型的特征 - 值类型的值是不可变的,不可变是指值类型指向的空间不可变。比如: ```js var a = 2; a = a + 2; console.log(a); // 打印结果为 4。 ``` 在上述例子中,a 变量指向的值变了,但是 2 的内存没有变。 - 按值传递的变量之间互不影响。比如: ```js var a = 1; var b = a; a = a + 2; console.log(a, b); // 打印结果为 3, 1 ``` - 值类型赋值,直接将值赋值一份。比如: ```js var num1 = 10; var num2 = num1; ``` 上述代码在内存中的体现为: ![1](https://doc.shiyanlou.com/document-uid897174labid9222timestamp1547617637137.png) - 当参数为值类型的时候,函数内和函数外的两个变量完全不同,仅仅只是存的值一样而已,修改时互不影响。比如: ```js function foo(num) { num = num + 1; } var a = 1; foo(a); console.log(a); // 打印结果为 1 ``` #### 引用类型的特征 - 引用类型赋值,是将地址复制一份。比如: ```js var p = { name: "zhangsan", age: 18 }; var p1 = p; ``` 上述代码在内存中的体现为: ![1](https://doc.shiyanlou.com/document-uid897174labid9222timestamp1547618081944.png) 再来看一段代码: ```js var p = { name: "张三", age: 18 }; var p1 = p; console.log(p.name); // 打印结果为张三 console.log(p1.name); // 打印结果为张三 p.name = "李四"; console.log(p.name); // 打印结果为李四 console.log(p1.name); // 打印结果为李四 ``` - 当参数为引用类型的时候,函数内和函数外的两个变量不同,但是共同指向同一个对象,在函数内修改对象数据时会影响外部。比如: ```js function foo(o) { o.age = o.age + 1; } var p = { name: "zhangsan", age: 18 }; foo(p); console.log(p.age); // 打印结果为 19。 ``` 注:其实我们可以这样理解。引用类型中的地址是一把钥匙,钥匙指向的是宝藏,复制一把钥匙后,两把钥匙能打开的是同一个宝藏。 ### 异常处理 #### 异常捕获 我们使用 `try-catch` 语句开捕获异常,语法为: ```javascript try { // 这里写可能出现异常的代码 } catch (err) { // 在这里写,出现异常后的处理代码 } ``` 例子: ```html ``` 需要注意以下几点: - 语句 `try` 和 `catch` 是成对出现的。 - 如果在 `try` 中出现了错误,`try` 里面出现错误的语句后面的代码都不再执行,直接跳转到 `catch` 中,`catch` 处理错误信息,然后再执行后面的代码。 - 如果 `try` 中没有出现错误,则不会执行 `catch` 中的代码,执行完 `try` 中的代码后直接执行后面的代码。 - 通过 `try-catch` 语句进行异常捕获之后,代码将会继续执行,而不会中断。 #### throw 语句 通过 `throw` 语句,我们可以创建自定义错误。`throw` 语句常常和 `try catch` 语句一起使用。 例子: ```html

请输入 0 到 100 之间的数字:

``` ## 面向对象编程 下面我们通过一个例子,来感受一下什么叫面向对象编程。比如我们设置页面中的 div 标签 和 p 标签的背景色为 color。如果按照我们前面所需我们可能会这样写: ```html
你好吗?

我很好

测试一下嘛

好的啊

``` 是不是觉得有点麻烦?好像有重复的?有的人可能会想到用函数来封装一下相同的代码: ```html
你好吗?

我很好

测试一下嘛

好的啊

``` 我们再来看看使用面向对象的方式: ```html
你好吗?

我很好

测试一下嘛

好的啊

``` ### 构造函数 首先,我们来复习一下创建对象的方式。 1. 通过对象字面量来创建。 ```js var student = { name: "zhangsan", age: 18, gender: "male", sayHi: function () { console.log("hi,my name is " + this.name); }, }; ``` 1. 通过 new Object() 创建对象。 ```js var student = new Object(); (student.name = "zhangsan"), (student.age = 18), (student.gender = "male"), (student.sayHi = function () { console.log("hi,my name is " + this.name); }); ``` 上面两种都是简单的创建对象的方式,但是如果有两个 student 实例对象呢?cv(Ctrl C + Ctrl V) 大法?分别命名一下 student1 和 student2?那如果是一个班的学生,n 个学生呢?显然如果这样做的话代码冗余率太高,是不可取的。我们也学过函数,所以简单方式的改进是:工厂函数。 1. 通过工厂函数来创建对象。 ```js function createStudent(name, age, gender) { var student = new Object(); student.name = name; student.age = age; student.gender = gender; student.sayHi = function () { console.log("hi,my name is " + this.name); }; return student; } var s1 = createStudent("zhangsan", 18, "male"); var s2 = createStudent("lisi", 19, "male"); ``` 这样封装代码确实解决了代码冗余的问题,但是每次调用函数 `createStudent()` 都会创建新函数 `sayHi()`,也就是说每个对象都有自己的 `sayHi()` 版本,而事实上,每个对象都共享一个函数。为了解决这个问题,我们引入面向对象编程里的一个重要概念:构造函数。 1. 通过构造函数来创建对象。 ```js function Student(name, age, gender) { this.name = name; this.age = age; this.gender = gender; this.sayHi = function () { console.log("hi,my name is " + this.name); }; } var s1 = new Student("zhangsan", 18, "male"); ``` 来看看构造函数与工厂函数的区别: - 首先在构造函数内没有创建对象,而是使用 `this` 关键字,将属性和方法赋给了 `this` 对象。 - 构造函数内没有 `return` 语句,`this` 属性默认下是构造函数的返回值。 - 函数名使用的是大写的 Student。 - 用 `new` 运算符和类名 Student 创建对象。 ### 构造函数存在的问题 构造函数虽然科学,但仍然存在一些问题。 我们使用前面的构造函数例子来讲解(修改了 `sayHi()` 方法): ```js function Student(name, age, gender) { this.name = name; this.age = age; this.gender = gender; this.sayHi = function () { console.log("hi"); }; } ``` 首先我们创建两个实例化对象: ```js var s1 = new Student("zhangsan", 18, "male"); s1.sayHi(); // 打印 hi var s2 = new Student("lisi", 18, "male"); s2.sayHi(); // 打印 hi console.log(s1.sayHi == s2.sayhi); // 结果为 false ``` 眼见为实,来看看效果: ![1](https://doc.shiyanlou.com/document-uid897174labid9222timestamp1547694897490.png) 由于每个对象都是由 new Student 创建出来的,因此每创建一个对象,函数 `sayHi()` 都会被重新创建一次,这个时候,每个对象都拥有一个独立的,但是功能完全相同的方法,这样势必会造成内存浪费。有的人可能会想,既然是一样的那我们就单独把它提出来,写一个函数,每次调用不就可以了吗?比如: ```js function sayHi() { console.log("hi"); } function Student(name, age, gender) { this.name = name; this.age = age; this.gender = gender; this.sayHi = sayHi; } var s1 = new Student("zhangsan", 18, "male"); s1.sayHi(); // 打印 hi var s2 = new Student("lisi", 18, "male"); s2.sayHi(); // 打印 hi console.log(s1.sayHi == s2.sayHi); // 结果为 true ``` ![1](https://doc.shiyanlou.com/document-uid897174labid9222timestamp1547702361417.png) 但是这样做会导致全局变量增多,可能会引起命名冲突,代码结果混乱,维护困难。通过使用原型可以很好的解决这个问题。 ### 原型 在 JavaScript 中,每一个函数都有一个 `prototype` 属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。我们来看看前面例子原型的写法: ```js function Student(name, age, gender) { this.name = name; this.age = age; this.gender = gender; } Student.prototype.sayHi = function () { console.log("hi"); }; var s1 = new Student("zhangsan", 18, "male"); s1.sayHi(); // 打印 hi var s2 = new Student("lisi", 18, "male"); s2.sayHi(); // 打印 hi console.log(s1.sayHi == s2.sayHi); // 结果为 true ``` ![1](https://doc.shiyanlou.com/document-uid897174labid9222timestamp1547702473320.png) ### 构造函数、实例、原型三者之间的关系 我们之前提到过:每一个函数都有一个 `prototype` 属性,指向另一个对象。让我们用代码验证一下,在编辑器中输入以下代码: ```html ``` 上述代码在浏览器中打印结果为 Object,验证了我们所说的 `prototype` 属性,指向另一个对象。 构造函数的 `prototype` 对象默认都有一个 `constructor` 属性,指向 `prototype` 对象所在函数。在控制台中运行下面的代码: ```js function F() {} console.log(F.prototype.constructor === F); // 结果为 ture ``` 通过构造函数得到的实例对象内部会包含一个指向构造函数的 `prototype` 对象的指针 `__proto__`。`__proto__` 属性最早是火狐浏览器引入的,用以通过实例对象来访问原型,这个属性在早期是非标准的属性。在控制台中运行下面的代码: ```js function F() {} var a = new F(); console.log(a.__proto__ === F.prototype); // 结果为 true ``` 实例对象可以直接访问原型对象成员,所有实例都直接或间接继承了原型对象的成员。 总结:每个构造函数都有一个原型对象,原型对象包含一个指向构造函数的指针 `constructor`,而实例都包含一个指向原型对象的内部指针`__proto__`。 ### 原型链 我们说过所有的对象都有原型,而原型也是对象,也就是说原型也有原型,那么如此下去,也就组成了我们的原型链。 #### 属性搜索原则 属性搜索原则,也就是属性的查找顺序,在访问对象的成员的时候,会遵循以下原则: - 首先从对象实例本身开始找,如果找到了这个属性或者方法,则返回。 - 如果对象实例本身没有找到,就从它的原型中去找,如果找到了,则返回。 - 如果对象实例的原型中也没找到,则从它的原型的原型中去找,如果找到了,则返回。 - 一直按着原型链查找下去,找到就返回,如果在原型链的末端还没有找到的话,那么如果查找的是属性则返回 undefined,如果查找的是方法则返回 `xxx is not a function`。 #### 更简单的原型语法 在前面的例子中,我们是使用 `xxx.prototype.` 然后加上属性名或者方法名来写原型,但是每添加一个属性或者方法就写一次显得有点麻烦,因此我们可以用一个包含所有属性和方法的对象字面量来重写整个原型对象: ```js function Student(name, age, gender) { this.name = name; this.age = age; this.gender = gender; } Student.prototype = { hobby: "study", sayHi: function () { console.log("hi"); }, }; var s1 = new Student("wangwu", 18, "male"); console.log(Student.prototype.constructor === Student); // 结果为 false ``` 但是这样写也有一个问题,那就是原型对象丢失了 `constructor` 成员。所以为了保持 constructor 成员的指向正确,建议的写法是: ```js function Student(name, age, gender) { this.name = name; this.age = age; this.gender = gender; } Student.prototype = { constructor: Student, // 手动将 constructor 指向正确的构造函数 hobby: "study", sayHi: function () { console.log("hi"); }, }; var s1 = new Student("wangwu", 18, "male"); console.log(Student.prototype.constructor === Student); // 结果为 true ``` ### 原型链继承 我们都听过这么一句话:子承父业。而在我们的 JavaScript 中也有继承,接下来我们会学习原型链继承。原型链继承的主要思想是利用原型让一个引用类型继承另外一个引用类型的属性和方法。 ```js function Student(name, age, gender) { this.name = name; this.age = age; this.gender = gender; } Student.prototype.sayHi = function () { console.log("hi"); }; var s1 = new Student("zhangsan", 18, "male"); s1.sayHi(); // 打印 hi var s2 = new Student("lisi", 18, "male"); s2.sayHi(); // 打印 hi ``` 上述例子中实例化对象 s1 和 s2 都继承了 `sayHi()` 方法。 ## Object.prototype 成员介绍 在控制台中输入以下代码: ```js Object.prototype; ``` ![1](https://doc.shiyanlou.com/document-uid897174labid9222timestamp1547709166287.png) 我们介绍常用的几个 Object.prototype 成员: ![1](https://doc.shiyanlou.com/document-uid897174labid9222timestamp1547709733167.png) ## 函数进阶 ### call,apply,bind #### call `call()` 方法调用一个函数, 其具有一个指定的 `this` 值和分别地提供的参数(参数的列表)。语法为: ```js fun.call(thisArg, arg1, arg2, ...) ``` 注: - `thisArg` 指的是在 `fun` 函数中指定的 `this` 的值。如果指定了 null 或者 undefined 则内部 `this` 指向 window,同时值为原始值(数字,字符串,布尔值)的 `this` 会指向该原始值的自动包装对象。是一个可选项。 - arg1, arg2, ...指定的参数列表。也是可选项。 - 使用调用者提供的 `this` 值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined。 - `call()` 允许为不同的对象分配和调用属于一个对象的函数/方法。 - `call()` 提供新的 `this` 值给当前调用的函数/方法。你可以使用 `call()` 来实现继承:写一个方法,然后让另外一个新的对象来继承它(而不是在新对象中再写一次这个方法)。 1. 使用 `call()` 方法调用函数并且指定上下文的 `this`。前面的例子可以改写成: ```js function foods() {} foods.prototype = { price: "¥15", say: function () { console.log("My price is " + this.price); }, }; var apple = new foods(); orange = { price: "¥10", }; apple.say.call(orange); // My price is ¥10 ``` 2. 在一个子构造函数中,你可以通过调用父构造函数的 `call()` 方法来实现继承。在控制台输入如下代码: ```js function Father(name, age) { this.name = name; this.age = age; } function Son(name, age) { Father.call(this, name, age); this.hobby = "study"; } var S1 = new Son("zhangsan", 18); S1; // Son {name: "zhangsan", age: 18, hobby: "study"} ``` #### apply `apply()` 方法与 `call()` 方法类似,唯一的区别是 `call()` 方法接受的是参数,`apply()` 方法接受的是数组。语法为: ```js fun.apply(thisArg, [argsArray]); ``` 1. 使用 `apply()` 方法将数组添加到另一个数组。 例子: ```js var array = ["a", "b", "c"]; var nums = [1, 2, 3]; array.push.apply(array, nums); array; // ["a", "b", "c", 1, 2, 3] ``` > 注:concat() 方法连接数组,不会改变原数组,而是创建一个新数组。而使用 push() 是接受可变数量的参数的方式来添加元素。使用 apply() 则可以连接两个数组。 1. 使用 `apply()` 方法和内置函数。 例子: ```js var numbers = [7, 10, 2, 1, 11, 9]; var max = Math.max.apply(null, numbers); max; // 11 ``` > 注:直接使用 max() 方法的写法为:Math.max(7, 10, 2, 1, 11, 9); #### bind `bind()` 方法创建一个新的函数(称为绑定函数),在调用时设置 `this` 关键字为提供的值。并在调用新函数时,将给定参数列表作为原函数的参数序列的前若干项。语法为: ```js fun.bind(thisArg[, arg1[, arg2[, ...]]]) ``` 注:参数 thisArg:当绑定函数被调用时,该参数会作为原函数运行时的 `this` 指向。当使用 `new` 操作符调用绑定函数时,该参数无效。参数:arg1,arg2,...表示当目标函数被调用时,预先添加到绑定函数的参数列表中的参数。 我们创建一个简单的绑定函数例子: ```js var bin = function () { console.log(this.x); }; var foo = { x: 10, }; bin(); // undefined var func = bin.bind(foo); // 创建一个新函数把 'this' 绑定到 foo 对象 func(); // 10 ``` 我们再来看一个例子: ```js this.num = 6; var test = { num: 66, getNum: function () { return this.num; }, }; test.getNum(); // 返回 66 var newTest = test.getNum; newTest(); // 返回 6, 在这种情况下,"this"指向全局作用域 // 创建一个新函数,将"this"绑定到 test 对象 var bindgetNum = newTest.bind(test); bindgetNum(); // 返回 66 var newTest = test.getNum; newTest(); // 上面这两行代码其实相当于: var newTest(){ return this.num; } // 所以 this 指向的是全局作用域,返回 6。 ``` ### 递归 在程序中,递归就是函数自己直接或者间接的调用自己。 例子:计算 1 到 10 之间的整数相加的和: ```js function foo(n) { if (n == 0) { return 0; } // 临界条件 else { return n + foo(n - 1); } } var a = foo(10); a; // 55 ``` 注:一定要写临界条件,不然程序无法结束并且会报错。 使用例: ```html //斐波那契数列 ``` ### 作用域 作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。简单来说,作用域的值就是作用范围,也就是说一个变量或函数在什么地方可以使用,在什么地方不能使用。 #### 块级作用域 在 JavaScript 中是没有块级作用域的。比如: ```js { var num = 123; { console.log(num); } } console.log(num); ``` 上面的例子并不会报错,而是打印两次 123,但是在其他编程语言中(C#、C、JAVA)会报错,这是因为在 JavaScript 中是没有块级作用域。也就是说,使用 `{}` 标记出来的代码块中声明的变量 `num`,是可以被 `{}` 外面访问到的。 #### 函数作用域 JavaScript 的函数作用域是指在函数内声明的所有变量在函数体内始终是可见的,不涉及赋值。来看个例子: ```js function test() { var num = 123; console.log(num); if (2 == 3) { var k = 5; for (var i = 0; i < 10; i++) {} console.log(i); } console.log(k); // 不会报错,而是显示 undefined,也就意味着被定义后的变量会存在,但是在该函数的内部,变量的外部只能访问到变量,无法得到值 } test(); ``` ![1](https://doc.shiyanlou.com/document-uid897174labid9222timestamp1547884049674.png) 我们把上面的代码 `if` 语句中的条件改为 `2 == 2` 来看看效果: ![1](https://doc.shiyanlou.com/document-uid897174labid9222timestamp1547884177854.png) 我们再来对比一下如果没有在函数体内会是什么效果: ![1](https://doc.shiyanlou.com/document-uid897174labid9222timestamp1547884380455.png) #### 全局作用域 全局作用域也就是说什么地方都能够访问到。比如我们不用 `var` 关键字,直接声明变量的话,那这个变量就是全局变量,它的作用域就是全局作用域。使用 window 全局对象来声明,全局对象的属性也是全局变量。另外在所有的函数外部用 `var` 声明的变量也是全局变量,这是因为内层作用域可以访问外层作用域。 注: - 内层作用域可以访问外层作用域,反之不行。 - 整个代码结构中只有函数可以限定作用域。 - 如果当前作用规则中有名字了,就不考虑外面的同名变量。 - 作用域规则首先使用提升规则分析。 #### 变量名提升 JavaScript 是解释型的语言,但是它并不是真的在运行的时候完完全全的逐句的往下解析执行。我们来看个例子: ```js func(); function func() { console.log("Hello syl"); } ``` 在控制台中运行效果为: ![1](https://doc.shiyanlou.com/document-uid897174labid9222timestamp1547797629301.png) 这说明了它并不是完全的逐句往下解析的,否则是会报错的。显然,在执行 `func()` 之前,引擎就已经解析到了 `function func(){}`,发生了变量名提升。那么变量名提升是在什么时候发生的呢?JavaScript 引擎在对 JavaScript 代码进行解释执行之前,会对 JavaScript 代码进行预解析,在预解析阶段,会将以关键字 `var` 和 `function` 开头的语句块提前进行处理。当变量和函数的声明处在作用域比较靠后的位置的时候,变量和函数的声明会被提升到作用域的开头。也就是说上面的代码,我们可以理解为: ```js function func() { console.log("Hello syl"); } func(); ``` 再来看看变量声明的例子: ```js console.log(num); var num = 10; ``` ![1](https://doc.shiyanlou.com/document-uid897174labid9222timestamp1547798103346.png) 结果可能有些出人意料,但是我们这里说的提示,是声明的提升,也就是说上面的代码,我们可以理解为: ```js var num; // 这里是声明 console.log(num); // 变量声明之后并未有初始化和赋值操作,所以这里是 undefined num = 10; // 最终打印结果为 10 ``` 下面再来看几个复杂一点的例子。 函数同名的时候: ```js func(); function func() { console.log("Hello syl"); } func(); function func() { console.log("hi syl"); } // 最终结果打印了两次 hi syl ``` 上面代码相当于: ```js function func() { console.log("Hello syl"); } function func() { console.log("hi syl"); } func(); func(); ``` 函数变量同名的时候: ```js console.log(foo); function foo() {} var foo = 6; ``` 当出现变量声明和函数同名的时候,只会对函数声明进行提升,变量会被忽略。所以上面的代码相当于: ```js function foo() {} console.log(foo); foo = 6; ``` 我们再来看一种: ```js var num = 1; function num() { alert(num); } num(); ``` ![1](https://doc.shiyanlou.com/document-uid897174labid9222timestamp1547800169327.png) 上面的代码相当于: ```js function num() { alert(num); } num = 1; num(); ``` 下面我们来看一个思考题: ```js var num = 3; function foo() { console.log(num); var num = 4; console.log(num); } foo(); ``` 注:大家可以先思考一下,然后根据我们前面所学的变量名提升,试着写一下等价的代码,最后再复制上述代码到控制台中运行看看结果。 上面的代码相当于: ```js var num = 3; function foo() { var num; // 在函数顶部声明了局部变量,覆盖了函数体外同名的全局变量 console.log(num); // 变量存在,但是它的值为 undefined num = 4; // 将其初始化赋值。 console.log(num); //打印我们期望的值 4 } foo(); ``` #### 闭包 闭包是指函数可以使用函数之外定义的变量。 #### 简单的闭包 在 JavaScript 中,使用全局变量是一个简单的闭包实例。比如: ```js var num = 3; function foo() { console.log(num); } foo(); //打印 3 ``` #### 复杂的闭包 ```js function f1() { var num1 = 6; function f2() { var num2 = 7; } console.log(num1 + num2); } f1(); ``` ![1](https://doc.shiyanlou.com/document-uid897174labid9222timestamp1547804618008.png) 在上述代码中函数 `f2` 能够访问到它外层的变量 `num1`,但是 `f1` 是不能访问 `f2` 中的变量`num2`,因此我们可以把 `num2` 作为 `f2` 的返回值,然后通过 `f2` 的返回值就可以访问到 `sum2` 了。 ```js function f1() { var num1 = 6; function f2() { var num2 = 7; return num2; } console.log(num1 + f2()); } f1(); ``` ![图片描述](https://dn-simplecloud.shiyanlou.com/questions/uid810810-20220224-1645674408258) #### arguments 对象 在函数代码中,使用特殊对象 `arguments`,无需明确指出参数名,我们就能访问它们。第一个参数是 `arguments[0]`,第二个参数是 `arguments[1]`,以此类推。比如: ```js function foo() { console.log(arguments[0]); console.log(arguments[1]); } foo(2, 3); // 打印 2 3 ``` 还可以用 `arguments` 对象检测函数的参数个数,引用属性 `arguments.length` 即可。来看一个遍历参数求和的例子: ```js function add() { var sum = 0; for (var i = 0; i < arguments.length; i++) { sum += arguments[i]; } return sum; } add(); // 0 add(1); // 1 add(1, 2); // 3 add(1, 2, 3); // 6 ``` #### Function 对象 用 `Function()` 对象创建函数的语法如下: ```js var function_name = new Function(arg1, arg2, ..., argN, function_body) ``` 注:每个参数都必须是字符串,`function_body` 是函数主体,也就是要执行的代码。 例子: ```js var add = new Function("a", "b", "console.log(a+b);"); add(2, 5); // 打印 7 ``` 再看一个例子: ```js var add = new Function("a", "b", "console.log(a+b);"); var doAdd = add; doAdd(2, 5); // 打印 7 add(2, 5); // 打印 7 ``` 在上述例子中,变量 `add` 被定义为函数,然后 `doAdd` 被声明为指向同一个函数的指针。用这两个变量都可以执行该函数的代码,并输出相同的结果。因此,函数名只是指向函数的变量,那么我们可以把函数作为参数传递给另一个函数,比如下面的例子: ```js function addF(foo, b, c) { foo(b, c); } var add = new Function("a", "b", "console.log(a+b);"); addF(add, 2, 5); // 打印 7 ``` #### Function 对象的 length 属性 函数属于引用类型,所以它们也有属性和方法。`length` 属性声明了函数期望的参数个数。 例子: ```js var add = new Function("a", "b", "console.log(a+b);"); console.log(add.length); // 打印 2 ``` #### Function 对象的方法 `Function()` 对象也有与所有对象共享的 `valueOf()` 方法和 `toString()` 方法。这两个方法返回的都是函数的源代码。 例子: ```js var add = new Function("a", "b", "console.log(a+b);"); add.valueOf(); add.toString(); ``` ![1](https://doc.shiyanlou.com/document-uid897174labid9222timestamp1547866078578.png) ![1](https://doc.shiyanlou.com/document-uid897174labid9222timestamp1547866243634.png) #### Event对象 event 对象,是事件对象,通过事件对象的属性,我们可以了解一个事件的详细情况。 常用的 event 对象属性如下表所示: | 属性 | 说明 | | ------------------------------------------------------------ | ----------------- | | type | 查看事件类型 | | [keyCode](https://developer.mozilla.org/zh-CN/docs/Web/API/KeyboardEvent/keyCode) | 查看键值码 | | shiftKey | 是否按下 shift 键 | | ctrlkey | 是否按下 ctrl 键 | | altkey | 是否按下 alt 键 | > keyCode 属性已经从 Web 标准中删除,这里了解即可。 ### 原生AJAX AJAX 的英文全称为 Asynchronous JavaScript And XML,Asynchronous 是异步的意思。何为异步呢?在这里异步是指通过 AJAX 向服务器请求数据,在不刷新整个页面的情况下,更新页面上的部分内容。 其工作原理图如下所示: ![图片描述](https://doc.shiyanlou.com/courses/3773/1347963/1389bcd14fc2ba77b5f8343420f96304-0) 用户在浏览器执行一些操作,通过 AJAX 引擎发送 HTTP 请求到服务器请求数据,请求成功后,由服务器把请求的数据拿给 AJAX 引擎,再由 AJAX 拿给浏览器。AJAX 在这个过程中相当于是服务员,用户点好菜,由服务员把菜单交给厨师,厨师做好菜,由服务员把菜送到用户的餐桌上。 同学们可能会想,我直接把菜单交给厨师,省去中间人沟通岂不是更简单? 浏览器如果直接向服务器请求数据的话,在请求过程中,你是不能对页面进行其他操作的,这叫同步请求,而把请求数据这个活外包给 AJAX 后,在请求过程中,用户还是可以对页面进行其他操作,这就是我们的异步请求了,也是 AJAX 的核心特点。 #### 创建AJAX的基本步骤 **1. 创建 XMLHttpRequest 对象** 在 AJAX 中,`XMLHttpRequest` 对象是用来与服务器进行数据交换的。其创建如下所示: ```js var httpRequest = new XMLHttpRequest(); ``` 为了保证浏览器的兼容性,我们可以用以下方式来创建。 ```js if (window.XMLHttpRequest) { // Mozilla,Safari,IE7+ 等浏览器适用 httpRequest = new XMLHttpRequest(); } else if (window.ActiveXObject) { // IE 6 或者更老的浏览器适用 httpRequest = new ActiveXObject("Microsoft.XMLHTTP"); } ``` **2.向服务器发送请求** 在步骤一中我们已经创建了用于服务器交换数据的 `XMLHttpRequest` 对象,要向服务器发送请求,我们需要调用该对象中的 `open` 和 `send` 方法。 其使用如下: ```js // 规定发送请求的一些要求 httpRequest.open("method", "url", async); // 将请求发送到服务器 httpRequest.send(); ``` `open` 方法中的参数说明如下: - `method` 是请求的类型,常见的有 `GET` 和 `POST`。 - `url` 是请求的地址。 - `async` 是设置同步或者异步请求,其值为布尔类型,当为 true 时,使用异步请求;当为 false 时,使用同步请求,默认为 true。 **3.服务器响应状态** 我们使用 HTTP 请求数据后,请求是否成功,会反馈给我们相应的请求状态。我们使用 `onreadystatechange` 去检查响应的状态,当 `httpRequest.readyState` 为 4 并且 `httpRequest.status` 等于 200 时,说明数据请求成功,其使用如下: ```js httpRequest.onreadystatechange = function () { if (httpRequest.readyState == 4 && httpRequest.status == 200) { // 请求成功执行的代码 } else { // 请求失败执行的代码 } }; ``` ``