Where is JavaScript?

JavaScript爬坑笔记

“坑”也就是“陷阱”。由于JavaScript“弱语言”的性质,使得其在使用过程中异常的宽松灵活,但也极为容易“中招”。这些坑往往隐藏着,只有亲自尝试并爬坑,才能在学习与应用的道路上走的一帆风顺。
平时积累一些觉得比较有意思或是比较有难度的JavaScript题目,顺便附上理解和心得。既是为了将来的面试做准备,也增强印象以防自己掉进坑里。

一、 函数声明的预解析

1.1 函数的声明方式

定义一个函数有两种方式:

              
  • 1
  • 2
foo() {}; // 函数声明 var foo = function() {}; // 函数表达式

不同之处 -

  1. 函数表达式后面加括号可以直接执行
  2. 函数声明会提前预解析

1.2 预解析

下面这段代码的输出结果是什么?

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
function test(){ console.log(a); console.log(foo()); var a =1; function foo(){ return 2; } } test();

undefined和2

换种写法

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
function test(){ console.log(a); console.log(foo()); var a =1; var foo = function(){ return 2; } } test();

undefined
第二条打印会报错 - TypeError: foo is not a function

1.3 变量提升

JavaScript解析器会在自身作用域内将变量和函数声明提前(hoist)。

也就是说,上面的例子其实被解析器理解解析成了以下形式:

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
function test(){ var a; var foo; console.log(a); console.log(foo()); a = 1; foo = function(){ return2; } } test();

这样也就可以解释,为什么在函数表达式之前调用函数,会返回错误了,因为它还没有被赋值,只是一个未定义变量,当然无法被执行。

变量声明被提到最前(所以不会报出变量不存在的错误),但赋值没有被提前,所以第一句的输出结果是undefined。

1.4 可能的问题

由于函数声明会被预解析,所以不要使用此种方法来声明不同函数。尝试猜想下面例子的输出结果:

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
if(true){ function aaa(){ alert('1'); } } else{ function aaa(){ alert('2'); } } aaa();

与我们预想的不同,该段代码弹出的是“2”.这是因为两个函数声明在if语句被执行之前就被预解析了,所以if语句根本没有用,调用aaa()的时候直接执行了下面的函数。

1.5 最佳实践

  • 变量和函数声明一定要放在作用域/函数的开头。
  • 函数表达式在赋值后再去使用。

二、作用域

看一下下面的代码:

              
  • 1
  • 2
  • 3
  • 4
(function(){ var a = b = 5; })(); console.log(b);

5

这个问题考查的要点是两个不同的作用域,'a'被var声明成了一个局部变量,但是'b'实际上没有被定义,所以它是一个全局变量。
如果你选择了strict mode,上面的代码就会报Uncaught ReferenceError,因为b没有被定义,它可以帮你检查出代码的一些问题:

              
  • 1
  • 2
  • 3
  • 4
  • 5
(function(){ 'use strict'; var a = window.b = 5; })(); console.log(b);

三、继承问题

JavaScript是基于对象和实例的语言,它没有类的概念。所以,要想实现继承,必须熟练应用其prototype机制、函数构造以及callapply等特殊方法。

可以简单的把prototype看做是一个模板,新创建的自定义对象都是这个模板(prototype)的一个拷贝(实际上不是拷贝而是链接,只不过这种链接是不可见,新实例化的对象内部有一个看不见的__proto__指针,指向原型对象)。

3.1 原型链继承

为了让子类继承父类的属性(也包括方法),首先需要定义一个构造函数。然后,将父类的新实例赋值给构造函数的原型。

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
function Super() { this.belong = "human"; } function Sub(age) { this.age = age; } Sub.prototype = new Super();

简单的说法就是拿父类的实例来充当子类的原型对象。

优点是实现方式相当简单,易于实现。

缺点之一是来自原型对象的引用属性是所有实例共享的,会导致覆盖的属性篡改;二是创建子类实例时,无法向父类构造函数传参。

3.2 借用构造函数

原型链够简单,但缺点也很明显。于是出现了借用构造函数的方式。

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
function Super(val){ this.val = val; this.arr = [1]; this.fun = function(){ // ... } } function Sub(val){ Super.call(this, val); // 核心 // ... } var sub1 = new Sub(1); var sub2 = new Sub(2); sub1.arr.push(2); alert(sub1.val); // 1 alert(sub2.val); // 2 alert(sub1.arr); // 1, 2 alert(sub2.arr); // 1 alert(sub1.fun === sub2.fun); // false

借父类的构造函数来增强子类实例,等于是把父类的实例属性复制了一份给子类实例装上了(完全没有用到原型)。

优点:

  • 解决了子类实例共享父类引用属性的问题
  • 创建子类实例时,可以向父类构造函数传参

缺点:

  • 无法实现函数复用,每个子类实例都持有一个新的fun函数,太多了就会影响性能,内存溢出。

3.3 组合继承

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
function Super(){ // 只在此处声明基本属性和引用属性 this.val = 1; this.arr = [1]; } // 在此处声明函数 Super.prototype.fun1 = function(){}; Super.prototype.fun2 = function(){}; //Super.prototype.fun3... function Sub(){ Super.call(this); // 核心 // ... } Sub.prototype = new Super(); // 核心 var sub1 = new Sub(1); var sub2 = new Sub(2); alert(sub1.fun === sub2.fun); // true

把实例函数都放在原型对象上,以实现函数复用。同时还要保留借用构造函数方式的优点,通过Super.call(this);继承父类的基本属性和引用属性并保留能传参的优点;通过Sub.prototype = new Super();继承父类函数,实现函数复用。

优点:

  • 不存在引用属性共享问题
  • 可传参,并且函数可复用

缺点:

  • 子类原型上有一份多余的父类实例属性,因为父类构造函数被调用了两次,生成了两份,而子类实例上的那一份屏蔽了子类原型上的。。。

3.4 寄生组合继承

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
function beget(obj){ // 其实只需要原型的一份copy var F = function(){}; F.prototype = obj; return new F(); } function Super(){ // 只在此处声明基本属性和引用属性 this.val = 1; this.arr = [1]; } // 在此处声明函数 Super.prototype.fun1 = function(){}; Super.prototype.fun2 = function(){}; //Super.prototype.fun3... function Sub(){ Super.call(this); // 核心 // ... } var proto = beget(Super.prototype); // 核心 proto.constructor = Sub; // 核心 Sub.prototype = proto; // 核心 var sub = new Sub(); alert(sub.val); alert(sub.arr);

用beget(Super.prototype)切掉了原型对象上多余的那份父类实例属性。
暂时看起来是集合了上述几种方法的优点,使用起来会稍微麻烦一点。

3.5 关于beget函数

              
  • 1
  • 2
  • 3
  • 4
  • 5
function beget(obj){ // 其实只需要原型的一份copy var F = function(){}; F.prototype = obj; return new F(); }

beget函数的意义在于不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。
这个函数其实在IE9+、Firefox 4+、Safari 5+、Opera 12+和Chrome中就是Object.create()方法。

此外JS中还有原型式继承和寄生式继承,它们也有明显的缺点,适用于特点的场景,使用不频繁,在此不赘述了。

四、数据类型判断

JavaScript中有5种基本数据类型和一种复杂数据类型。

  • Null, Undefined
    特殊值
  • number
    所有的数值类型,也包括伪数值NaNInfinity
  • boolean
    truefalse
  • string
    字符类型

其余类型在JavaScript中都为Object类型,比如functionarray

1. typeof方法

typeof方法是JavaScript中返回类型的原生方法。

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
typeof undefined // "undefined" typeof 0 // "number" typeof true // "boolean" typeof "foo" // "string" typeof {} // "object" // 注意下面三行 typeof null // "object" typeof function(){} // "function" typeof NaN // "number"
  • typeof null == "object"算是JS语言中的一个内置Bug。
  • 按照语言数据类型定义,Functions应该属于Object类型。
  • typeof NaN == 'number'略显滑稽,NaN本来是Not A Number的缩写。

此外

              
  • 1
  • 2
  • 3
typeof {} // 'object' typeof [] // 'object' typeof new Date // 'object'

typeof无法识别具体的Object类型。

简而言之,typeof函数适用于基本类型的判断。同时,由于Uncaught ReferenceError的存在,不要用typeof函数检测变量的存在性。最佳实践是:

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
function f(x) { if (typeof x == 'function') { ... // in case when x is a function } else { ... // in other cases } }

2. toString方法

toString方法是最为可靠的类型检测手段,它会将当前对象转换为字符串并输出。toString属性定义在Object.prototype上,因而所有对象都拥有toString方法。 但Array, Date等对象会重写从Object.prototype继承来的toString, 所以最好用Object.prototype.toString来检测类型。

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
toString = Object.prototype.toString; toString.call(new Date); // [object Date] toString.call(new String); // [object String] toString.call(Math); // [object Math] toString.call(3); // [object Number] toString.call([]); // [object Array] toString.call({}); // [object Object] // Since JavaScript 1.8.5 toString.call(undefined); // [object Undefined] toString.call(null); // [object Null]

对于原生对象和内置数据类型,toString方法是非常适用的。

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
function Animal(name) { this.name = name } var animal = new Animal("Goofy") var class = {}.toString.apply(animal) alert(class) // [object Object]

3. 检测自定义类型

检测自定义类型通常使用instanceof方法。

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
function Animal(name) { this.name = name } var animal = new Animal("Goofy") alert( animal instanceof Animal ) // true

4. Duck Typing

目前前端惯例是使用Duck Typing的方式,比如jQuery是这样判断一个Window的:

              
  • 1
  • 2
  • 3
isWindow: function(obj){ return obj && typeof obj === 'object' && "setInterval" in obj; }

五、使用匿名函数

匿名函数就是指没有分配名称,直接通过函数表达式定义的函数。关于函数表达式和函数声明的区别在上节

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
// function expression var sayHello = function (){ alert("hi"); }; // in an object var Person = { sayHello: function(){ alert("hi"); } }; // event handler $("p").click = function(){ alert("hello"); }; // ajax callback from http://api.jquery.com/jQuery.ajax/ $.ajax({ url: "test.html", context: document.body, success: function(){ $(this).addClass("done"); } }); // Self-evoking anonymous functions ((function () { alert("hi"); })());

使用匿名函数

匿名函数有如下好处:

  • 代码简洁性,通常匿名函数会在回调函数和事件处理函数中被调用。
  • 控制作用域,匿名函数可以创建私有的作用域,不会干扰其他作用域。
  • 匿名函数可以便于实现闭包和递归函数。
以jQuery为例
              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
(function( window, undefined ) { var jQuery = (function() { }); // Expose jQuery to the global object window.jQuery = window.$ = jQuery; // })(window);

上面是个立即执行的匿名函数,其中定义的变量作用范围都是匿名函数以内,不会影响全局域。而且在函数执行完后,其作用域链也会及时清理。

在递归函数中也常常使用匿名函数

通常会配合arguments.callee方法一起使用,避免函数名称变更引起的递归错误。

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
function f(n) { if (n < 2) return n; if (f.answers[n] != null) return f.answers[n]; f.answers[n] = arguments.callee(n - 1) + arguments.callee(n - 2); return f.answers[n]; } f.answers ={}; var fibo = f; alert(fibo(10)); //55 alert(fibo(11)); //89

这种机制确保即使将函数赋值给另外的变量,也能正常实现函数递归;同时,将每次递归的计算值保存,避免了多次重复的计算过程。

在闭包中的使用

各种专业文献上的"闭包"(closure)定义非常抽象,很难看懂。我的理解是,闭包就是能够读取其他函数内部变量的函数。

由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。

所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。而匿名函数和闭包基本上是相辅相成的。

              
  • 1
  • 2
  • 3
$('#foo').click( function() { alert('User clicked on "foo."'); });

在闭包中使用循环常常出现错误,比如

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
for (var i = 1; i <= 10; i++) { $('#Div' + i).click(function () { alert('#Div' + i + " is kicked"); return false; }); }

上述代码会输出10次‘#Div10 is kicked’,而利用匿名函数做下修改,

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
for (var i = 1; i <= 10; i++) { (function(v){ $('#Div' + v).click(function () { alert('Div' + v + " is kicked"); return false; }) })(i); }

此时,代码的输出会如我们预期。

更多闭包的细节将在下一章中展开。

A quiz
              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
var foo = { bar: function() { return this.baz; }, baz: 1 }; (function(){ return typeof arguments[0](); })(foo.bar);

返回值是“undefined”

关注this的指向是理解这个过程的重点,开始调用foo.bar()时,this指向foo,而在匿名函数中调用arguments[0](),this指向arguments,显然arguments.bazundefined

六、闭包

要理解闭包,首先必须理解Javascript特殊的变量作用域。

变量的作用域无非就是两种:全局变量和局部变量。

闭包的定义

闭包就是能够读取其他函数内部变量的函数。

闭包这个概念引入了三个作用域链,访问其自己的作用域,访问外部作用域和访问全局作用域。

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
function showName (firstName, lastName) { var nameIntro = "Your name is "; ​function makeFullName () { return nameIntro + firstName + " " + lastName; } return makeFullName (); } showName("Michael", "Jackson"); // Your name is Michael Jackson
              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
$(function() { var selections = []; $(".inners").click(function() { // this closure has access to the selections variable​ selections.push (this.prop("name")); // update the selections variable in the outer function's scope​ }); });

闭包的使用原则和注意事项

闭包可以在外部函数返回后访问其变量

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
function celebrityName (firstName) { var nameIntro = "This celebrity is "; // this inner function has access to the outer function's variables, including the parameter​ function lastName (theLastName) { return nameIntro + firstName + " " + theLastName; } return lastName; } ​ ​var mjName = celebrityName("Michael"); // At this juncture, the celebrityName outer function has returned.​ ​ ​// The closure (lastName) is called here after the outer function has returned above​// Yet, the closure still has access to the outer function's variables and parameter​ mjName ("Jackson"); // This celebrity is Michael Jackson

celebrityName返回后,实际上相当于在全局域上创建了一个函数。

              
  • 1
  • 2
  • 3
  • 4
mjName = function lastName(theLastName) { var firstName = "Michael"; return nameIntro + firstName + " " + theLastName; }

闭包储存了外部函数变量的引用

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
function celebrityID () { var celebrityID = 999; // We are returning an object with some inner functions​ // All the inner functions have access to the outer function's variables​ return { getID: function () { // This inner function will return the UPDATED celebrityID variable​ // It will return the current value of celebrityID, even after the changeTheID function changes it​ return celebrityID; }, setID: function (theNewID) { // This inner function will change the outer function's variable anytime​ celebrityID = theNewID; } } } var mjID = celebrityID(); // At this juncture, the celebrityID outer function has returned mjID.getID(); // 999 mjID.setID(567); // Changes the outer function's variable mjID.getID(); // 567: It returns the updated celebrityId variable

闭包失效

参考在闭包中的使用,返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量。如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变。

七、 setTimeout和setInterval

setTimeout和setInterval方法都是浏览器对象,即BOM提供的方法。这两个方法分别用来进行延时执行,和设置时间间隔执行代码。

1.1 基本用法

setTimeout用来对代码进行一个延时执行,定义为:

              
  • 1
window.setTimeout("javascript function", milliseconds);

两个参数分别是执行代码和延时时间,可以用clearTimeout来取消定时:

              
  • 1
  • 2
var timer=setTimeout("alert('hello world')", 5000); clearTimeout(timer);

setInterval方法用来设定一个时间间隔来执行代码,其定义方式和setTimeout方法类似:

              
  • 1
window.setInterval('javascript function', milliseconds)

同样的,可以用clearInterval方法来取消setInterval。

1.2 深入原理

举个简单的例子

              
  • 1
  • 2
  • 3
  • 4
  • 5
function test(){ setTimeout('console.log(1)',0); console.log(2); } test();

结果是先打印2,再打印1,这不难理解。

setTimeout中的代码会在单独的线程中执行,不同的线程有优先级的区别。

再看一个特殊的例子

              
  • 1
  • 2
setTimeout( function(){ while(true){} } , 100); setTimeout( function(){ console.log(’你好!’); } , 200);

这段代码不会打印“你好!”了。

这表明js是单线程的运行在浏览器引擎中,只要上面代码没有执行完,下面代码是永远不会执行的。而实际上浏览器提供的两个计时函数,只是告诉浏览器,我要插入回调函数到待执行队列的时间点,而不是执行的时间点。而什么时候执行,则取决于当前执行队列是否空闲。

1.3 一些例子

看下面的例子,

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
for (var i = 0; i < 3; i++) { setTimeout(function() { console.log(i); }, 0); console.log(i); }

结果是:0 1 2 3 3 3

这个例子中体现了“异步、作用域、闭包”等内容,其实上面的代码等价于:

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
var i = 0; setTimeout(function() { console.log(i); }, 0); console.log(i); i++; setTimeout(function() { console.log(i); }, 0); console.log(i); i++; setTimeout(function() { console.log(i); }, 0); console.log(i); i++;

又因为setTimeout是单独的线程,需要等待前面的代码执行完成后再执行,继续等价:

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
var i = 0; console.log(i); i++; console.log(i); i++; console.log(i); i++; setTimeout(function() { console.log(i); }, 0); setTimeout(function() { console.log(i); }, 0); setTimeout(function() { console.log(i); }, 0); //弹出 0 1 2 3 3 3

再举个例子:

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
function Obj(msg){ this.msg=msg; this.shout=function(){ alert(this.msg); } this.waitAndShout=function(){ setTimeout(this.shout, 2000); } } var aa = new Obj("xixi"); aa.waitAndShout();

结果为undefined,这个例子说明是setTimeout里面函数的内部,也就是第一个参数的内部指向window,所以弹出了undefined。

解决的方式也很简单,利用var that = this做一下this的绑定即可

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
function Obj(msg){ this.msg = msg; var that = this; this.shout = function(){ alert(that.msg); } this.waitAndShout = function(){ setTimeout(this.shout,5000); } } var obj = new Obj('xixi'); obj.waitAndShout();

八、 EVENT LOOP

JavaScript的时间循环机制就是主线程在执行间隙去“任务队列”中读取之前添加好的时间,循环往复。

摘张经典大图《Help, I'm stuck in an event-loop》

https://static.littlewin.wang/blog/19-1.jpg

如上图所示,主线程开启堆和栈区,在栈中执行代码,同时调用外部API在“任务队列”中添加各种事件。栈中的代码执行完毕后,主线程去读取任务队列,依次执行相应的回调函数。

本文于 2016-2-28  发布在  编程  分类下, 当前已被围观 290 次

并被添加「 JavaScript 前端 」标签

本站使用「 署名 4.0 国际」创作共享协议