JavaScript解析与执行过程
JavaScript解析和执行过程介绍
js的执行过程分为两大部分:
- 解析过程,也称预编译期 。
- 主要工作:对于js的代码中声明的所有变量和函数进行预处理。
- 仅是声明函数开辟出一块内存空间,不进行赋值操作。
- 执行过程,浏览器的js引擎对于每个代码块进行顺序执行,如果有外部 引用的js,且js有相互关联,此时就要注意,不同js的引入顺序,如果声明代码块在调用代码块后调用则 将不会达到预期的效果。
全局预处理阶段
预处理:
创建一个词法环境(LexicalEnvironment,在后面简写为LE),扫描JS中的用声明的方式声明的函数,用var定义的变量并将它们加到预处理阶段的词法环境中去。
预处理阶段先读取代码块,不是一行一行的解析执行定义的方法和用var 定义的变量,会放到一个 (不同的环境,会有对应的词法环境)词法环境中。
var a = 1; //用var定义的变量,以赋值
var b; //用var定义的变量,未赋值
c = 3; //未定义,直接赋值
function d(){ //用声明的方式声明的函数
console.log('hello');
}
var e = function(){ //函数表达式
console.log('world');
}
词法环境:
LE{ //此时的LE相当于window
a:undefined
b:undefined
没有c
d:对函数的一个引用
e: undefined
}
注意:预处理的函数必须是JS中用声明的方式声明的函数(不是函数表达式)。
示例:
d();
e();
function d(){//用声明的方式声明的函数
console.log('hello');
}
var e = function(){//函数表达式
console.log('world');
}
结果:hello;报错e is not a function。
词法环境:
LE{ //此时的LE相当于window
d:对函数的一个引用
e: undefined
}
命名冲突
变量和函数同名冲突 —— 函数优先,函数是一等公民,在既有函数声明又有变量声明的时候,函数声明的权重总是高一些,所以最终结果往往是指向函数声明的引用。
console.log(f);
var f = 1;
function f(){
console.log('foodoir');
}
//结果:[Function: f]
console.log(f);
function f(){
console.log('foodoir');
}
var f = 1;
//变量和函数同名名冲突 —— 后者会覆盖前者
//结果:[Function: f]
f()
function f(){
console.log('foodoir');
}
function f(){
console.log('hello world');
}
//结果:hello world
var f = 1;
var f = 2;
console.log(f)
结果:2
执行阶段
console.log(a); //为什么是undefined?在预处理期间只调用未赋值
console.log(b);//报错,未定义
console.log(c); //函数在任意地方都可以调用
console.log(d);//函数表达式,undefined
var a = 1;
b = 2;
console.log(b);//2
function c() {
console.log('c');
}
var d = function () {
console.log('d');
}
console.log(d);
//注释第2行之后,结果为
undefined
[Function: c]
undefined
2
[Function: d]
LE{
a:undefined
没有b
c:对函数的一个引用
d:undefined
}
注释掉上面代码第二行
- 在第6行代码执行完,LE中的a的值变为1;
LE {
a: 1
没有b
c: 对函数的一个引用
d: undefined
}
-
第7行代码"console.log(b);"执行完,LE中就有了b的值(且b的值为2,此时b的值直接变为全局变量)
LE { a: 1 b: 2 c: 对函数的一个引用 d: undefined }
-
第10行代码执行完"function c(){}"
LE{ a:1 b:2 c:指向函数 d:undefined }
-
第14行代码"var d = function(){}"执行完,此时
LE{
a:1
b:2
c:指向函数
d:指向函数
}
函数冲突原则
- 处理函数声明有冲突时,会覆盖。
- 处理变量声明时有冲突,会忽略。以传入参数的值为准。
预处理阶段传入参数值一一对应
function f(a,b){
alert(a);//指向函数a
alert(b);//输出2
var b = 100;
function a(){}
}//b:2;a:指向定义好的函数;arguments:2
f(1,2);
f(1)//传入参数没有对应的值 LE{b:undefined;a:对函数的一个引用;arguments:1}
LE{
b:2
a:指向函数的引用
arguments:2
}
//arguments,调用函数时实际调用的参数个数
没有用var声明的变量,会变成最外部LE的成员,即全局变量
function a(){
function b(){
g = 12;
}
b();
}
a();
console.log(g);//12
JavaScript中的作用域
什么是作用域
作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定 了代码区块中变量和其他资源的可见性。
function outFun2() {
var inVariable = "内层变量2";
}
outFun2();//要先执行这个函数,否则根本不知道里面是啥
console.log(inVariable); // Uncaught ReferenceError: inVariable is not defined
变量 inVariable 在全局作用域没有声明,所以在全局作用域下取值会报错。我们可以这样理解:“作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就 是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突”。
ES6 之前 JavaScript 没有块级作用域,只有全局作用域和函数作用域。ES6 的到来,为我们提供 了‘块级作用域’,可通过新增命令 let 和 const 来体现。
全局作用域和局部作用域
在代码中任何地方都能访问到的对象拥有全局作用域,一般来说以下几种情形拥有全局作用域:
-
最外层函数 和在最外层函数外面定义的变量拥有全局作用域
var outVariable = "我是最外层变量"; //最外层变量 function outFun() { //最外层函数 var inVariable = "内层变量"; function innerFun() { //内层函数 console.log(inVariable); } innerFun(); } console.log(outVariable); //我是最外层变量 outFun(); //内层变量 console.log(inVariable); //inVariable is not defined innerFun(); //innerFun is not defined
-
所有末定义直接赋值的变量自动声明为拥有全局作用域
function outFun2() { variable = "未定义直接赋值的变量"; var inVariable2 = "内层变量2"; } outFun2(); //要先执行这个函数,否则根本不知道里面是啥 console.log(variable); //未定义直接赋值的变量 console.log(inVariable2); //inVariable2 is not defined
-
所有 window 对象的属性拥有全局作用域
一般情况下,window 对象的内置属性都拥有全局作用域
这就是为何 jQuery、Zepto 等库的源码,所有的代码都会放在 (function(){…})() 中。因为 放在里面的所有变量,都不会被外泄和暴露,不会污染到外面,不会对其他的库或者 JS 脚本造成影响。 这是函数作用域的一个体现。
函数作用域,是指声明在函数内部的变量,和全局作用域相反,局部作用域一般只在固定的代码片段 内可访问到,最常见的例如函数内部
function foo(a){
var b=a*2;
function bar(c){
console.log(a,b,c);
}
}
foo(2);//2,4,12
块语句(大括号“{}”中间的语句),如 if 和 switch 条件语句或 for 和 while 循 环语句,不像函数,它们不会创建一个新的作用域。在块语句中定义的变量将保留在它们已经存在的作用域中。
if (true) {
// 'if' 条件语句块不会创建一个新的作用域
var name = 'Hammad'; // name 依然在全局作用域中
}
console.log(name); // logs 'Hammad'
块级作用域
块级作用域可通过新增命令 let 和 const 声明,所声明的变量在指定块的作用域外无法被访问。块级作用域在如下情况被创建:
- 在一个函数内部.
- 在一个代码块(由一对花括号包裹)内部.
let 声明的语法与 var 的语法一致。你基本上可以用 let 来代替 var 进行变量声明,但会将变量的作 用域限制在当前代码块中。
块级作用域的特点
-
声明不会被提升到当前代码块的顶部
let/const 声明并不会被提升到当前代码块的顶部,因此你需要手动将 let/const 声明放置到顶部, 以便让变量在整个代码块内部可用。
function getValue(condition) { if (condition) { let value = "blue"; return value; } else { // value 在此处不可用 return null; } // value 在此处不可用 }
-
禁止重复声明
如果一个标识符已经在代码块内部被定义,那么在此代码块内使用同一个标识符进行 let 声明就会 导致抛出错误。例如:
var count = 30; let count = 40; // Uncaught SyntaxError: Identifier 'count' has already been declared
let 不能在同一作用域内重复声明一个已有标识符,此处的 let 声明就会抛出错误
在嵌套的作用域内使用 let 声明一个 同名的新变量,则不会抛出错误。
var count = 30; // 不会抛出错误 if (condition) { let count = 40; // 其他代码 }
-
循环中绑定块级作用域的妙用
开发者可能最希望实现 for 循环的块级作用域了,因为可以把声明的计数器变量限制在循环内,例 如,以下代码在 JS 经常见到:
<button>测试1</button> <button>测试2</button> <button>测试3</button> <script type="text/javascript"> var btns = document.getElementsByTagName('button') for (var i = 0; i < btns.length; i++) { btns[i].onclick = function () { console.log('第' + (i + 1) + '个') } } </script>
实现这样的一个需求: 点击某个按钮, 提示"点击的是第 n 个按钮",此处我们先不考虑事件代理, 万万没想到,点击任意一个按钮,后台都是弹出“第四个”,这是因为 i 是全局变量,执行到点击事件时,此 时 i 的值为 3。那该如何修改,最简单的是用 let 声明 i。
for (let i = 0; i < btns.length; i++) {
btns[i].onclick = function () {
console.log('第' + (i + 1) + '个')
}
}
JavaScript中的作用域链
什么是自由变量?
console.log(a)
要得到a
变量,但是在当前的 作用域中没有定义 a(可对比一下 b)。当前作用域没有定义的变量,这成为自由变量 。自由变量的值 如何得到 —— 向父级作用域寻找(不严谨).
var a = 100
function fn() {
var b = 200
console.log(a) // 这里的a在这里就是一个自由变量
console.log(b)
}
fn()
什么是作用域链?
如果父级也没呢?再一层一层向上寻找,直到找到全局作用域还是没找到,就宣布放弃。这种一层 一层的关系,就是作用域链 。
var a = 100
function F1() {
var b = 200
function F2() {
var c = 300
console.log(a) // 自由变量,顺作用域链向父作用域找
console.log(b) // 自由变量,顺作用域链向父作用域找
console.log(c) // 本作用域的变量
}
F2()
}
F1()
关于自由变量的取值
关于自由变量的值,上文提到要到父作用域中取,其实有时候这种解释会产生歧义。
var x = 10;
function fn() {
console.log(x);
}
function show(f) {
var x =20;
(function () {
f(); //10,而不是20
})();
}
show(fn);
在 fn 函数中,取自由变量 x 的值时,要到哪个作用域中取?——要到创建 fn 函数的那个作用域中 取,无论 fn 函数将在哪里调用。 所以,不要在用以上说法了。相比而言,用这句话描述会更加贴切:要到创建这个函数的那个域”。作用 域中取值,这里强调的是“创建”,而不是“调用”,切记切记——其实这就是所谓的"静态作用域"。
var a = 10
function fn() {
var b = 20
function bar() {
console.log(a + b) //30
}
return bar
}
var x = fn(),
b = 200
x() //bar()
//fn()返回的是 bar 函数,赋值给 x。执行 x(),即执行 bar 函数代码。取 b 的值时,直接在 fn 作用域取出。取 a 的值时,试图在 fn 作用域取,但是取不到,只能转向创建 fn 的那个作用域中去查找,结果找到了,所以最后的结果是 30。
JavaScript中的变量提升和函数提升
变量提升
通常JS引擎会在正式执行之前先进行一次预编译,在这个过程中,首先将变量声明及函数声明提升 至当前作用域的顶端,然后进行接下来的处理。
function hoistVariable() {
if (!foo) {
var foo = 5;
}
console.log(foo); // 5
}
hoistVariable();
//预编译之后
function hoistVariable() {
var foo;//undefined
if (!foo) {
foo = 5;
}
console.log(foo); // 5
}
hoistVariable();
var foo = 3;
function hoistVariable() {
var foo = foo || 5;
console.log(foo); // 5
}
hoistVariable();
var foo = 3;
// 预编译之后
function hoistVariable() {
var foo;//undefined
foo = foo || 5;
console.log(foo); // 5
}
hoistVariable();
// 预编译之后
function hoistVariable() {
foo = y || 5;//y传了值foo就等于y;y没传值就是undefined
console.log(foo); // 5
}
hoistVariable();
如果当前作用域中声明了多个同名变量,那么根据我们的推断,它们的同一个标识符会被提升至作用域顶部,其他部分按顺序执行
function hoistVariable() {
var foo = 3;
{
var foo = 5;
}
console.log(foo); // 5
}
hoistVariable();
//由于JavaScript没有块作用域,只有全局作用域和函数作用域,所以预编译之后的代码逻辑为
// 预编译之后
function hoistVariable() {
var foo;
foo = 3;
{
foo = 5;
}
console.log(foo); // 5
}
hoistVariable();
函数提升
function hoistFunction() {
foo(); // output: I am hoisted
function foo() {
console.log('I am hoisted');
}
}
hoistFunction();
//为什么函数可以在声明之前就可以调用,并且跟变量声明不同的是,它还能得到正确的结果,其实引擎是把函数声明整个地提升到了当前作用域的顶部
// 预编译之后
function hoistFunction() {
function foo() {
console.log('I am hoisted');
}
foo(); // output: I am hoisted
}
hoistFunction();
//如果在同一个作用域中存在多个同名函数声明,后面出现的将会覆盖前面的函数声明
function hoistFunction() {
function foo() {
console.log(1);
}
foo(); // output: 2
function foo() {
console.log(2);
}
}
hoistFunction();
函数声明和函数表达式的对比:这个函数名只能在此函数内部使用。我们也看到了,其实函数表达式可以通过变量访问,所以也存在变量提升同样的效果。
// 函数声明
function foo() {
console.log('function declaration');
}
// 匿名函数表达式
var foo = function() {
console.log('anonymous function expression');
};
// 具名函数表达式
var foo = function bar() {
console.log('named function expression');
};
函数声明的优先级最高,会被提升至当前作用域最顶端
- 第一次调用时实际执行了下面定义的函数声明
- 第二次调用时,由于前面的函数表达式与之前的函数声明同名,故将其覆盖,
function hoistFunction() {
foo(); // 2
var foo = function() {
console.log(1);
};
foo(); // 1
function foo() {
console.log(2);
}
foo(); // 1
}
hoistFunction();
// 预编译之后
function hoistFunction() {
var foo;
foo = function foo() {
console.log(2);
}
foo(); // 2
foo = function() {
console.log(1);
};
foo(); // 1
foo(); // 1
}
hoistFunction();
函数和变量重名:函数的优先权是最高的,它永远被提升至作用域最顶部,然后才是函数表达式和变量按顺序 执行
var foo = 3;
function hoistFunction() {
console.log(foo); // function foo() {}
foo = 5;
console.log(foo); // 5
function foo() {}
}
hoistFunction();
console.log(foo); // 3
//预编译之后
var foo = 3;
function hoistFunction() {
var foo;
foo = function foo() {};
console.log(foo); // function foo() {}
foo = 5;
console.log(foo); // 5
}
hoistFunction();
console.log(foo); // 3
为什么要进行提升?
函数提升 就是为了解决相互递归(是A函数内会调用到B函数,而B函数也会调用到A函数)的问题,大体上可以解决像ML语言这样自下而上的顺序问题。
如果没有函数提升,而是按照自下而上的顺序,当A函数被调用时,B函数还未声明,所 以当A内部无法调用B函数。所以Brendan Eich设计了函数提升这一形式,将函数提升至当前 作用域的顶部.
无论变量还是函数,都必须先声明后使用。
变量的本质
变量的本质
变量是什么
有一个数据保存起来了,当接下来需要使用到这个数据时, 需要在保存这个数据的位置把它拿出来用,一般的解决方式就是用一个名称与这个数据对应起来,下次 要用数据直接使用这个名称就行,这个名称就是变量
变量的本质
变量保存的就是这个内存地址的编号
读取变量的值 即是使用变量保存的地址编号去查看该地址段当前保存的值是什么
内存的分类:栈空间和堆空间,基本类型变量在第一次赋值被分配到栈空间。
对象(引用)类型变量第一次赋值被分配到堆空间,对象类型变量都是内存地址,并不是真正的对象 值。
声明方式
-
使用var关键字变量
- 方式一:声明和赋值不分离
- 方式二:声明和赋值分离
-
const关键字用于修饰常量
定义的变量不可修改,而且必须初始化,声明位置不限
-
let声明的变量在{}中使用,变量的作用域限制在块级域中。
变量的产生与死亡
声明在函数外部的变量
- 产生:js加载到该变量所在行时产生。
- 死亡:js代码加载完毕,变量死亡
声明在函数内部的变量
- 前提:该变量所在的函数被调用
- 产生:js执行到该变量所在行时产生。
- 死亡:该变量所在的函数执行行结束。
变量的数据类型
基本数据类型和引用数据类型
浮点型运算问题
console.log(1+2); //3
console.log(0.1+0.2); //0.30000000000000004
产生问题的原因
因为浮点数使用64位存储时,最多只能存储52的小数位,对于一些存在无限循环的小数位浮点数,会 截取前52位,从而丢失精度.
解决方案
主要 是将浮点数乘以一定的数值转换成整数,在进行完整数运算后,再除以扩大的数值,最后得到浮点数并 返回
string类型的常见算法
字符串逆序输出算法
使用reverse()函数
首先将字符串转换为字符数组,然后通过调用原生的reverse()函数进行逆序,得到逆序数组后再通 过join()函数得到逆序字符串
function reverseString1(str){
return str.split('').reverse().join('');
}
var str = 'abcdefg';
console.log(reverseString1(str));
使用栈的先进后出原则
可以使用数组来模拟 一个栈的结构,同时满足先进后出的原则
function Stack(){
this.data = []; //保存栈内元素
this.top = 0; //记录栈顶位置
}
//原型链增加入栈和出栈方法
Stack.prototype = {
//入栈:现在栈顶加入元素,然后元素个数加1
push:function push(element){
this.data[this.top++] = element;
},
//出栈:先返回栈顶的元素,然后元素个数减1
pop:function pop(){
return this.data[--this.top];
},
//返回栈内的元素,即长度
length:function(){
return this.top
}
};
//然后通过自定义的栈实现字符串逆序输出
function reverseString2(str){
//创建一个栈的实例
var s = new Stack();
//将字符串转换成数组
var arr = str.split('');
var len = arr.length;
var result = '';
//将元素压入栈内
for(var i = 0;i<len;i++){
s.push(arr[i]);
}
//输出栈内元素
for(var j =0;j<len;j++){
result += s.pop(j);
}
return result;
}
var str = 'abcdefg';
console.log(reverseString2(str))
递归算法
递归就是一个函数直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个 与原问题相似的规模较小的问题来求解。递归算法是一种在变成中经常出现的算法,无论是JavaScript还 是其他语言都有递归算法的存在。
递归的步骤
- 假设递归函数已经写好。
- 寻找递推关系。
- 将递推关系的结构转换为递归体。
- 将临界条件加入到递归体中。
求1-100的和
function sum(n){
if(n==1) return 1;
return sum(n-1) + n;
}
求1,3,5,7,9,奇数的和
分析:
- 假设递归函数已经写好为Sum,既Sum(50),就是求1-50奇数的和。
- 寻找递推关系: 就是 n 与 n-2 之间的关系。
- 将递归结构转换成递归体。
- 将临界条件加入到递归中。
- 递归函数。
function Sum(n){
if(n % 2 == 0){
n--;
}
if(n == 1){
return 1;
}else{
return n + Sum(n - 2);
}
}
var sum = Sum(50);
console.log('50以内奇数和==' + sum);
.Fibonacci数列(斐波那契数列)
1,1,2,3,5,8,13,21,34,55,89...求第 n 项
function fib(n){
if(n == 0 || n ==1) return 1;
return fib(n-1) + fib(n-2);
}
阶乘
function foo(n){
if( n == 1) return 1;
return foo(n - 1) * n;
}
求幂
function power(n,m){
if(m == 1) return n;
return power(n,m-1) * n;
}
使用递归实现字符串逆序
//递归实现字符串逆序
function reverseString3(strIn,pos,strOut){
if(pos<0){
return strOut;
}
strOut += strIn.charAt(pos--);
return reverseString3(strIn,pos,strOut);
}
var str = 'abcdefg';
var result = '';
console.log(reverseString3(str,str.length-1,result));
统计字符串中出现最多的字符及出现的次数
算法一
主要思想:通过key-value 形式的对象来存储字符串以及字符串出现的次数,然后逐个判断 出现次数最大值,同时获取对应的字符
function getMaxCount(str) {
var json = {};
//遍历str的每一个字符得到ket-value形式的对象
for (var i = 0; i < str.length; i++) {
//判断json中是否有当前str的值
if (!json[str.charAt(i)]) {
//如果不存在,就将当前值添加到json中去
json[str.charAt(i)] = 1;
} else {
//如果存在,则让value加1
json[str.charAt(i)]++;
}
}
//存储出现次数最多的值和出现次数
var maxCountChar = '';
var maxCount = 0;
//遍历json对象,找出出现次数最大的值
for (var key in json) {
//如果当前项大于下一项
if (json[key] > maxCount) {
//就让当前值更改为出现最多次数的值
maxCount = json[key];
maxCountChar = key;
}
}
//最终返回出现最多的值以及出现的次数
return '出现最多的值是:' + maxCountChar + '出现的次数为:' + maxCount;
}
var str = 'helloJavascripthellohtmlhellocss';
console.log(getMaxCount(str));
算法二
主要思想:对字符串进行排序,然后通过lastIndexOf()函数获取索引值后,判断索引值的大 小以获取出现的最大次数。
function getMaxCount2(str) {
//定义两个遍历分别表示出现最大次数和对应的字符
var maxCount = 0,
maxCountChar = '';
//先处理成数组,调用sort()函数排序,在处理成字符串
str = str.split('').sort().join('');
for (var i = 0, j = str.length; i < j; i++) {
var char = str[i];
//计算每个字符串出现的次数
var charCount = str.lastIndexOf(char) - i + 1;
//与次数最大值比较
if (charCount > maxCount) {
//更新maxCounthe maxCountChar的值
maxCount = charCount;
maxCountChar = char;
}
//变更索引为字符出现的最后位置
i = str.lastIndexOf(char);
}
//最终返回出现最多的值以及出现的次数
return '出现最多的值是:' + maxCountChar + '出现的次数为:' + maxCount;
}
var str = 'helloJavascripthellohtmlhellocss';
console.log(getMaxCount2(str));
字符串去重
算法一
主要思想:使用key-value 类型的对象存储,key表示唯一的字符,处理完后将所有的 key 拼接在一起即可得到去重后的结果。
function removeStrChar(str){
//结果数组
var result = [];
//key-value形式的对象
var json = {};
for(var i = 0;i<str.length;i++){
//当前处理的字符
var char = str[i];
//判断是否在对象中
if(!json[char]){
//value设置为true
json[char] = true;
//添加至结果数组中
result.push(char);
}
}
return result.join('');
}
var str = 'helloJavascripthellohtmlhellocss';
console.log(removeStrChar(str));
算法二
主要思想是借助数组的filter()函数,然后再filter()函数中使用indexOf()函数判断。
- 通过call()函数改变filter()函数的执行体,让字符串可以直接执行filter()函数。
- 在自定义的filter()函数回调中,通过indexOf()函数判断其第一次出现的素引位置,如果与filter()函 数中的index一样,则表示第一次出现,符合条件则return出去。这就表示只有第一次出现的字符 会被成功过滤出来,而其他重复出现的字符会被忽略掉。
- filter()函数返回的结果便是已经去重的字符数组,将其转换为字符串输出即为最终需要的结果。
function removeStrChar2(str){
//使用call()函数改变filter()函数的执行主体
let result = Array.prototype.filter.call(str,function(char,index,arr){
//通过indexOf()函数与index的比较,判断是否是第一次出现的字符
return arr.indexOf(char) === index;
});
return result.join('');
}
var str = 'helloJavascripthellohtmlhellocss';
console.log(removeStrChar2(str));
JavaScript中常用判断为空的方法
判断变量为空对象
判断变量为null或者undefined
判断一个变量是否为空时,可以直接将变量与null或者undefined比较,需要注意==
和===
的区别
if(obj == null){} //判断null或者undefined
if(obj === undefined) //只能判断undefined
判断变量为空对象
- 通过for…in语句遍历变量的属性
- 调用hasOwnProperty() 函数,判断是否有自身存在的属性
- 如果存在则不为空对象,如果不存在自身的属性(不包括继承的属 性)。那么变量为空对象。
//判断变量为空
function isEmpty(obj){
for(let key in obj){
if(obj.hasOwnProperty(key)){
return false;
}
}
return true;
}
//定义空的字面量对象
var obj = {};
console.log(isEmpty(obj));
//继承的属性
function Person(){}
Person.prototype.name = 'cao teacher';
//通过new操作符获取对象
var pbj = new Person;
console.log(isEmpty(pbj));
判断变量为空数组
判断变量为空数组时,
首先需要判断变量是否为数组,
然后通过length属性确定。
arr.instanceOf Array && arr.length ===0
判断变量为空字符串
判断变量是否为空字符串时,可以直接将其与空字符比较,或者调用trim()函数去掉前后的空格,然 后判断字符串的长度。
str ==='' || str.trim().length == 0
数组类型
数组类型介绍
Array类型中提供了丰富的函数用于对数组进行处理,例如过滤,去重,遍历等。
判断一个变量是数组还是对象
使用typeof运算符并不能直接判断 一个变量是数组还是对象,实际上,使用typeof运算符在判断基本数据类型时很方便,但是在判断引用 数据类型时就会很吃力。
instanceof运算符
instanceof运算符用于通过查找原型链来检测某个变量是否为某个类型数据的实例,使用 instanceof运算符可以判断一个变量是数组还是对象。
var a = [1,2,3];
console.log(a instanceof Array);//true
console.log(a instanceof Object);/true
var b = {name:'cao teacher'};
console.log(b instanceof Array);//false
console.log(b instanceof Object);//true
我们发现数组不仅是Array类型的实例,也是Object类型的实例,因此我们在判断 一个变量是数组还是对象时,应该应判断数组类型,然后再去判断对象类型。所以我们可以将上面的操 作进行函数封装。
//判断变量是数组还是对象
function getDataType(o){
if(o instanceof Array){
return 'Array';
}else if(o instanceof Object){
return 'Object';
}else{
return 'param is not object type';
}
}
var o = {};
console.log(getDataType(o));
Array.isArray()函数
静态函数,用于判断变量是否是数组,传入需要判断的变量,就可以确定变量是否是数组
//判断变量是否是数组
var a = [];
var b = [1,2,3];
var c = new Array();
console.log(Array.isArray(a));
console.log(Array.isArray(b));
console.log(Array.isArray(c));
filter()函数过滤条件
filter()函数
filter()函数用于过滤出满足条件的数据,返回一个新的数组,不会改变原来的数组。它不仅可以过 滤简单类型的数组,而且可以通过自定义方法过滤复杂类型的数组。
filter()函数接受一个函数作为其参数,返回值为true的元素会被添加至新的数组中,返回值为false 的元素则不会被添加至新的数组中,最后返回这个新的数组,如果没有符合条件的值则返回空数组。
找出数组中所有奇数的数字
var filterFn = function(x){
return x%2;
}
var arr = [1,2,3,4,5,6,7,8,9];
var result = arr.filter(filterFn);
console.log(result);
找出数组中年龄大于18的男生
var arrObj = [
{
gender:'男',
age:20
},
{
gender:'女',
age:16
},
{
gender:'男',
age:20
},
{
gender:'女',
age:16
}
];
var filterFn = function(obj){
return obj.age>18 && obj.gender == '男';
}
var result = arrObj.filter(filterFn);
console.log(result);
reduce()函数累加器
reduce()函数
reduce()函数的最主要的功能是做累加 处理,即接受一个函数作为累加器,将数组中的每一个元素从左到右执行累加器,返回最终的结果。
语法:arr.reduce(callback,[,initalValue]);
initialValue
用作 callback 的第一个参数值,如果没有设置,则会使用数组的第一个元素值 。callback
会接收4个参数(accumulator,currentValue,currentIndex,array
)。accumulator
表示上一次调用累加器的返回值,或设置的initialValue值。如果设置了initialValue, 则accumulator=initialValue
;否则accumulator=数组的第一个元素值 。currentValue
表示数组正在处理的值。currentIndex
表示当前正在处理值的索引。如果设置了initialValue.则cunrentlndex从0开始,否则 从1开始。- array表示数组本身。
求数组每个元素相加的和
var arr = [1,2,3,4,5];
var sum = arr.reduce(function(accmulator,currentValue){
return accmulator+currentValue;
})
console.log(sum);
统计数组中每个元素出现的次数
将initialValue
设置为一个空对象,initialValue
作为累加器accumulate
的初始值,依次往后执行每 个元素,
如果执行的元素在accumulate中存在,则将其计数加1,
否则将当前执行元素作为accumulate 的key,其value值为1,依次执行完所有元素之后,最终返回结果。
var countArrNum = function(arr){
return arr.reduce(function(accumulator,currentValue){
accumulator[currentValue] ? accumulator[currentValue]++ :
accumulator[currentValue] = 1;
return accumulator;
},{});
};
console.log(countArrNum([1,2,2,3,3,3,4,4,4,4]));
求数组中的最大和最小值
通过prototype属性扩展min()和max()
主要思想是借助自定义的min()和max()函数,通过循环由第一个值依次与后面的值进行比 较,动态更新最大值和最小值,从而找到结果。
//最小值
Array.prototype.min=function(){
var min = this[0];
var len = this.length;
for(var i=0;i<len;i++){
if(this[i]<min){
min = this[i];
}
}
return min;
};
//最大值
Array.prototype.max=function(){
var max = this[0];
var len = this.length;
for(var i=0;i<len;i++){
if(this[i]>max){
max = this[i];
}
}
return max;
};
var arr = [1,2,3,4,5,6,7,8,9];
console.log(arr.min());
console.log(arr.max());
借助Math对象的min()函数和max()函数
主要思想是通过apply()函数改变函数的执行体,将数组作为参数传递给apply()函数,这样 数组就可以直接调用Math对象的min()函数和max()函数来获取返回值。
//最小值
Array.min = function(array){
return Math.min.apply(Math,array);
}
//最大值
Array.max = function(array){
return Math.max.apply(Math,array);
}
var arr = [1,2,3,4,5,6,7,8,9];
console.log(Array.min(arr));
console.log(Array.max(arr));
算法2的优化,在上面的算法中并没有实现链式调用,而平时开发中链式调用是最常见的用法,所 以我们要对算法2进行一下优化。
//最小值
Array.prototype.min = function(){
return Math.min.apply({},this);
}
//最大值
Array.prototype.max = function(){
return Math.max.apply({},this);
}
var arr = [1,2,3,4,5,6,7,8,9];
console.log(arr.min());
console.log(arr.max());
数组遍历
for循环遍历数组
优点:使用简单,没有兼容性问题。
var arr = [1,2,3,4,5,6];
for(var i=0;i<arr.length;i++){
console.log(arr[i]);
}
forEach()方法
forEach()函数算是在数组实例方法中用于遍历调用次数最多的函数,forEach()函数接收一个回调函 数,参数分别表示 当前执行的元素值 , 当前值的索引 和 数组本身 ,在方法体中输出每个数组元素即可完 成遍历。
var arr = [1,2,3,4,5,6];
arr.forEach(function(element,index,array){
console.log(element);
})
forEach函数是在ES5中新添加的,它不兼容只支持低版本的js浏览器,这是我们需要提供一个 polyfill。通过for循环,在循环中判断this对象,即数组本身是否包含遍历的索引,如果包含则利用call()函数 去调用回调函数,传入回调函数所需的参数。
// forEach()函数的的polyfill
Array.prototype.forEach = Array.prototype.forEach ||
function (fn, context) {
for (var k = 0, length = this.length; k < length; k++) {
if (typeof fn === 'function' &&
Object.prototype.hasOwnProperty.call(this, k)) {
fn.call(context, this[k], k, this);
}
}
};
var arr = [1, 2, 3, 4, 5, 6];
arr.forEach(function (element, index, array) {
console.log((element));
})
数组去重算法1
主要思想是在函数内部新建一个数组,对传入的数组进行遍历,如果遍历的值不在新数组 中就添加进去,如果已经存在就不做处理。
function arrayUnique(array) {
var result = [];
for (var i = 0; i < array.length; i++) {
if (result.indexOf(array[i]) === -1) {
result.push(array[i]);
}
}
return result;
}
var array = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4];
console.log(arrayUnique(array));
数组去重算法2
主要思想是借助原生的sort()函数对数组进行排序,然后对排序后的数组进行相邻元素去重,将去重后的元素添加至新的数组中,返回这个新的数组
var arr = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4];
function arrayRepeat2(arr) {
// var result = [];
//result 要是一个数组元素,默认有一个值。
var result = [arr[0]];
console.log(result);
arr.sort(function (a, b) {
return a - b
});
for (var i = 0; i < arr.length; i++) {
if (arr[i] !== result[result.length - 1]) {
result.push(arr[i]);
}
}
return result;
}
console.log(arrayRepeat2(arr));
Date类型
日期格式化的主要目的是以对用户友好的形式,将日期,时间等数据展示出来,例如2021年7月31 日11点23分24秒,常见的形式为"2021-7-31 11:23:24"。
基于成型的库Moment.js
基于原生的JavaScript实现,除了原生js的开发我们还可以通过成型的JavaScript 时间类库,如:Moment.js,Date.js等。
中文官网:http://momentjs.cn/
CDN:http://cdn.staticfile.org/moment.js/2.24.0/moment.js
console.log(moment().format('MMMM Do YYYY, h:mm:ss a')); // 七月 8日 2021,
11:11:57 上午
console.log(moment().format('dddd')); // 星期四
console.log(moment().format("MMM Do YY")); // 7月 8日 21
console.log(moment().format('YYYY [escaped] YYYY')); // 2021 escaped
2021
console.log(moment().format()); // 2021-07-
08T11:11:57+08:00
日期合法性校验
校验日期合法性的主要思想是利用正则表达式,将正则表达式按分组处理,匹配到不同位置的数据 后,得到一个数组。利用数组的数据构造一个Date 对象,获得 Date 对象的年,月日的值,再去与数组 中表示年、月、日的值比较。如果都相等的话则为合法的日期,如果不相等的话则为不合法的日期。
第一种:
//| 日期有效性验证
//| 格式为:YYYY-MM-DD或YYYY/MM/DD
function IsValidDate(DateStr){
var sDate=DateStr.replace(/(^\s+|\s+$)/g,'');//去两边空格;
if(sDate==''){
return true;
}
//如果格式满足YYYY-(/)MM-(/)DD或YYYY-(/)M-(/)DD或YYYY-(/)M-(/)D或YYYY-
(/)MM-(/)D就替换为''
//数据库中,合法日期可以是:YYYY-MM/DD(2003-3/21),数据库会自动转换为YYYY-MM-DD
格式
var s=sDate.replace(/[\d]{ 4,4 }[\-/]{1}[\d]{1,2}[\-/]{1}[\d]{1,2}/g,'');
if(s==''){//说明格式满足YYYY-MM-DD或YYYY-M-DD或YYYY-M-D或YYYY-MM-D
var t=new Date(sDate.replace(/\-/g,'/'));
var ar=sDate.split(/[-/:]/);
if(ar[0]!=t.getYear()||ar[1]!=t.getMonth()+1||ar[2]!=t.getDate())
{//alert('错误的日期格式!格式为:YYYY-MM-DD或YYYY/MM/DD。注意闰年。');
return false;
}
}else{//alert('错误的日期格式!格式为:YYYY-MM-DD或YYYY/MM/DD。注意闰年。');
return false;
}
return true;
}
console.log(IsValidDate('2019-5-60'));
第二种
//| 格式为:YYYY-MM-DD HH:MM:SS
function CheckDateTime(str) {
var reg = /^(\d+)-(\d{ 1,2})-(\d{ 1,2})(\d{ 1,2}):(\d{1,2}):(\d{1,2})$/;
var r = str.match(reg);
if (r == null) return false;
r[2] = r[2] - 1;
var d = new Date(r[1], r[2], r[3], r[4], r[5], r[6]);
if (d.getFullYear() != r[1]) return false;
if (d.getMonth() != r[2]) return false;
if (d.getDate() != r[3]) return false;
if (d.getHours() != r[4]) return false;
if (d.getMinutes() != r[5]) return false;
if (d.getSeconds() != r[6]) return false;
return true;
}
函数的定义与调用
函数的定义
- 函数实际上也是一种对象,
- 每个函数都是Function类型的实例,
- 能够定义不同类型的属性与方法。
- 在使用函数之前,我们得先学会定义函数,函数的定义大体上可以分为三种,分别是 函数声明,函数表达式和Function构造函数。
函数声明
函数声明是直接使用function关键字接一个函数名,函数名后是接受函数的形参。
function sum(num1,num2){
return num1+num2;
}
sum(1,2)
函数表达式
函数表达式的形式类似于陪同变量的初始化,只不过这个变量初始化的值是一个函数。
var sum = function(num1,num2){
return num1+num2;
}
sum(1,2);
属于匿名函数表达式,使用函数声明和匿名函数表达式定义的函数,函数 名称即跟在function关键字后的值。
Function构造函数
使用new操作符,调用Function()构造函数,传入对应的参数,也可以定义一个函数。
var add = new Function("num1","num2","return num1+num2");
其中的参数,除了最后一个参数是执行的函数体,其他参数都是函数的形参。
相比于前两种函数的声明的方式,构造方式申明函数用的会更少一些,两个原因。
- Function()构造函数在执行时,都会解析函数的执行主体,并创建一个新的函数对象,所以当在一 个循环或者频繁执行的函数中调用Function()构造函数时,效率时非常低的。
- 在使用Function()构造函数创建的函数,并不遵循典型的作用域,它将一直作为顶级函数执行,所 以在函数A内部调用Function()构造函数时,其中的函数体并不能访问到函数A中的全局变量,而只能访问到全局变量。
// var y = 'global';
function construct(){
var y = 'local';
return new Function('return y');
}
console.log(construct()());
函数表达式的应用场景
- 函数递归
//于斐波那契数列
//函数递归
function fib(n){
if(n == 0 || n ==1) return 1;
return fib(n-1) + fib(n-2);
}
//函数表达式
var fibonacci = function(num){
if(num ===1 || num === 2){
return 1;
}
return fibonacci(num-2)+fibonacci(num-1);
}
console.log(fibonacci(15));
-
代码模块化
在ES6之前,JavaScript中没有块级作用域的概念,但是我们可以通过函数表达式来间接的实现模块化,将特定的模块代码封装在一个函数中,只对外暴露接口,使用者也不用关心具体细节,这样做可以很好的避免全局环境的污染。
var Person = (function () {
var _name = "";
return {
getName: function () {
return _name;
},
setName: function (newName) {
_name = newName;
}
};
}());
Person.setName('cao teacher');
console.log(Person.getName());
函数声明和函数表达式的区别
-
函数名称
在使用函数声明时,必须设置函数名的,这个函数名相当于一个变量,以后函数的调用也会通过这个变量执行。
而对于函数表达式来说,函数名称是可选的,我们可以定义一个匿名函数表达式,并赋给一个变 量,然后通过这个变量进行函数的调用。
-
函数提升
对于函数声明,存在函数提升,也就是我们可以在函数声明之前调用函数。
对于函数表达式,不存在函数提升,所以在定义函数前不能对其进行函数调用。
函数的调用
函数调用模式
函数调用模式是通过函数声明或者函数表达式的方式定义函数,然后直接通过函数名进行调用。
function sum(num1, num2) {
return num1 + num2;
}
sum(1, 2)
var sum = function (num1, num2) {
return num1 + num2;
}
sum(1, 2);
方法调用模式
方法调用模式会优先定义一个对象obj,然后再对象内部定义值为函数的属性property,通过对象的 obj.property()来进行调用。
var obj = {
name: 'cao teacher',
getName: function () {
return this.name;
}
};
console.log(obj.getName());
函数还可以通过中括号来调用:
obj['getName']();
如果某个方法中返回的是函数对象本身this,那么可以利用链式调用原理进行连续的函数调用。
var obj = {
name: 'cao teacher',
getName: function () {
console.log(this.name);
},
setName: function (name) {
this.name = name;
return this; //在函数内部返回函数对象本身
}
};
obj.setName('cao teacher2').getName();
构造器调用模式
构造器调用模式会定义一个函数,在函数中定义实例属性,在原型上定义函数,然后通过new操作 符生成函数的实例,在通过实例调用原型上定义的函数。
//定义函数对象
function Person(name){
this.name = name;
}
//原型上定义函数
Person.prototype.getName = function(){
return this.name;
};
//通过new 操作符生成实例
var p = new Person('cao teacher');
//通过实例进行函数的调用
console.log(p.getName());
通过call()函数、apply()函数
通过call()函数或者apply()函数可以改变函数的执行主体,某些不具有特定函数的对象可以直接 调用该特定函数。
//定义一个函数
function sum(num1,num2){
return num1+num2;
}
//定义一个对象
var Person = {}
//通过call()函数和apply()函数可以改变函数的执行主体
console.log(sum.call(Person,1,2));
console.log(sum.apply(Person,[1,2]));
匿名函数调用模式
匿名函数,顾名思义就是没有名字的函数,匿名函数的调用有两种方式,一种是通过函数表达式定 义函数,赋值给变量,通过变量进行调用。
var sum = function(num1,num2){
return num1+num2;
}
sum(1,2);
另一种是使用小括号()将匿名函数包裹起来,然后再后面使用()传递对应的参数进行调用。
(function(num1,num2){
return num1+num2;
}(1,2));
自执行函数
自执行函数即函数定义和函数调用后的行为先后连续产生,它需要一个函数表达式的身份进行函数 调用,上面的匿名函数调用也属于自执行函数的一种。
var a = function (x) {
console.log(x)
}(1);
true && function (x) {
console.log(x)
}(1);
! function (x) {
console.log(x)
}(1);
~ function (x) {
console.log(x)
}(1); -
function (x) {
console.log(x)
}(1);
new function (x) {
console.log(x)
}(1);
函数参数
形参和实参
形参
- 形参全称是形式参数
- 是在定义函数名称与函数体时使用的参数
- 目的使用接收调用该函数是传入的参数。
实参
- 实参全称是实际参数
- 是在调用时传递给函数的参数
- 实参可以是常量,变量,表达式,函数等类型。
形参和实参的区别
-
形参出现在函数的定义中,只能在函数体内使用,一旦离开函数则不能使用,实参出现在主调函数 中,进入被调函数后,实参也不能被访问。
function fn1() { var param = 'hello'; fn2(param); console.log(arg); } function fn2(arg) { console.log(arg); console.log(param); } fn1();
-
在强类型语言中,定义的形参和实参在数量,数据类型和顺序上要保证严格的一致,否则会抛出类型不匹配的异常。
-
在函数调用过程中,数据传输是单向的,即只能把实参的值传递给形参,而不能把形参的值传递给实参,因此在函数执行时,形参的值可能会发生变化,但不会影响到实参中的值。
var arg = 1;
function fn(param) {
param = 2
}
fn(arg);
console.log(arg);
-
当实参是基本数据类型的值时,实际是将实参的值复制一份传递给形参,在函数运行结束前形参被 释放。而实参中的值不会变化。
当实参是引用类型的值时,实际是将实参的内存地址传递给形参,即实参和形参都指向相同的内存地址,此时形参可以修改实参的值,但是不能修改实参的内存地址。
var arg = {
name: 'cao teacher'
};
function fn(param) {
param.name = 'cao teacher2';
param = {};
}
fn(arg);
console.log(arg);
定义了一个实参arg为一个对象,name属性值为cao teacher,在调用fn()函数时首先修改了形参param的name属性值为cao teacher2,此时形参param与实参arg指向的是同一个内存地址(假设为A),因此arg 的值也会发生变化。然后将形参param重新赋值为一个空对象,表示的是将形参parm指向了一个新的内存地址(假设为B)。但是这并不会影响实参arg的值,它仍然指向原来的内存地址A,因此最后的输出结果为{name:‘cao teacher2’}。
arguments对象的性质
arguments对象是所有函数都具有的一个内置局部变量,表示的是函数实际接收的参数,是一个类数组结构,之所以说arguments对象是一个类数组结构,是因为它除了具有length属性之外,不具有数组的一些常用方法。
函数外部无法访问
arguments对象只能在函数内部访问,无法在函数外部访问到arguments对象,同时arguments对象存在于函数作用域中,一个函数无法直接获取另一个函数的arguments对象。
function foo() {
console.log(arguments.length);//3,访问到(1,2,3)
function foo2() {
console.log(arguments.length);//0,无法访问到外部的(1,2,3)
}
foo2();
}
foo(1, 2, 3);
可通过索引访问
arguments对象是一个类数组结构,可以通过索引访问,每一项表示对象传递的实参值,如果该项索 引不存在,则会返回undefined。
function sum(num1, num2) {
console.log(arguments[0]);//1
console.log(arguments[1]);//2
console.log(arguments[2]);//undefined
}
sum(1, 2);
由实参决定
arguments对象的值由实参决定,而不是由定义的形参决定,形参与arguments对象占用独立的内 存空间,关于arguments对象与形参之间的关系,可以总结为以下几点。
- arguments对象的length属性在函数调用的时候就已经确定,不会随着函数的处理而改变。
- 指定的形参在传递实参的情况下,arguments对象与形参值相同,并且可以相互改变。
- 指定的形参在未传递实参的情况下,arguments对象对应索引值返回undefined。
- 指定的形参在未传递实参的情况下,arguments对象与形参值不能相互改变。
function foo(a, b, c) {
console.log(arguments.length);//2
arguments[0] = 11;
console.log(a);//11
b = 12;
console.log(arguments[1]);//12
arguments[2] = 3;
console.log(c);//undefined
c = 13;
console.log(arguments[2]);//3
console.log(arguments.length)//2
}
foo(1, 2);
特殊的arguments.callee属性
arguments对象有一个特殊的属性callee,表示的是当前正在执行的函数,在比较时时严格相等 的。
function foo() {
console.log(arguments.callee === foo);//true
}
foo();
通过arguments.callee属性获取到函数对象后,可以直接传递参数进行函数的调用,这个属性在匿名的递归函数中非常有用。
function create() {
return function (n) {
if (n <= 1)
return 1;
return n * arguments.callee(n - 1);
};
}
var result = create()(5);
console.log(result);//120
arguments对象的应用
实参的个数判断
定义一个函数,明确要求在调用时只能传递3个参数,如果传递的参数个数不等于3个,则直接抛出 异常。
function f(x, y, z) {
//检查传递参数的个数
if (arguments.length !== 3) {
throw new Error("必须传入三个参数");
}
};
f(1, 2);
任意个数参数的处理
定义一个函数,该函数只会特定的处理传递的前几个参数,对于后面的参数不论传递多少个都会统 一处理,这种场景下我们可以使用arguments对象。
function joinStr(str) {
//arguments对象时一个类数组结构,可以通过call()函数间接调用slice()函数,得到一个数组
var strArr = Array.prototype.slice.call(arguments, 1);
//strArr数组直接调用join()函数
return strArr.join(str);
}
console.log(joinStr('-', 'orange', 'green', 'blue', 'yellow', 'black'));
console.log(joinStr(',', 'orange', 'green', 'blue', 'yellow', 'black'));
//orange-green-blue-yellow-black
//orange,green,blue,yellow,black
模拟函数重载
函数重载表示的是在函数名相同的情况下,通过函数形参的不同参数类型或者不同参数个数来定义不同的参数,但是JavaScript中是没有函数重载的,主要有以下几点原因导致JavaScript没有函数重载:
- JavaScript是一门弱类型的语言,变量只有在使用时才能确定数据类型,通过形参是无法确定数据 类型的。
- 无法通过函数参数的个数来指定调用不同的函数,函数的参数的个数实际在函数调用时才确定下来 的。
- 使用函数声明定义具有相同名称的函数,后者会覆盖前者。
function sum(num1, num2) {
return num1 + num2;
}
function sum(num1, num2, num3) {
return num1 + num2 + num3;
}
console.log(sum(1, 2));//NaN
console.log(sum(1, 2, 3));//6
需要写一个通用的函数,来实现任意个数字相加的结果求和。首先通过 call()函数间接调用数组的slice()函数,以得到函数参数的数组。然后调用数组的reduce()函数进行多个值 的求和并返回。
//通用求和函数
function sum() {
//通过call()函数间接调用数组的slice()函数,以得到函数参数的数组。
var arr = Array.prototype.slice.call(arguments);
//调用数组的reduce()函数进行多个值的求和
return arr.reduce(function (pre, cur) {
return pre + cur;
}, 0)
}
console.log(sum(1, 2));//3
console.log(sum(1, 2, 3));//6
console.log(sum(1, 2, 3, 4));//10