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

引用变量赋值传递

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

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

    • Boolean

    • Null

    • Undefined

    • Number

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

    • Symbol (ECMAScript 6 新定义)

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

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

引用类型:存储在堆(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:其实这个分析过程是一个连续的过程,光凭一张图有点难展示的很好,能说说明的也不是太详细。

!一些经典面试题分析(上)_第1张图片

作用域与作用域链

引用自《You-Dont-Know-JS》https://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构造函数的原型上查找。
*/

后话

以上只是我个人一些很浅的看法,如有错误欢迎指出交流,希望能共同进步。

你可能感兴趣的:(面试,javascript)