涉及知识点广泛的一道题分析总结

function Foo() {  
    getName = function () {   
        console.log (1);   
    };  
    console.log('this is'+this)  
    return this;  
}  
Foo.getName = function () {   
    console.log (2);   
};  
Foo.prototype.getName = function () {   
    console.log('baidu' && 'google');   
};  
var getName = function () {   
    console.log (4);  
};  
function getName() {   
    console.log (5);  
}  
  
// 请写出一下的输出结果  
Foo.getName();   
getName();   
Foo().getName();    
getName();    
new Foo.getName();    
new Foo().getName();    
new new Foo().getName();   

  此题涉及的知识点众多,要做对此题,你得有一定的分析问题的能力,还得具备包括变量定义提升、声明变量与声明函数的提升优先级、this指针指向、运算符优先级、原型、继承、全局变量污染、对象属性及原型属性优先级、覆盖等知识点。

  我们先看此题的上半部分做了什么:首先定义了一个叫Foo的函数,之后为Foo创建了一个叫getName的静态属性存储了一个匿名函数,之后为Foo的原型对象新创建了一个叫getName的匿名函数,之后又通过函数变量表达式创建了一个getName的函数,最后再声明一个叫getName函数。

  我们先把上面5个条件进行分类编号分析:

// 方式1:一个构造函数,里面有个全局变量getName 指向一个匿名函数(小心闭包)  
function Foo() {  
    getName = function () {   
        console.log (1);   
    };  
    return this;  
}  
// 方式2:构造函数的一个属性getName 指向一个匿名函数  
Foo.getName = function () {   
    console.log (2);   
};  
// 方式3:构造函数的原型上有个getName方法  
Foo.prototype.getName = function () {   
    console.log('baidu' && 'google');   
};  
// 方式4:定义一个变量指针指向一个匿名函数  
var getName = function () {   
    console.log (4);  
};  
// 方式5:声明一个叫getName的有名函数  
function getName() {   
    console.log (5);  
}

  每块函数的意义都清楚了,接下来再分析执行语句的代码:

1、Foo.getName();

  自然是访问Foo函数上存储的静态属性,相当于直接指向方式2,打印2;

2、getName();这里就需要考虑一个函数声明提升、变量声明提升与赋值的问题。

  直接调用 getName 函数。既然是直接调用那么就是访问当前上文作用域内的叫getName的函数,所以跟1 2 3都没什么关系。此处有两个坑,一是变量声明提升,二是函数表达式。

  如果单独看这条执行语句,一般会有个全局声明的函数方式5"function getName(){} ",但要是放在这么个复杂的上下文环境中,那肯定是不一样的啦,因为还有个方式4 “var getName = function(){}” 来捣乱,这就要考察一个知识点:当定义的变量和声明的函数重名了怎么办?答:它们都会进行预解析,函数声明提前于变量声明,但是最终会被变量覆盖!so方式4获胜,输出结果为“4”!

//代码最终执行的是
function Foo() {
    getName = function () { alert (1); };
    return this;
}
var getName;//只提升变量声明
function getName() { alert (5);}//提升函数声明,覆盖var的声明
 
Foo.getName = function () { alert (2);};
Foo.prototype.getName = function () { alert (3);};
getName = function () { alert (4);};//最终的赋值再次覆盖function getName声明
 
getName();//最终输出4

3、Foo().getName();

  本句有个执行顺序的,先执行方式1的“Foo()”,结果是"this" 并指向window,并产生了一个全局getName(window.getName)指针指向一个匿名函数,然后再执行"this.getName()", 其实就是执行刚刚造出来的那个全局getName指向的匿名函数,所以输出“1”。

  这里还有个问题需要注意,就是这个Foo()造出来的那个全局变量getName,已经把之前声明的方式4给覆盖了。这就是同名的变量,后出现的覆盖之前出现的原因。

  Foo函数的第一句  getName = function () { alert (1); };  是一句函数赋值语句,注意它没有var声明,所以先向当前Foo函数作用域内寻找getName变量,没有。再向当前函数作用域上层,即外层作用域内寻找是否含有getName变量,找到了,也就是第二问中的alert(4)函数,将此变量的值赋值为 function(){alert(1)}。此处实际上是将外层作用域内的getName函数修改了。

  注意:此处若依然没有找到会一直向上查找到window对象,若window对象中也没有getName属性,就在window对象中创建一个getName变量。

  此处考察了两个知识点,一个是变量作用域问题,一个是this指向问题。

4、getName();

  此句执行的是方式1执行出来的那个全局变量getName 指针指向的匿名函数,有人问为啥不执行方式4?因为已经说过了,方式4已经被Foo()后创建出来的全局getName覆盖了!所以结果为 “1”。

5、new Foo.getName()

  首先还是先看运算符优先级吧,我自个看完的结果是【new Foo() >  Foo() > new Foo】,先运算方式2的Foo.getName() 结果为“2”,再new一个Foo实例对象。

  通过查上表可以得知点(.)的优先级高于new操作,遂相当于是:new (Foo.getName)()所以实际上将getName函数作为了构造函数来执行,所以是2。

6、new Foo().getName()

  运算符优先级括号高于new,实际执行为 (new Foo()).getName();先执行 new Foo(),结果产生一个新的实例对象,并且继承了Foo()这个构造函数中的getName方法,所以再执行方式3函数块,

  而接着有涉及到另一个知识点:逻辑运算符运算, 对于 “&&”来说,如果前者为真,那么就去执行后者,否则只执行前者; 对于“||”来说,如果前者为真,只执行前者,后者不必执行,否则还得执行后者。因此结果为 “google”。

  这里需要说明下js中的构造函数返回值问题。

构造函数的返回值

  在传统语言中,构造函数不应该有返回值,实际执行的返回值就是此构造函数的实例化对象。而在js中构造函数可以有返回值,也可以没有。

1、没有返回值则按照其他语言一样返回实例化对象。

2、若有返回值则检查其返回值是否为引用类型。如果是非引用类型,如基本类型(string,number,boolean,null,undefined)则与无返回值相同,实际返回其实例化对象。

3、若返回值是引用类型,则实际返回值为这个引用类型。

  原题中,返回的是this,而this在构造函数中本来就代表当前实例化对象,所以最终Foo函数返回实例化对象。之后调用实例化对象的getName函数,因为在Foo构造函数中没有为实例化对象添加任何属性(没有用var声明,所以getName是window下,而不是实例化对象,这点需要注意),故到当前对象的原型对象(prototype)中寻找getName。

7、new new Foo().getName()

  先执行new Foo(),变成了 new Foo的实例对象.getName(),然后再执行 Foo的实例对象.getName(),又回到了方式3函数块,结果为“google”,最后执行new Foo的实例对象。

  同样是运算符优先级问题,最终实际执行为:new ((new Foo()).getName)(),先初始化Foo的实例化对象,然后将其原型上的getName函数作为构造函数再次new。

最终结果:

原文地址:https://www.cnblogs.com/goloving/p/8556972.html