共计 7520 个字符,预计需要花费 19 分钟才能阅读完成。
本文当时写在本地,发现换电脑很不是方便,在这里记录下。
深入认知 Javascript
前言
关于 Javascript,平时我们仅仅做到了使用,但是真的理解为什么这么使用吗?
这里详细介绍一些我们常用的 Javascript 语法。
关键字
what: 在 Javascript 关键字是有很多的,而普通的关键字基本没有太多的难度,例如 var,eval,void,break…,这里仅仅挑选两个 this 和 new 也是最让人疑惑的关键字。
1.1 this
在 Javascript6.0 以下,Javascript 是没有块级作用域的,只有函数作用域。而如果在作用域中嵌套作用域,那么就会有作用域链。
foo = "window";
function first(){
var foo = "first";
function second(){
var foo = "second";
console.log(foo);
}
function third(){console.log(foo);
}
second(); //second
third(); //first}
first();
理解:当执行 second 时,JS 引擎会将 second 的作用域放置链表的头部,其次是 first 的作用域,最后是 window 对象,于是会形成如下作用域链:second->first->window, 此时,JS 引擎沿着该作用域链查找变量 foo, 查到的是 second。当执行 third 时,third 形成的作用域链:third->first->window, 因此查到的是:frist。
弄清楚作用域,我们在来看 this
关键字,Javascript 中的 this
总是指向当前函数的所有者对象,this 总是在运行时才能确定其具体的指向, 也才能知道它的调用对象
window.name = "window";
function f(){console.log(this.name);
}
f();//window
var obj = {name:'obj'};
f.call(obj); //obj
理解:在执行 f()时,此时 f()的调用者是 window 对象,因此输出 window,f.call(obj) 是把 f()放在 obj 对象上执行,相当于 obj.f(), 此时 f 中的 this 就是 obj, 所以输出的是 obj
Demo 1
var foo = "window";
var obj = {
foo : "obj",
getFoo : function() {return function() {return this.foo;};
}
};
var f = obj.getFoo();
f();
Demo 2
var foo = "window";
var obj = {
foo : "obj",
getFoo : function() {
var that = this;
return function(){return that.foo;};
}
};
var f = obj.getFoo();
f();
❓ Demo1 和 Demo2 的返回值是多少
代码解析:
// demo1:
// 执行 var f = obj.getFoo()返回的是一个匿名函数,相当于:
var f = function(){return this.foo;}
// f() 相当于 window.f(), 因此 f 中的 this 指向的是 window 对象,this.foo 相当于 window.foo, 所以 f()返回 "window"
// demo2:
// 执行 var f = obj.getFoo() 同样返回匿名函数,即:var f = function(){return that.foo;}
// 唯一不同的是 f 中的 this 变成了 that, 要知道 that 是哪个对象之前,先确定 f 的作用域链:f->getFoo->window 并在该链条上查找 that,
// 此时可以发现 that 指代的是 getFoo 中的 this, getFoo 中的 this 指向其运行时的调用者,// 从 var f = obj.getFoo() 可知此时 this 指向的是 obj 对象,因此 that.foo 就相当于 obj.foo, 所以 f()返回 "obj"
1.2 new
what: 和其他高级语言一样 Javascript 中也有 new 运算符,我们知道 new 运算符是用来实例化一个类,从而在内存中分配一个实例对象。但在 Javascript 中,万物皆对象,为什么还要通过 new 来产生对象
1.2.1 认识
先看一个例子
01 function Animal(name){
02 this.name = name;
03 }
04 Animal.color = "black";
05 Animal.prototype.say = function(){06 console.log("I'm " + this.name);
07 };
08 var cat = new Animal("cat");
09
10 console.log(
11 cat.name, //cat
12 cat.height //undefined
13 );
14 cat.say(); //I'm cat
15
16 console.log(
17 Animal.name, //Animal
18 Animal.color //back
19 );
20 Animal.say(); //Animal.say is not a function
代码解析:
1- 3 行创建了一个函数 Animal
,并在其 this
上定义了属性 name
,name 的值是函数被执行时的形参。
4 行在 Animal
对象(Animal
本身是一个函数对象)上定义了一个静态属性 color
,并赋值“black”
5- 7 行在 Animal
函数的原型对象 prototype
上定义了一个 say
方法,say
方法输出了 this.name
。
8 行通过 new
关键字创建了一个新对象 cat
。
10-14 行 cat
对象尝试访问 name
和 color
属性,并调用 say
方法。
16-20 行 Animal
对象尝试访问 name
和 color
属性,并调用 say
方法。
重点解析:
注意到第 8 行,
var cat = new Animal("cat");
JS 引擎执行这句代码时,在内部做了很多工作,用伪代码模拟其工作流程如下:
new Animal("cat") = {var obj = {};
obj.__proto__ = Animal.prototype;
var result = Animal.call(obj, "cat");
return typeof result === 'object'? result : obj;
}
代码解析:
创建一个空对象obj
;
把obj
的 __proto__
指向 Animal
的原型对象 prototype
,此时便建立了 obj
对象的原型链:
obj
Animal.prototype
Object.prototype
null
简单解释下原型对象和原型链:
- 原型对象:指给后台函数继承的父对象
- 原型链:链接成 java 的继承
???? 在 obj
对象的执行环境调用 Animal
函数并传递参数 cat
。相当于var result = obj.Animal("cat")
。当这句执行完之后,obj
便产生了属性 name
并赋值为cat
。
简单解释下 call 和 apply:
相同:调用一个对象的一个方法,用另一个对象替换当前对象。
- B.call(A, args1,args2); 即 A 对象调用 B 对象的方法。
- B.apply(A, arguments); 即 A 对象应用 B 对象的方法
不同: 两者传入的列表形式不一样
- call 可以传入多个参数
- apply 只能传入两个参数,所以其第二个参数往往是作为数组形式传入
???? 考察第 3 步返回的返回值,如果无返回值或者返回一个非对象值,则将 obj
返回作为新对象;否则会将返回值作为新对象返回。
❗️ 深入解析:
理解 new 的运行机制以后,我们知道 cat 其实就是过程(4)的返回值,因此我们对 cat 对象的认知就多了一些:
cat 的原型链是:cat
Animal.prototype
Object.prototype
null
cat 上新增了一个属性:name
分析完了 cat 的产生过程,我们再看看输出结果:
???? cat.name -> 在过程(3)中,obj 对象就产生了 name 属性。因此 cat.name 就是这里的 obj.name
???? cat.color -> cat 会先查找自身的 color,没有找到便会沿着原型链查找,在上述例子中,我们仅在 Animal 对象上定义了 color, 并没有在其原型链上定义,因此找不到。
???? cat.say -> cat 会先查找自身的 say 方法,没有找到便会沿着原型链查找,在上述例子中,我们在 Animal 的 prototype 上定义了 say, 因此在原型链上找到了 say 方法。
另外,在 say 方法中还访问 this.name,这里的 this 指的是其调用者 obj, 因此输出的是 obj.name 的值。
对于 Animal 来说,它本身也是一个对象,因此,它在访问属性和方法时也遵守上述查找规则,所以:
???? Animal.color -> “black”
???? Animal.name -> “Animal” , Animal 先查找自身的 name, 找到了 name,注意:但这个 name 不是我们定义的 name, 而是函数对象内置的属性。一般情况下,函数对象在产生时会内置 name 属性并将函数名作为赋值(仅函数对象)。
???? Animal.say -> Animal 在自身没有找到 say 方法,也会沿着其原型链查找,话说 Animal 的原型链是什么呢?
从测试结果看:Animal 的原型链是这样的:
Animal
Function.prototype
Object.prototype
null
因此 Animal 的原型链上没有定义 say 方法!
1.2.2 存在的意义
之前提到 js 中,万物皆对象,为什么还要通过 new 来产生对象?要弄明白这个问题,我们首先要搞清楚 cat 和 Animal 的关系。
通过上面的分析,我们发现 cat 继承了 Animal 中的部分属性,因此我们可以简单的理解:Animal 和 cat 是继承关系。
另一方面,cat 是通过 new 产生的对象,那么 cat 到底是不是 Animal 的实例对象?我们先来了解一下 JS 是如何来定义“实例对象”的?
A instanceof B
如果上述表达式为 true,JS 认为 A 是 B 的实例对象,我们用这个方法来判断一下 cat 和 Animal
var isInstance = cat instanceof Animal; //true
❗️ 代码解析:
cat
确实是 Animal
实例,要想证实这个结果,我们再来了解一下 JS 中 instanceof
的判断规则:
var L = A.proto;
var R = B.prototype;
console.log(L === R);
在 new
的执行过程中,cat 的 __proto__
指向了 Animal 的 prototype
,所以cat
和Animal
符合 instanceof
的判断结果。因此,我们认为:cat
是 Animal
的实例对象。
1.2.3 总结
在 javascript 中, 通过 new 可以产生原对象的一个实例对象,而这个实例对象继承了原对象的属性和方法。因此,new 存在的意义在于 它实现了 javascript 中的继承,而不仅仅是实例化了一个对象!
高级函数
what: 在 Javascript 中,对象分普通对象和函数对象,普通的常见,这里不做说明。说起 Javascript 的函数,十分强大。它们是第一类对象,也可以作为另一个对象的方法,还可以作为参数传入另一个函数,不仅如此,还能被一个函数返回!可以说,在 JS 中,函数无处不在,无所不能。
why: 除了函数相关的基础知识外,掌握一些高级函数并应用起来,不仅能让 JS 代码看起来更为精简,还可以提升性能。
2.1 安全构造函数
常规写法:
function Person(name,age){
this.name = name;
this.age = age;
}
var p1 = new Person("Claiyre",80);
但是,如果忘记加 new
了会发生什么?
var p3 = Person("Tom",30);
console.log(p3); //undefined
console.log(window.name); //Tom
由于使用了不安全的构造函数,上面的代码意外的改变了 window
的name
,因为 this
对象是在运行时绑定的,使用 new 调用构造函数时 this
是指向新创建的对象的,不使用 new
时,this
是指向 window
的。
由于 window
的name
属性是用来识别链接目标和 frame
的,所在这里对该属性的偶然覆盖可能导致其他错误。
作用域安全的构造函数会首先确认 this
对象是正确类型的实例,然后再进行更改,如下:
function Person(name,age){if(this instanceof Person){
this.name = name;
this.age = age;
} else {return new Person(name,age);
}
}
作用:避免了在全局对象上意外更改或设置属性。在多人协作的项目中,为了避免他们误改了全局对象,使用作用域安全的构造函数会更好。
2.2 惰性载入函数
由于浏览器间的行为差异,代码中可能会有许多检测浏览器行为的 if 语句。但用户的浏览器若支持某一特性,便会一直支持,所以这些 if 语句,只用被执行一次,即便只有一个 if 语句的代码,也比没有要快。惰性载入表示函数执行的分支仅会执行一次,有两种实现惰性载入的方式
第一种就是在函数第一次被调用时再处理函数,用检测到的结果重写原函数。
function detection(){if(// 支持某特性){detection = function(){// 直接用支持的特性}
} else if(// 支持第二种特性){detection = function(){// 用第二种特性}
} else {detection = function(){// 用其他解决方案}
}
}
第二种实现惰性载入的方式是在声明函数时就指定适当的函数
var detection = (function(){if(// 支持某特性){return function(){// 直接用支持的特性}
} else if(// 支持第二种特性){return function(){// 用第二种特性}
} else {return function(){// 用其他解决方案}
}
})();
???? 作用:惰性载入函数的有点是在只初次执行时牺牲一点性能,之后便不会再有多余的消耗性能。
2.3 函数绑定作用域
在 JS 中,函数的作用域是在函数被调用时动态绑定的,也就是说函数的
this
对象的指向是不定的,但在一些情况下,我们需要让某一函数的执行作用域固定,总是指向某一对象。这时可以用函数绑定作用域函数。
function bind(fn, context){return function(){return fn.apply(context, arguments);
}
}
// 具体一点
var person1 = {
name: "claiyre",
sayName: function(){alert(this.name);
}
}
var sayPerson1Name = bind(person1.sayName, person1);
sayPerson1Name(); // claiyre
???? 作用 :函数的this
对象固定,总是指向某一对象
2.4 函数柯里化
只传递部分参数来调用函数,然后让函数返回另一个函数去处理剩下的参数。可以理解为赋予了函数“加载”的能力。
// 较为简单的实现 curry 的方式
function curry(fn){
var i = 0;
var outer = Array.prototype.slice.call(arguments,1);
var len = fn.length;
return function(){var inner = outer.concat(Array.prototype.slice.call(arguments));
return inner.length === len?fn.apply(null,inner) : function (){var finalArgs = inner.concat(Array.prototype.slice.call(arguments));
return fn.apply(null,finalArgs);
}
}
}
// 一旦函数经过柯里化,我们就可以先传递部分参数调用它,然后得到一个更具体的函数。var match = curry(function(what,str){return str.match(what)
});
var hasNumber = match(/[0-9]+/g);
var hasSpace = match(/\s+/g)
hasNumber("123asd"); // ['123']
hasNumber("hello world!"); // null
hasSpace("hello world!"); // [' '];
hasSpace("hello"); // null
console.log(match(/\s+/g,'i am Claiyre')); // 直接全部传参也可:['',' ']
???? 作用:逐步的具体化函数,最后得到结果
2.5 debounce 函数 (去抖函数)
当函数被调用时,不立即执行相应的语句,而是等待固定的时间 w, 若在 w 时间内,即等待还未结束时,函数又被调用了一次,则再等待 w 时间,重复上述过程,直到最后一次被调用后的 w 时间内该函数都没有被再调用,则执行相应的代码。
var myFunc = debounce(function(){// 繁重、耗性能的操作},250); // 函数节流
window.addEventListener('resize',myFunc);
???? 作用:防止某一函数被连续调用,从而导致浏览器卡死或崩溃
2.6 once 函数
function once(fn){
var result;
return function(){if(fn){result = fn(arguments);
fn = null; // 在被执行过一次后, 参数 fn 就被赋值 null 了,那么在接下来被调用时,便再也不会进入到 if 语句中了,也就是第一次被调用后,该函数永远不会被执行了。}
return result;
}
}
var init = once(function(){// 初始化操作})
???? 作用:仅仅会被执行一次的函数,防止过多的污染
结语
Javascript 太多内容了,基本讲一个点就能引申出很多点出来。
刚刚上述中关键字我们很常用,但是稍微不注意就可能会弄混淆;而在高级函数中,不难发现很多“高级函数”的实现其实并不复杂,数十行代码便可搞定,但重要的是能真正理解它们的原理,在实际中适时地应用,以此性能提升,让代码简洁,逻辑清晰。
参考
- 关键字
- 高级函数