首先还是那句话:
答面试题,不要背题!不要背题!不要背题!题可以有一千一万道,但道理可能就几十行字,所以,请研究道理,而不要背题。努力站在语言创始人的角度看待语言,你会发现,语言其实很亲切,并不难懂,绝大多数情况下,语言的每一个特征都是有它存在的道理的,你只需弄懂这些道理。
下面几点能应付百分之九十五的面试题,前提是严格按我的知识点和“算草”做题。
知识点
0、名词解释:
算草就是演算的草稿,谢谢。
var a;
我称为声明,也叫定义
var a = 1;
我称为赋值
1、作用域内的变量可能有5种情况:
- 它指向外层函数本身(隐式变量赋值)
- 它指向上层作用域的变量 (隐式变量赋值)
- 它是作用域内的参数(隐式变量赋值)
- 它是个本作用域函数声明
- 它是个本作用域变量
什么叫隐式变量赋值?这个JS引擎的赋值操作是我假想的操作(也可能是JS解析器真实的操作),不管怎样,真实效果等同于确实存在隐式变量赋值。
- 函数声明和函数表达式的函数名,会在内部作为变量最先隐式赋值,指向自身。
- 上层作用域的变量,在本层作用域内隐式赋值,是第二优先隐式赋值。
- 本作用域内的参数是第三优先隐式赋值。
这三种隐式赋值是越先赋值,越早被覆盖。
隐式赋值一定会被显式赋值覆盖。显式赋值包括变量赋值跟函数声明。
算草:把三种隐式赋值全显式写出来,写在作用域最顶部,比如下方的伪代码的写法。当然你熟练之后并不需要挨个严格地写出来,心算即可。
var g = 函数自身;
var h = 上层变量h;
var g = 上层变量g,覆盖本层变量g;
var para = 传入的参数值4;
2、所有末定义直接赋值的变量由JS解析器自动声明为拥有全局作用域,a等同于window.a,但是要注意函数的参数是隐式定义并赋值的,不要当做未定义直接赋值的变量,同时也别忘了具名函数表达式、具名函数声明的具名在内部是隐式定义并赋值的,值就是函数表达式和函数声明本身。
算草:给所有这种变量改成“window.变量名”的形式。
3、所有window对象的属性拥有全局作用域。在全局作用域里,var a = 4
跟window.a = 3
是指同一个变量。
4、变量定义提升和函数声明提升:
- 所有变量定义都自动挪到当前作用域的尽可能靠上的位置,比如:
console.log(a); // undefined
var a = 3;
相当于
var a; // 这就是变量声明,并没有赋值动作
console.log(a); // undefined
a = 3;
- 所有函数声明也自动挪到当前作用域的尽可能靠上的位置。
console.log(a); // function a() {}
function a() {};
相当于
function a() {};
console.log(a); // function a() {}
函数声明和变量定义、变量赋值的提升原则:
原则是:函数声明覆盖变量定义,无论谁先写谁后写;函数声明会被变量赋值覆盖,无论谁先写谁后写。
函数声明和变量定义、变量赋值的提升操作:
- 从作用域内代码第一行到最后一行过一遍,凡是遇到变量声明或变量赋值语句,比如
var a;
或者b = 4;
,就在作用域顶部写上var a; var b;
,注意按顺序写。 - 重新过一遍,找函数声明,找到之后往上拽,直到遇到同名的
var a;
语句,就放到语句下面,这意味着,如果函数声明的下方有a的赋值语句,那么就按a的赋值为准,如果没有,就按函数声明为准。
5、变量的覆盖规律:如果你严格按照我的算草,把隐式赋值改成显式赋值,然后把变量定义提升,把函数声明提升,那么,覆盖规律就六个字:下面写的覆盖上面写的。如果你不听我的算草的建议,那么覆盖规律就很乱了。。。
6、作用域链:
- 给某变量a赋值的时候,如果当前作用域查不到变量名a,包括隐式赋值的都查不到的话,就到上一级去查a,直到查到全局作用域,依然差不到就视为window.a。
- 利用a没有赋值的前提下就做计算的话,如果一直查到全局作用域都查不到,就JS报错。
function b() {
a = 5;
}
b();
a;
// 5
function b() {
return a++;
}
b();
// Uncaught ReferenceError: a is not defined(…)
7、函数声明跟函数表达式是两码事,当长相一致的时候,ECMAScript是通过上下文来区分的,如果function foo(){}是作为赋值表达式的一部分的话,那它就是一个函数表达式,如果function foo(){}被包含在一个函数体内,或者位于全局的话,那它就是一个函数声明。
总结两者区别:
- 函数表达式不具有函数声明提升的特性。
- 函数表达式的具名,只在内部作用域有意义,也就是上文说的隐式声明,在外部不存在;这跟函数声明不一样,函数声明的函数名,在外部作用域也一样存在。具名函数表达式的具名,虽然在外部等于白写,但对于内部有意义,所以,究竟应不应该具名要视内部需要而定。
证明函数表达式的具名在外部没意义:
var a = function b() {};
b; // Uncaught ReferenceError: b is not defined(…)
证明函数表达式和函数声明的具名在内部有意义:
var b = function a() {
console.log(a);
}
b();
// function a() {
// console.log(a);
// }
function c() {
console.log(c);
}
c();
// function c() {
// console.log(c);
// }
证明函数声明和函数表达式的具名都会被参数覆盖:
var b = function a(a) {
console.log(a);
}
b(1);
// 1
function a(a) {
console.log(a);
}
a(1);
// 1
另外,自执行函数等同于函数表达式跟执行写在一起了,本质还是函数表达式。括号()是一个分组操作符,它的内部只能包含表达式。
函数故意不传入参数,视为传入undefined,而不是真的什么都没传入。
证明:
var b = function a(a) {
console.log(a);
}
b();
// undefined,说明JS并没有忽略参数a,而是给它隐式赋值了一个undefined
8、new到底new个啥?
按照javascript语言精粹中所说,如果在一个函数前面带上new来调用该函数,那么new干四件事:
1、将创建一个隐藏连接到该函数的prototype成员的新对象。
2、将函数的作用域赋给新对象,所以this也被绑定到那个新对象上。
3、像执行普通函数一样,执行一遍函数。
4、返回这个新对象。
怎么理解?首先,我就知道有个匿名对象被创建了。其次,我就知道函数里的this指向不是原来的指向了,而是这个匿名对象。最后,执行一遍函数。举例说:
function a() {
this.b = 1;
}
console.log(new a());
// a {b: 1} 没错吧?是个对象吧?这个对象目前匿名,不要以为它叫a
var c;
function a() {
this.b = 1;
alert(2);
c = 3;
}
console.log(new a());
console.log(c);
// 弹出2
// 3 没错吧?弹出了2,又打印了3,说明函数里所有的语句都执行了。
function a() {
}
console.log(new a());
// a {} 一个匿名的空对象
讲一个概念:构造函数,构造函数是个相对概念,需要结合上下文来定义,一个函数只有被new过至少一次,它才是构造函数,而且也只在new的时候是构造函数。说白了,被new的函数就是构造函数。
构造函数里一般不应该出现return语句,也应该含有尽量少的运算,因为运算应该是new出来的实例对象干的事情。但是,面试题里面往往故意给构造函数里加入return和运算,你只需要知道,如果return返回的值是一个对象,它会代替新创建的对象实例返回,如果返回的值是一个原始类型,它会被忽略,新创建的实例会被返回,至于其他的运算,每new一次,运算都会执行一次。
如果你打算把这个函数当构造函数用,你就别当做普通函数用。不要一函数两用。
function a() {
return 1;
}
console.log(new a());
// a {} 依然是一个匿名的空对象,return 1其实也是执行了,但对当前代码来讲没有意义
function Test1(str) {
this.a = str;
this.b = function () {
return this.a;
}
}
var myTest = new Test1("test1");
console.log(myTest.b());
//"test1"
如果把new出来的对象赋值给另一个函数的prototype对象,会是怎样?
function Test1(str) {
this.a = str;
this.b = function () {
return this.a;
}
}
function Test2(str) {
this.c = str;
}
Test2.prototype = new Test1("test1"); // 这一行叫原型继承
Test2.prototype.d = function () { // 给新的构造函数的原型对象增加新的方法
return this.c;
};
var myTest2 = new Test2("test2");
console.log(myTest2);
console.log(myTest2.a);
console.log(myTest2.b());
console.log(myTest2.c);
console.log(myTest2.d());
执行结果:
由此看到,将函数1的new出来的对象赋值给函数2的原型,函数2就继承了函数1的所有属性和所有方法,这就是JS的原型继承。
10、call和apply
直接看函数对象的call()方法和apply()方法和ES5引入的函数对象的bind()方法
例题
以下例题的解答中,我没有显式给上层作用域变量赋值,因为真心没必要手写,心算一下即可。
例题1:
function a(b){
alert(b);
b = function (){
alert(b);
}
b();
}
a(1);
算草:
function a(b){
var b = 1; // 显式传入1
alert(b); // 弹出b的值,也就是1
b = function (){ // 变量b被覆盖为一个函数表达式
alert(b); // 这个b在当前作用域没定义,就往上一层作用域中寻找,于是寻找到b的值是个函数表达式
}
b(); // 由于b在这之前是个函数表达式,所以b()表示执行表达式
}
a(1);
例题2:
function t(g){
var g='hello';
alert(g);
function g(){
console.log(g);
}
alert(g);
}
t(null);
算草:
function t(g){
var g = null; // 传入null
function g(){ // 这个函数声明前置,覆盖掉null
console.log(g);
}
var g = 'hello'; // 又一次的覆盖,重置为hello
alert(g); // 弹出hello
alert(g); // 还是弹出hello
}
t(null);
例题3:
function a(b){
alert(b);
function b(){
alert(b);
}
b();
}
a(1);
算草:
function a(b){
var b = 1; // 显式化参数赋值
function b() { // b 被覆盖为函数声明
var b = 函数本身; // 这行就是函数名变量的显式赋值
alert(b); // 弹出函数声明的字面代码
}
alert(b); // 弹出函数声明的字面代码
b(); // 执行函数
}
a(1);
例题4:
function Foo() {
getName = function () { alert (1); };
return this;
}
Foo.getName = function () { alert (2);};
Foo.prototype.getName = function () { alert (3);};
var getName = function () { alert (4);};
function getName() { alert (5);}
//请写出以下输出结果:
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
算草:
var getName; // 捋第一遍,只捋变量,然后把变量声明前置
function Foo() {
var getName = 上层变量;
getName = function () { alert (1); };
return this; // 指向window
}
function getName() { alert (5);} // 捋第二遍,只捋函数声明,然后把函数声明前置,由于getName并没有赋值,所以目前getName等于这个函数声明
Foo.getName = function () { alert (2);}; // 给Foo函数对象加一个方法
Foo.prototype.getName = function () { alert (3);};
getName = function () { alert (4);}; // 由于顶部已经声明了getName,为避免混乱,我删掉了var关键字。变量getName又被覆盖为一个函数表达式
//请写出以下输出结果:
Foo.getName(); 执行Foo的getName方法,当然就是alert(2),弹出2,这一步最稳,没啥可说的
getName(); 变量getName被覆盖了两次,根据最新的覆盖,getName目前是个函数表达式,执行这个表达式就是alert(4),弹出4
Foo().getName(); 这个要拆成两步计算,第一步是Foo(),考察this知识,this指向window,所以执行函数返回window,第二步等同于window.getName(),但此时请注意,由于Foo()的执行,全局变量getName的值又变了,等于被第3次赋值,赋值为一个表达式,也就是alert(1),弹出1
getName(); 它等同于window.getName(),所以跟上一行一样,alert(1),弹出1
new Foo.getName(); 运算顺序是先Foo.getName,返回alert2的函数表达式,再new这个表达式,于是弹出2
new Foo().getName(); 相当于(new Foo()).getName(),显然括号里运算结果是Foo构造函数的实例化对象,这个对象的getName方法就是alert3,弹出3
new new Foo().getName(); 按照运算优先级,改写成new ((new Foo()).getName)(); 第一步是new一个实例,第二步得到alert3函数表达式,第三步new这个表达式的实例,于是还是弹出3
例题5:
if (!("a" in window)) {
var a = 1;
}
alert(a); // undefined
算草:
var a;
if (!("a" in window)) { // "a" in window是true,即使window.a的值是undefined
var a = 1; // if不成立,所以这句不执行
}
alert(a); // 只声明了a没有赋值,当然是undefined
例题6:
var a = 1,
b = function a(x) {
x && a(--x);
};
alert(a); // 1
算草:
var a = 1,
b = function a(x) {
x && a(--x);
};
alert(a); // 函数表达式的函数名称在作用域不生效,只在函数内部生效
例题7:
function b(x, y, a) {
arguments[2] = 10;
alert(a);
}
b(1, 2, 3);
算草:
需要先了解一下arguments的知识,arguments是函数作用域内的一个数组(但是又不完全是数组,这里不深入讨论),arguments的每一个元素都指向参数值。也就是arguments[0]指向x,arguments[1]指向y,arguments[2]指向a。如果函数参数有3个,但传入的参数有四个,比如b(1, 2, 3, 4),这时arguments[3]值为4。这道题,arguments[2]重新赋值为10,所以a的值也重新赋值为10。
算草代码如下:
function b(x, y, a) {
arguments[2] = 10;
alert(a);
}
b(1, 2, 3); // 10
例题8:
var b = 5;
function a() {
var b = 10;
alert(this.b);
}
a.call(null);
算草:
这道题考察.call的知识,参考函数对象的call()方法和apply()方法,当传入函数为null或undefined时会怎样呢?此时传入的是window。严格模式下传入的是undefined,但是题目没有给出严格模式的代码,所以还是window。
var b = 5;
function a() {
var b = 10; // 只是改变了本作用域的b的值,window.b的值依然是5
alert(this.b); // call的作用就是改变this的指向,现在它指向window,所以弹出5
}
a.call(null);
例题9:
function B(a) {
this.a = a;
}
function C(a) {
if (a) {
this.a = a;
}
}
B.prototype.a = 1;
C.prototype.a = 1;
console.log(new B().a); // undefined
console.log(new C(2).a); // 2
算草:
再复习一遍new的操作:
1、将创建一个隐藏连接到该函数的prototype成员的新对象。
2、将函数的作用域赋给新对象,所以this也被绑定到那个新对象上。
3、像执行普通函数一样,执行一遍函数。
4、返回这个新对象。
然后复习一下原型继承:
给一个构造函数的原型增加一个属性,那么构造函数new出来的对象就会继承构造函数的原型,也就多了一个属性。但是,如果new出来的对象本身已经有这个属性,那么它就不会继承属性。
先说console.log(new B().a);
,首先有了一个新对象,目前是{}。然后,this也被绑定到这个新对象上,所以空对象的a属性等于传入的参数值,然而并没有传入参数值,所以空对象的a属性等于undefined。所以答案是undefined。尽管a属性的值是undefined,但是a属性的确是存在的,所以不继承,B.prototype.a = 1;
等于白写。
再说console.log(new C(2).a)
,首先还是有了一个空对象,然后,this也被绑定到这个新对象上,所以空对象的a属性等于传入的参数值,是2。既然a属性是存在的,所以不继承,C.prototype.a = 1;
等于白写。
例题10:
var a = 1;
function b() {
var a = 2;
function c() {
console.log(a);
}
return c;
}
b()(); // 2
算草:
b()()实际上是先b(),然后返回的函数再执行一次。
那么b()得到什么?表面看是得到函数c。但根据词法作用域的规则,c在定义的时候写在哪里,它就永远在哪里,无论谁引用这个c,只要是执行c,都是基于写的位置来考虑执行。所以,b()得到函数b作用域下的函数c。
那么b()()实际上是函数b作用域下的函数c执行一次:打印变量a。c的内部作用域里没有定义变量a,往上追溯,追到了var a = 2;
,于是打印2。