a.slice(2, 5); // 结果为[3, 4, 5],另外需要注意的是该方法不会修改数组,只是返回一个子数组,a 数组还是 [1, 2, 3, 4, 5, 6]
- splice() 方法,删除或替换当前数组的某些项目。
语法为:
arrayObject.splice(start, deleteCount, options);
// start 值是必需的,规定删除或替换项目的位置
// deleteCount 值是必需的,规定要删除的项目数量,如果设置为 0,则不会删除项目
// options 值是可选的,规定要替换的新项目
// 和 slice() 方法不同的是 splice() 方法会修改数组
在控制台中输入以下代码:
var a = [1, 2, 3, 4, 5, 6];
a.splice(2, 2, “abc”);
a; // 最终 a 数组变成了[1, 2, “abc”, 5, 6]
- sort() 方法,将数组进行排序。
语法为:
arrayObject.sort(sortby);
// sortby 是可选的,规定排序顺序,必需是函数。如果没有参数的话,将会按照字母顺序进行排序,更准确的说是按照字符编码(可自行百度了解)的顺序进行排序。如果想按照其他标准进行排序,则需要提供比较函数
例子:
运行结果为:
- toString() 方法,把数组转换为字符串,并返回结果。
String 对象
1.String 对象的常用属性:length,获取字符串的长度。
2. String 对象的常用方法:
- charAt() 方法,获取指定位置处字符。
语法为:
stringObject.charAt(index);
// 字符串中第一个字符的下标是 0。如果参数 index 不在 0 与 string.length 之间,该方法将返回一个空字符串
例子:
var str = “Hello world!”;
document.write(str.charAt(2));
// 以上代码输出为 l
- charCodeAt() 方法,获取指定位置处字符的 Unicode 编码。
语法为:
stringObject.charCodeAt(index);
// 字符串中第一个字符的下标是 0。如果 index 是负数,或大于等于字符串的长度,则 charCodeAt() 返回 NaN
例子:
var str = “Hello world!”;
document.write(str.charCodeAt(2));
// 以上代码输出为 l08
- oncat() 方法,连接字符串,等效于 “+”,“+” 更常用。与数组中的 concat() 方法相似。
- slice() 方法,提取字符串的片断,并在新的字符串中返回被提取的部分(字符串章节有详细介绍,这里不过多的赘述,下面的类似情况同样处理)。
- indexOf() 方法,检索字符串。
- toString() 方法,返回字符串。
- toLowerCase() 方法,把字符串转换为小写。
- toUpperCase() 方法,把字符串转换为大写。
- replace() 方法,替换字符串中的某部分。
- split() 方法,把字符串分割为字符串数组。
Date 对象
Date 对象方法:
- Date():返回当日的日期和时间(输出的是中国标准时间)。
- getDate():从 Date 对象返回一个月中的某一天 (1 ~ 31)。
- getDay():从 Date 对象返回一周中的某一天 (0 ~ 6)。
- getMonth():从 Date 对象返回月份 (0 ~ 11)。
- getFullYear():从 Date 对象以四位数字返回年份。
- getHours():返回 Date 对象的小时 (0 ~ 23)。
- getMinutes():返回 Date 对象的分钟 (0 ~ 59)。
- getSeconds():返回 Date 对象的秒数 (0 ~ 59)。
- getMilliseconds():返回 Date 对象的毫秒(0 ~ 999)。
Math 对象
Math 对象的常用属性:
- E :返回常数 e (2.718281828…)。
- LN2 :返回 2 的自然对数 (ln 2)。
- LN10 :返回 10 的自然对数 (ln 10)。
- LOG2E :返回以 2 为底的 e 的对数 (log2e)。
- LOG10E :返回以 10 为底的 e 的对数 (log10e)。
- PI :返回 π(3.1415926535…)。
- SQRT1_2 :返回 1/2 的平方根。
- SQRT2 :返回 2 的平方根。
Math 对象的常用方法:
- abs(x) :返回 x 的绝对值。
- round(x) :返回 x 四舍五入后的值。
- sqrt(x) :返回 x 的平方根。
- ceil(x) :返回大于等于 x 的最小整数。
- floor(x) :返回小于等于 x 的最大整数。
- sin(x) :返回 x 的正弦。
- cos(x) :返回 x 的余弦。
- tan(x) :返回 x 的正切。
- acos(x) :返回 x 的反余弦值(余弦值等于 x 的角度),用弧度表示。
- asin(x) :返回 x 的反正弦值。
- atan(x) :返回 x 的反正切值。
- exp(x) :返回 e 的 x 次幂 (e^x)。
- pow(n, m) :返回 n 的 m 次幂 (nm)。
- log(x) :返回 x 的自然对数 (ln x)。
- max(a, b) :返回 a, b 中较大的数。
- min(a, b) :返回 a, b 中较小的数。
- random() :返回大于 0 小于 1 的一个随机数。
创建对象的方式有很多,下面讲解几种常用的方式。
通过对象字面量来创建
var student = {
name: “zhangsan”,
age: 18,
gender: “male”,
sayHi: function () {
console.log("hi,my name is " + this.name);
},
};
把上面的代码复制到控制台中,然后尝试依次输入下面的代码看看效果:
student.name;
student.age;
student.gender; // 调用对象的属性
student.sayHi(); // 调用对象的方法
通过上面的例子会发现对象的属性和方法通过 “.” 来访问。
通过 new Object() 创建对象
var student = new Object();
(student.name = “zhangsan”),
(student.age = 18),
(student.gender = “male”),
(student.sayHi = function () {
console.log("hi,my name is " + this.name);
});
通过工厂函数创建对象
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”);
自定义构造函数
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”);
new 关键字
构造函数,是一种特殊的函数。主要用来在创建对象时初始化对象,即为对象成员变量赋初始值,总与 new 运算符一起使用在创建对象的语句中。
这里有需要特别注意的几点:
- 构造函数用于创建一类对象,首字母要大写。
- 内部使用 this 关键字给对象添加成员。
- 使用 new 关键字调用对象构造函数。
this 详解
在 JavaScript 中,我们经常会使用到 this 关键字,那么 this 到底指向什么呢?这里有一个口诀:谁调用 this,它就是谁。
- 函数在定义的时候 this 是不确定的,只有在调用的时候才可以确定。
- 一般函数直接执行,内部 this 指向全局 window。
比如:
function test() {
console.log(this);
}
test(); // window.test();
- 函数作为一个对象的方法,被该对象所调用,那么 this 指向的是该对象。
- 构造函数中的 this,始终是 new 的当前对象。
遍历对象的属性
通过 for…in 语句用于遍历数组或者对象的属性,对数组或者对象的属性进行循环操作。比如:
注:key 是一个变量,这个变量中存储的是该对象的所有的属性的名字。
删除对象的属性
使用 delete 删除对象的属性。比如在控制台中输入以下代码:
var student = {
name: “zhangsan”,
age: 18,
gender: “male”,
};
student.name; // zhangsan
delete student.name;
student.name; // undefined
============================================================================
API(Application Programming Interface,应用程序编程接口):“计算机操作系统”(Operating system)或"程序库"提供给应用程序调用使用的代码。其主要目的是让应用程序开发人员得以调用一组例程功能,而无须考虑其底层的源代码为何、或理解其内部工作机制的细节。API 本身是抽象的,它仅定义了一个接口,而不涉及应用程序在实际实现过程中的具体操作。
注:定义来自维基百科。
Web API
Web API 是浏览器提供的一套操作浏览器功能和页面元素的 API(BOM 和 DOM)。
浏览器对象模型(Browser Object Model (BOM))指的是由 Web 浏览器暴露的所有对象组成的表示模型。BOM 与 DOM(Document Object Model,文档对象模型)不同,其既没有标准的实现,也没有严格的定义,所以浏览器厂商可以自由地实现 BOM。
作为显示文档的窗口,浏览器程序将其视为对象的分层集合。当浏览器分析文档时,它将创建一个对象的集合,以定义文档,并详细说明它应如何显示。浏览器创建的对象称为文档对象,它是浏览器使用的更大的对象集合的一部分。此浏览器对象集合统称为浏览器对象模型或 BOM。
BOM 层次结构的顶层是窗口对象,它包含有关显示文档的窗口的信息。某些窗口对象本身就是描述文档和相关信息的对象。
注:定义来自维基百科。
window 是浏览器的顶级对象,当调用 window 下的属性和方法时,可以省略 window。
对话框
- alert():显示带有一段消息和一个确认按钮的警告框。
- prompt():显示可提示用户输入的对话框。
- confirm():显示带有一段消息以及确认按钮和取消按钮的对话框。
页面加载事件
- onload
window.onload = function () {
// 当页面加载完成执行
// 当页面完全加载所有内容(包括图像、脚本文件、CSS 文件等)执行
};
- onunload
window.onunload = function () {
// 当用户退出页面时执行
};
浏览器尺寸
var width = window.innerWidth;
document.documentElement.clientWidth;
document.body.clientWidth;
var height = window.innerHeight;
document.documentElement.clientHeight;
document.body.clientHeight;
上述代码可以获取所有浏览器的宽高(不包括工具栏/滚动条)。
定时器
- setTimeout() 方法在指定的毫秒数到达之后执行指定的函数,只执行一次。clearTimeout() 方法取消由 setTimeout() 方法设置的 timeout。
// 创建一个定时器,2000毫秒后执行,返回定时器的标示
var timerId = setTimeout(function () {
console.log(“Hello”);
}, 2000);
// 取消定时器的执行
clearTimeout(timerId);
- setInterval() 方法设置定时调用的函数也就是可以按照给定的时间(单位毫秒)周期调用函数,clearInterval() 方法取消由 setInterval() 方法设置的 timeout。
// 创建一个定时器,每隔 2 秒调用一次
var timerId = setInterval(function () {
var date = new Date();
console.log(date.toLocaleTimeString());
}, 2000);
// 取消定时器的执行
clearInterval(timerId);
注:BOM 的操作方法还有很多,但是一般情况下我们常用的就是上面所介绍的。有兴趣的可以自行百度了解 BOM 的更多操作方法和介绍。
文档对象模型(Document Object Model,简称 DOM),是 W3C 组织推荐的处理可扩展标志语言的标准编程接口。DOM 定义了访问 HTML 和 XML 文档的标准。我们这里主要学习 HTML DOM。DOM 可以把 HTML 看做是文档树,通过 DOM 提供的 API 可以对树上的节点进行操作。下面来看一下 W3C 上的 DOM 树:
DOM 能够操作 HTML 的内容。
改变 HTML 输出流
在 JavaScript 中,使用 document.write() 可用于直接向 HTML 输出流写内容。比如:
document.write(“新设置的内容
标签也可以生成
”);在控制台中复制上述代码运行后:
改变 HTML 内容
使用 innerHTML 属性改变 HTML 内容。比如修改 p 标签中的内容:
Hello World!
改变 HTML 属性
语法:
document.getElementById(id).attribute = new value();
DOM 能够改变 HTML 元素的样式。语法为:
document.getElementById(id).style.property = new style();
例子:
Hello
注:在上述例子中,p 标签中Hello的颜色本来为红色,但是通过 DOM 方法,最后将其改变成了绿色。运行上述代码,最终的效果是显示一个颜色为绿色的Hello文本。
根据 W3C 的 HTML DOM 标准,HTML 文档中的所有内容都是节点:整个文档就是一个文档节点,而每一个 HTML 标签都是一个元素节点。HTML 标签中的文本则是文本节点,HTML 标签的属性是属性节点,一切都是节点。
获取节点
要操作节点,首先我们要找到节点。主要有以下三种办法:
- 通过 ID 找到 HTML 元素:使用方法 getElementById() 通过元素的 ID 而选取元素,比如:
document.getElementById(“demo”); // 假定已经有一个 ID 名为 demo 的标签,可以这样来获取它
- 通过标签名找到 HTML 元素:使用方法 getElementsByTagName() 来选取元素,如果有多个同类型标签,那么我们可以通过下标来确认,比如:
- 通过类名来找到 HTML 元素:使用方法 getElementsByClassName() 通过元素的类名来选取元素。比如:
document.getElementsByClassName(“name”); // 返回包含 class = “name” 的所有元素的一个列表。
DOM 节点之间的关系
DOM 的节点并不是孤立的,我们从 DOM 树中也可以看出,节点与节点之间存在着相对的关系,就如同一个家族一样,有父辈,有兄弟,有儿子等等。下面我们来看一下都有哪些节点:
| 父节点 | 兄弟节点 | 子节点 | 所有子节点 |
| — | — | — | — |
| parentNode | nextSibling | firstChild | childNodes |
| | nextElementSibling | firstElementChild | children |
| | previousSibling | lastChild | |
| | previousElementSibling | lastElementChild | |
例子:
我是h1标签
我是p标签
上面的例子中:
- 节点没有父节点,它是根节点。
- 和 的父节点是 节点。
- 文本节点 我是 p 标签 的父节点是
节点。
- 节点有两个子节点: 和 。
节点和
节点是兄弟节点,同时也是 的子节点。
需要注意以下几点:
- childNodes:它是标准属性,它返回指定元素的子元素集合,包括 HTML 节点,所有属性,文本节点。
- children:非标准属性,它返回指定元素的子元素集合。但它只返回 HTML 节点,甚至不返回文本节点。
- nextSibling 和 previousSibling 获取的是节点,获取元素对应的属性是 nextElementSibling 和 previousElementSibling。
- nextElementSibling 和 previousElementSibling 有兼容性问题,IE9 以后才支持。
DOM 节点的操作
- 创建节点
- 创建元素节点:使用 createElement() 方法。比如:
var par = document.createElement(“p”);
-
创建属性节点:使用 createAttribute() 方法。
-
创建文本节点:使用 createTextNode() 方法。
- 插入子节点
-
appendChild () 方法向节点添加最后一个子节点。
-
insertBefore (插入的新的子节点,指定的子节点) 方法在指定的子节点前面插入新的子节点。如果第二个参数没写或者为 null,则默认插入到后面。
- 删除节点:使用 removeChild() 方法。写法为:
父节点.removeChild(子节点);
node.parentNode.removeChild(node); // 如果不知道父节点是什么,可以这样写
- 替换子节点:使用 replaceChild() 方法。语法为:
node.replaceChild(newnode, oldnode);
- 设置节点的属性:
-
获取:getAttribute(name)
-
设置:setAttribute(name, value)
-
删除:removeAttribute(name)
事件的定义
在什么时候执行什么事。
事件三要素
事件由:事件源 + 事件类型 + 事件处理程序组成。
- 事件源:触发事件的元素。
- 事件类型:事件的触发方式(比如鼠标点击或键盘点击)。
- 事件处理程序:事件触发后要执行的代码(函数形式,匿名函数)。
常用的事件
例子 1 :鼠标单击事件:
请点击该文本
例子 2 :鼠标双击事件:
请点击该文本
例子 3 :鼠标移除悬停:
οnmοuseοver=“mOver(this)”
οnmοuseοut=“mOut(this)”
style=“background-color:deepskyblue;width:200px;height:100px;”
把鼠标移到上面
参考源码:
首页
HTML
CSS
JavaScript
关于
帮助
=================================================================================
值类型
值类型又叫基本数据类型,在 JavaScript 中值类型有以下五种:
- 数值类型
- 布尔类型
- undefined
- null
- 字符串
值类型存储在栈(stack)中,它们的值直接存储在变量访问的位置。比如:
var num = 18;
var flag = true;
var un = undefined;
var nu = null;
var str = “zhangsan”;
上面定义的这些值类型的数据在内存中的存储如下图所示:
引用类型
引用类型又叫复合数据类型,在 JavaScript 中引用类型有以下三种:
- 对象
- 数组
- 函数
引用类型存储在堆中,也就是说存储在变量处的值是一个指针,指向存储对象的内存处。比如:
var arr = [1, 2, 3];
var p = { name: “张三”, age: 18 };
上面定义的这些引用类型的数据在内存中的存储如下图所示:
值类型的特征
- 值类型的值是不可变的,不可变是指值类型指向的空间不可变。比如:
var a = 2;
a = a + 2;
console.log(a); // 打印结果为 4。
在上述例子中,a 变量指向的值变了,但是 2 的内存没有变。
- 按值传递的变量之间互不影响。比如:
var a = 1;
var b = a;
a = a + 2;
console.log(a, b); // 打印结果为 3, 1
- 值类型赋值,直接将值赋值一份。比如:
var num1 = 10;
var num2 = num1;
上述代码在内存中的体现为:
- 当参数为值类型的时候,函数内和函数外的两个变量完全不同,仅仅只是存的值一样而已,修改时互不影响。比如:
function foo(num) {
num = num + 1;
}
var a = 1;
foo(a);
console.log(a); // 打印结果为 1
引用类型的特征
- 引用类型赋值,是将地址复制一份。比如:
var p = { name: “zhangsan”, age: 18 };
var p1 = p;
上述代码在内存中的体现为:
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); // 打印结果为李四
- 当参数为引用类型的时候,函数内和函数外的两个变量不同,但是共同指向同一个对象,在函数内修改对象数据时会影响外部。比如:
function foo(o) {
o.age = o.age + 1;
}
var p = { name: “zhangsan”, age: 18 };
foo§;
console.log(p.age); // 打印结果为 19。
注:可以这样理解。引用类型中的地址是一把钥匙,钥匙指向的是宝藏,复制一把钥匙后,两把钥匙能打开的是同一个宝藏。
在编写 JavaScript 代码时,如果遇见错误,首先在浏览器中运行我们的代码,然后 F12,查看错误信息。比如运行以下代码:
首先查看控制台信息,在控制台中会有报错提示,一般看这个就能知道问题是什么了。
在 Sources 中能够清楚的看到哪一行的代码出问题了,会有很明显地提醒。
如果我们想知道变量的值,在调试的时候,可以加一句 console.log(变量) 语句来打印出来,然后在控制台中看。console.log() 语句也是我们编程中经常需要使用的,因为有时候,我们也不能直观的一下就知道传递进来的值到底是什么,可能需要看半天的逻辑然后计算半天。直接打印出来的方式也有利于帮助我们判断,不过一般情况下大家记得调试完,要把这行语句注释掉或者删掉。
设置断点,逐步执行
首先运行上述代码,点击 F12 进行调试界面,点击 Sources,设置断点的方法很简单,直接左键点击代码旁边的数字行数。如下所示:
将变量添加到 Watch 窗口,实时查看它的值的变化,如下所示:
准备工作做好之后我们还需要点击一下刷新.然后就可以通过点击运行按钮,逐行运行我们设置断点时的代码,并在 Watch 中查看变量值的变化,如下所示:
在我们实际编程过程中,经常会遇到各种各样的错误,有可能是语法错误,也可能是拼写错误,也可能是浏览器的兼容性问题或者其它莫名其妙的问题。当出现错误时,JavaScript 引擎通常会停止,并生成一个错误信息。那我们应该怎么来调试我们的代码呢?
异常捕获
我们使用 try-catch 语句开捕获异常,语法为:
try {
// 这里写可能出现异常的代码
} catch (err) {
// 在这里写,出现异常后的处理代码
}
例子:
需要注意以下几点:
- 语句 try 和 catch 是成对出现的。
- 如果在 try 中出现了错误,try 里面出现错误的语句后面的代码都不再执行,直接跳转到 catch 中,catch 处理错误信息,然后再执行后面的代码。
- 如果 try 中没有出现错误,则不会执行 catch 中的代码,执行完 try 中的代码后直接执行后面的代码。
- 通过 try-catch 语句进行异常捕获之后,代码将会继续执行,而不会中断。
throw 语句
通过 throw 语句,我们可以创建自定义错误。throw 语句常常和 try catch 语句一起使用。
例子:
请输入 0 到 100 之间的数字:
测试输入值
===========================================================================
通过一个例子,来感受一下什么叫面向对象编程。比如我们设置页面中的 div 标签 和 p 标签的背景色为 color。如果按照我们前面所需我们可能会这样写:
你好吗?我很好
测试一下嘛好的啊
是不是觉得有点麻烦?好像有重复的?有的人可能会想到用函数来封装一下相同的代码:
你好吗?我很好
测试一下嘛好的啊
我们再来看看使用面向对象的方式:
你好吗?我很好
测试一下嘛好的啊
首先,我们来复习一下创建对象的方式。
- 通过对象字面量来创建。
var student = {
name: “zhangsan”,
age: 18,
gender: “male”,
sayHi: function () {
console.log("hi,my name is " + this.name);
},
};
- 通过 new Object() 创建对象。
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 个学生呢?显然如果这样做的话代码冗余率太高,是不可取的。我们也学过函数,所以简单方式的改进是:工厂函数。
- 通过工厂函数来创建对象。
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() 版本,而事实上,每个对象都共享一个函数。为了解决这个问题,我们引入面向对象编程里的一个重要概念:构造函数。
- 通过构造函数来创建对象。
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() 方法):
function Student(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
this.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); // 结果为 false
效果:
由于每个对象都是由 new Student 创建出来的,因此每创建一个对象,函数 sayHi() 都会被重新创建一次,这个时候,每个对象都拥有一个独立的,但是功能完全相同的方法,这样势必会造成内存浪费。有的人可能会想,既然是一样的那我们就单独把它提出来,写一个函数,每次调用不就可以了吗?比如:
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
但是这样做会导致全局变量增多,可能会引起命名冲突,代码结果混乱,维护困难。通过使用原型可以很好的解决这个问题。
原型:prototype
在 JavaScript 中,每一个函数都有一个 prototype 属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。我们来看看前面例子原型的写法:
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
我们之前提到过:每一个函数都有一个 prototype 属性,指向另一个对象。让我们用代码验证一下,在编辑器中输入以下代码:
上述代码在浏览器中打印结果为 Object,验证了我们所说的 prototype 属性,指向另一个对象。
构造函数的 prototype 对象默认都有一个 constructor 属性,指向 prototype 对象所在函数。在控制台中运行下面的代码:
function F() {}
console.log(F.prototype.constructor === F); // 结果为 ture
通过构造函数得到的实例对象内部会包含一个指向构造函数的 prototype 对象的指针 __ proto__。__ proto__属性最早是火狐浏览器引入的,用以通过实例对象来访问原型,这个属性在早期是非标准的属性。在控制台中运行下面的代码:
function F() {}
var a = new F();
console.log(a.proto === F.prototype); // 结果为 true
实例对象可以直接访问原型对象成员,所有实例都直接或间接继承了原型对象的成员。
总结:每个构造函数都有一个原型对象,原型对象包含一个指向构造函数的指针 constructor,而实例都包含一个指向原型对象的内部指针__proto__。
我们说过所有的对象都有原型,而原型也是对象,也就是说原型也有原型,那么如此下去,也就组成了我们的原型链。
属性搜索原则
属性搜索原则,也就是属性的查找顺序,在访问对象的成员的时候,会遵循以下原则:
- 首先从对象实例本身开始找,如果找到了这个属性或者方法,则返回。
- 如果对象实例本身没有找到,就从它的原型中去找,如果找到了,则返回。
- 如果对象实例的原型中也没找到,则从它的原型的原型中去找,如果找到了,则返回。
- 一直按着原型链查找下去,找到就返回,如果在原型链的末端还没有找到的话,那么如果查找的是属性则返回 undefined,如果查找的是方法则返回 xxx is not a function。
更简单的原型语法
在前面的例子中,我们是使用 xxx.prototype. 然后加上属性名或者方法名来写原型,但是每添加一个属性或者方法就写一次显得有点麻烦,因此我们可以用一个包含所有属性和方法的对象字面量来重写整个原型对象:
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 成员的指向正确,建议的写法是:
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 中也有继承。原型链继承的主要思想是利用原型让一个引用类型继承另外一个引用类型的属性和方法。
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;
常用的几个 Object.prototype 成员:
=========================================================================
在学习 call()、apply()、bind() 方法之前,我们先来复习一下 this 的指向问题,我们前面说过一个口诀:谁调用 this,它就指向谁。让我们先来看一个例子:
function foods() {}
foods.prototype = {
price: “¥15”,
say: function () {
console.log("My price is " + this.price);
},
};
var apple = new foods();
apple.say(); // My price is ¥15
var orange = new foods();
orange.say(); // My price is ¥15
也就是说上述例子调用 say() 方法,最后打印的结果都是一样的,但是如果我们想打印橘子的价钱是 10 元呢?又不想重新定义 say() 方法。JavaScript 为我们专门提供了一些函数方法用来帮我们更优雅的处理函数内部 this 指向问题。这就是接下来我们要学习的 call()、apply()、bind() 三个函数方法。
call
call() 方法调用一个函数, 其具有一个指定的 this 值和分别地提供的参数(参数的列表)。语法为:
fun.call(thisArg, arg1, arg2, …)
注:
- thisArg 指的是在 fun 函数中指定的 this 的值。如果指定了 null 或者 undefined 则内部 this 指向 window,同时值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的自动包装对象。是一个可选项。
- arg1, arg2, …指定的参数列表。也是可选项。
- 使用调用者提供的 this 值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined。
- call() 允许为不同的对象分配和调用属于一个对象的函数/方法。
- call() 提供新的 this 值给当前调用的函数/方法。你可以使用 call() 来实现继承:写一个方法,然后让另外一个新的对象来继承它(而不是在新对象中再写一次这个方法)。
- 使用 call() 方法调用函数并且指定上下文的 this。前面的例子可以改写成:
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
- 在一个子构造函数中,你可以通过调用父构造函数的 call() 方法来实现继承。在控制台输入如下代码:
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() 方法接受的是数组。语法为:
fun.apply(thisArg, [argsArray]);
- 使用 apply() 方法将数组添加到另一个数组。
例子:
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() 则可以连接两个数组。
- 使用 apply() 方法和内置函数。
例子:
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 关键字为提供的值。并在调用新函数时,将给定参数列表作为原函数的参数序列的前若干项。语法为:
fun.bind(thisArg[, arg1[, arg2[, …]]])
注:参数 thisArg:当绑定函数被调用时,该参数会作为原函数运行时的 this 指向。当使用 new 操作符调用绑定函数时,该参数无效。参数:arg1,arg2,…表示当目标函数被调用时,预先添加到绑定函数的参数列表中的参数。
我们创建一个简单的绑定函数例子:
var bin = function () {
console.log(this.x);
};
var foo = {
x: 10,
};
bin(); // undefined
var func = bin.bind(foo); // 创建一个新函数把 ‘this’ 绑定到 foo 对象
func(); // 10
我们再来看一个例子:
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 之间的整数相加的和:
function foo(n) {
if (n == 0) {
return 0;
} // 临界条件
else {
return n + foo(n - 1);
}
}
var a = foo(10);
a; // 55
注:一定要写临界条件,不然程序无法结束并且会报错。
作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。简单来说,作用域的值就是作用范围,也就是说一个变量或函数在什么地方可以使用,在什么地方不能使用。
块级作用域
在 JavaScript 中是没有块级作用域的。比如:
{
var num = 123;
{
console.log(num);
}
}
console.log(num);
上面的例子并不会报错,而是打印两次 123,但是在其他编程语言中(C#、C、JAVA)会报错,这是因为在 JavaScript 中是没有块级作用域。也就是说,使用 {} 标记出来的代码块中声明的变量 num,是可以被 {} 外面访问到的。
函数作用域
JavaScript 的函数作用域是指在函数内声明的所有变量在函数体内始终是可见的,不涉及赋值。来看个例子:
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();
我们把上面的代码 if 语句中的条件改为 2 == 2 来看看效果:
我们再来对比一下如果没有在函数体内会是什么效果:
全局作用域
全局作用域也就是说什么地方都能够访问到。比如我们不用 var 关键字,直接声明变量的话,那这个变量就是全局变量,它的作用域就是全局作用域。使用 window 全局对象来声明,全局对象的属性也是全局变量。另外在所有的函数外部用 var 声明的变量也是全局变量,这是因为内层作用域可以访问外层作用域。
注:
- 内层作用域可以访问外层作用域,反之不行。
- 整个代码结构中只有函数可以限定作用域。
- 如果当前作用规则中有名字了,就不考虑外面的同名变量。
- 作用域规则首先使用提升规则分析。
变量名提升
JavaScript 是解释型的语言,但是它并不是真的在运行的时候完完全全的逐句的往下解析执行。
例子:
func();
function func() {
console.log(“Hello Xnm”);
}
在控制台中运行效果为:
这说明了它并不是完全的逐句往下解析的,否则是会报错的。显然,在执行 func() 之前,引擎就已经解析到了 function func(){},发生了变量名提升。那么变量名提升是在什么时候发生的呢?JavaScript 引擎在对 JavaScript 代码进行解释执行之前,会对 JavaScript 代码进行预解析,在预解析阶段,会将以关键字 var 和 function 开头的语句块提前进行处理。当变量和函数的声明处在作用域比较靠后的位置的时候,变量和函数的声明会被提升到作用域的开头。也就是说上面的代码,我们可以理解为:
function func() {
console.log(“Hello Xnm”);
}
func();
再来看看变量声明的例子:
console.log(num);
var num = 10;
这里说的提示,是声明的提升,也就是说上面的代码,我们可以理解为:
var num; // 这里是声明
console.log(num); // 变量声明之后并未有初始化和赋值操作,所以这里是 undefined
num = 10; // 最终打印结果为 10
下面再来看几个复杂一点的例子。
函数同名的时候:
func();
function func() {
console.log(“Hello xnm”);
}
func();
function func() {
console.log(“hi xnm”);
} // 最终结果打印了两次 hi xnm
上面代码相当于:
function func() {
console.log(“Hello xnm”);
}
function func() {
console.log(“hi xnm”);
}
func();
func();
函数变量同名的时候:
console.log(foo);
function foo() {}
var foo = 6;
当出现变量声明和函数同名的时候,只会对函数声明进行提升,变量会被忽略。所以上面的代码相当于:
function foo() {}
console.log(foo);
foo = 6;
再来看一种:
var num = 1;
function num() {
alert(num);
}
num();
上面的代码相当于:
function num() {
alert(num);
}
num = 1;
num();
闭包是指函数可以使用函数之外定义的变量。
简单的闭包
在 JavaScript 中,使用全局变量是一个简单的闭包实例。比如:
var num = 3;
function foo() {
console.log(num);
}
foo(); //打印 3
复杂的闭包
function f1() {
var num1 = 6;
function f2() {
var num2 = 7;
}
console.log(num1 + num2);
}
f1();
在上述代码中函数 f2 能够访问到它外层的变量 num1,但是 f1 是不能访问 f2 中的变量num2,因此我们可以把 num2 作为 f2 的返回值,然后通过 f2 的返回值就可以访问到 sum2 了。
function f1() {
var num1 = 6;
function f2() {
var num2 = 7;
return num2;
}
console.log(num1 + f2());
}
f1();
在函数代码中,使用特殊对象 arguments,无需明确指出参数名,我们就能访问它们。第一个参数是 arguments[0],第二个参数是 arguments[1],以此类推。比如:
function foo() {
console.log(arguments[0]);
console.log(arguments[1]);
}
foo(2, 3); // 打印 2 3
还可以用 arguments 对象检测函数的参数个数,引用属性 arguments.length 即可。来看一个遍历参数求和的例子:
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() 对象创建函数的语法如下:
var function_name = new Function(arg1, arg2, …, argN, function_body)
注:每个参数都必须是字符串,function_body 是函数主体,也就是要执行的代码。
例子:
var add = new Function(“a”, “b”, “console.log(a+b);”);
add(2, 5); // 打印 7
再看一个例子:
var add = new Function(“a”, “b”, “console.log(a+b);”);
var doAdd = add;
doAdd(2, 5); // 打印 7
add(2, 5); // 打印 7
在上述例子中,变量 add 被定义为函数,然后 doAdd 被声明为指向同一个函数的指针。用这两个变量都可以执行该函数的代码,并输出相同的结果。因此,函数名只是指向函数的变量,那么我们可以把函数作为参数传递给另一个函数,比如下面的例子
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 属性声明了函数期望的参数个数。
例子:
var add = new Function(“a”, “b”, “console.log(a+b);”);
console.log(add.length); // 打印 2
Function 对象的方法
Function() 对象也有与所有对象共享的 valueOf() 方法和 toString() 方法。这两个方法返回的都是函数的源代码。
例子:
var add = new Function(“a”, “b”, “console.log(a+b);”);
add.valueOf();
add.toString();
总结
三套“算法宝典”
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
算法刷题LeetCode中文版(为例)
人与人存在很大的不同,我们都拥有各自的目标,在一线城市漂泊的我偶尔也会羡慕在老家踏踏实实开开心心养老的人,但是我深刻知道自己想要的是一年比一年有进步。
最后,我想说的是,无论你现在什么年龄,位于什么城市,拥有什么背景或学历,跟你比较的人永远都是你自己,所以明年的你看看与今年的你是否有差距,不想做咸鱼的人,只能用尽全力去跳跃。祝愿,明年的你会更好!
由于篇幅有限,下篇的面试技术攻克篇只能够展示出部分的面试题,详细完整版以及答案解析,有需要的可以关注