一些经典面试题分析(上)

引用变量赋值传递

最新的 ECMAScript 标准定义了 7 种数据类型:javascript

  • 6 种原始类型(即值类型):java

    • Booleangit

    • Nullgithub

    • Undefined数组

    • Number浏览器

    • String(在许多语言中,字符串都被看做引用类型,而非原始类型,由于字符串的长度是可变的。ECMAScript 打破了这一传统。)app

    • Symbol (ECMAScript 6 新定义)函数

  • Object(即引用类型,包括Array、function这些)this

值类型:存储在栈(stack)中的简单数据段,也就是说,它们的值直接存储在变量访问的位置。spa

引用类型:存储在堆(heap)中的对象,也就是说,存储在变量处的值是一个指针(point),指向存储对象的内存处。

以下图所示:

图片描述

//第一题
//看完了前言 那么就来看题目了
var obj = {n: 1}
var obj2 = obj
obj2.n = 2
console.log(obj.n) // ?   输出 2, obj和obj2都是引用类型,他们都是在栈里保存的都是指针,指向了堆中同一个地址,因此修改其中一个另一个也会跟着改变

function fn1(a) {
  a.n = 3
}
fn1(obj)
console.log(obj.n) // ?   输出 3
/*
这又涉及到JavaScript中函数的传参机制,主要有两种状况
1. 若是参数是值类型
    基本数据类型的变量基本上都是值类型,例如字符串,数值,布尔值等。
    值类型的传参是值传递,函数内对这种类型参数的修改不会影响到原来传入的值。
2. 若是参数是引用类型
    复合数据类型如对象,数组等都是引用类型。
    引用传参是引用传递,函数内对这种类型参数的修改都会影响到原来传入的值。
    
因此上面fn1函数调用的时候会将 实参obj赋值给形参a,a与obj都指向堆中同一个地址,此时修改a.n会影响到obj.n的值
*/

function fn2(a) {
  a = {n:4}
}
fn2(obj)
console.log(obj.n) // ?  输出的仍是 3
/*
函数fn2调用的时候会会将 实参obj赋值给形参a,a与obj都指向堆中同一个地址,可是此时修改了a的指向,是其指向堆中另一个地址,因此最后输出obj.n的时候不会发生任何变化
*/
// 第二题
var a = {n: 1}
var b = a
a.x = a = {n: 2}
console.log(a) // ? {n:2}
console.log(b) 
/* ? b为 
{
  n:1,
  x:{
    n:2
  }
}
*/
分析过程以下图
ps:其实这个分析过程是一个连续的过程,光凭一张图有点难展现的很好,能说说明的也不是太详细。

!图片描述

做用域与做用域链

引用自《You-Dont-Know-JShttps://github.com/fishenal/Y...

​ 做用域有两种常见的模型,一种叫作 词法做用域 (Lexical Scope),一种叫作动态做用域 (Dynamic Scope)。其中词法做用域更常见,被大多数语言采用,包括JavaScript。

词法做用域简单来讲就是代码在编写过程当中体现出来的做用范围. 代码一旦写好, 不用执行, 做用范围就已经肯定好了. 这个就是所谓词法做用域.

在 js 中词法做用域规则:

  • 函数容许访问函数外的数据.

  • 整个代码结构中只有函数能够限定做用域.

  • 做用域规则首先使用提高规则分析

  • 若是当前做用规则中有名字了, 就不考虑外面的名字

变量搜索原则

在代码的运行过程当中, 若是访问某一个变量,那么:

  1. 首先在当前链上找

    • 若是有,则中止查找

    • 若是没有, 在 n-1 级( 假定当前做用域为第n级 )上找( 在函数内部容许访问定义在函数外部的变量 )

  2. 如此往复, 直到 0 级链

    • 若是找到, 则结束寻找, 直接得到该链上变量的数据

    • 若是尚未 抛出异常。

// 第一题
var x = 10
function fn() {
  console.log(x)
}
function show(f) {
  var x = 20
  f()
}
show(fn) // ? 输出10  js中的是词法做用域。那么从函数声明的时候开始一级一级往父级做用域查找,父级做用域中存在x变量。
// 第二题
var fn = function () {
  console.log(fn)
}
fn()   // ? 输出的是  function(){console.log(fn)}  当前做用域没有fn变量,因此向父级做用域寻找。

函数的四种调用模式

  1. 函数模式

特征:就是一个简单的函数调用,函数名前面没有任何的引导内容

this在函数模式中的含义: this在函数中表示全局对象,在浏览器中是window对象

  1. 方法模式

特征: 方法必定是依附于一个对象, 将函数赋值给对象的一个属性, 那么就成为了方法.

this在方法模式调用中的含义:表示函数所依附的这个对象

  1. 构造器调用模式

特征:使用 new 关键字, 来引导构造函数.

因为构造函数只是给 this 添加成员. 没有作其余事情. 而方法也能够完成这个操做, 就 this 而言, 构造函数与方法没有本质区别.

构造函数中发this与方法中同样, 表示对象, 可是构造函数中的对象是刚刚建立出来的对象

ps:补充关于构造函数中return关键字的补充说明

    • 构造函数中不须要return, 就会默认的return this

    • 若是手动的添加return, 就至关于 return this

    • 若是手动的添加return 基本类型; 无效, 仍是保留原来 返回this

    • 若是手动添加return null; 或return undefiend, 无效

    • 若是手动添加return 对象类型; 那么原来建立的this就会被丢掉, 返回的是 return后面的对象

    1. 上下文调用模式

    特征:上下文(Context),就是函数调用所处的环境。上下文调用,也就是自定义设置this的含义。

    常见的就是经过callapplybind调用

    var obj = {
      fn1: function () {
        console.log(this.fn1) // ? 
        console.log(fn1) // ? 
      }
    }
    obj.fn1()
    /*
    obj.fn1()这个调用模式是方法调用模式,函数内的this指向的obj,因此 this.fn1 === obj.fn1 即打印出来的是obj.fn1的函数体
    
    console.log(fn1)  会向上父级做用域查找变量fn1所表明的值,因为父级做用域并无这个变量,因此会报错 
    Uncaught ReferenceError: fn1 is not defined
    */

    变量提高

    在js代码的预解析阶段,系统会将全部的变量声明以及函数声明提高到其所在的做用域的最顶上,这个过程就是变量提高

    变量提高的特殊状况

    1. 函数同名
      所有提高,后面的会覆盖前面的

    2. 函数和变量同名
      只提高函数,忽略掉变量的声明

    3. 变量的提高是分做用域的

    4. 变量的提高是分段(script标签)
      当前script标签中的函数和变量声明不会被提高到上一个标签中,只会提高到当前标签中

    5. 条件式函数声明(在条件语句中声明的函数)
      将其当作函数表达式来处理便可

    只会提高函数名,不会提高函数体!

    1. 函数形参在变量提高中表现
      函数的形参的声明以及赋值过程优先于变量提高,而且不参与变量提高

    原型链

    • 原型链

    每个对象都有原型属性,那么对象的原型属性也会有原型属性,因此这样就造成了一个链式结构,咱们称之为原型链。

    • 属性搜索原则

    所谓的属性搜索原则,也就是属性的查找顺序,在访问对象的成员的时候,会遵循以下的原则:

    1. 首先在当前对象中查找,若是找到,中止查找,直接使用,若是没有找到,继续下一步

    2. 在该对象的原型中查找,若是找到,中止查找,直接使用,若是没有找到,继续下一步

    3. 在该对象的原型的原型中查找,若是找到,中止查找,直接使用,若是没有找到,继续下一步。

    4. 继续往上查找,直到查找到Object.prototype尚未, 那么是属性就返回 undefined,是方法,就报错xxx is not a function

    // 综合题
    function Person() {
      getAge = function () {
        console.log(10)
      }
      return this
    }
    
    Person.getAge = function () {
      console.log(20)
    }
    
    Person.prototype.getAge = function () {
      console.log(30)
    }
    
    var getAge = function () {
      console.log(40)
    }
    
    function getAge() {
      console.log(50)
    }
    
    
    Person.getAge() // ?  20   这个很好分析,直接调用Person对象(函数也是对象)的getAge方法
    
    
    getAge() // ?  40    有人会问为何这个输出的是40 而不是50 ? 这题考察了函数与变量重名的问题,执行顺序能够看作是 var getAge --> function getAge  -->  getAge = XXX  ,先是变量提高而后再是赋值操做。
    
    
    Person().getAge() // ?  10  
    /*
    考点1:this指向  Person() 这种调用模式称为函数调用,this指向window
    考点2:变量的搜索原则,调用Person()的时候会将变量getAge从新赋予新值,可是这个getAge当前做用域没有,因此向上父级做用域查找,父级做用域是咱们常说的全局做用域,由于Person()的返回值是this--> 即window,因此window.getAge()就会是执行在Person函数内重新赋值的getAge()函数,即输出 10
    */
    
    getAge() // ?  10   ---->  同上
    
    
    new Person.getAge() // ? 20   
    /*
    考点1:运算优先级,这里稍微提一嘴,成员访问(xx.xxx)是比new的运算优先级高的,具体参照mdn  https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Operator_Precedence
    考点2:new 关键字会更改this指向,可是这道题目没有关于this的问题。
    */
    
    new Person().getAge() // ? 30
    /*
    考点1:new 关键字通常作了如下三个事情:
            1. 建立一个空对象
            2. 调用构造函数,而且将构造函数中的this赋值为new出来的对象
            3. 默认的返回刚才建立好的对象
    考点2:属性搜索原则 new出来那个Person对象自己是没有getAge这个方法,因此去Person构造函数的原型上查找。
    */

    后话

    以上只是我我的一些很浅的见解,若有错误欢迎指出交流,但愿能共同进步。

    相关文章
    相关标签/搜索