文章结构

  • 原型与原型链
    • 原型
    • 原型链
    • 原型链中的 this
  • 作用域与闭包
    • 执行上下文
    • 作用域
    • 作用域链
    • 闭包
  • 面试题

原型与原型链

下面几条必须记住:

  • 所有的引用类型(数组,函数,对象),都具有对象特性,即可以自由扩展属性。
  • 所有的引用类型(数组,函数,对象),都具有proto,其值是一个对象。
  • 所有的函数,都具有 prototype,其值是一个对象。
  • 所有的引用类型(数组,函数,对象),其proto属性都指向它的构造函数的 prototype 属性值。即:obj.__proto__ === Object.prototype
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 要点一:自由扩展属性
var obj = {}; obj.a = 100;
var arr = []; arr.a = 100;
function fn () {}
fn.a = 100;

// 要点二:__proto__
console.log(obj.__proto__);
console.log(arr.__proto__);
console.log(fn.__proto__);

// 要点三:函数有 prototype
console.log(fn.prototype)

// 要点四:引用类型的 __proto__ 属性值指向它的构造函数的 prototype 属性值
console.log(obj.__proto__ === Object.prototype)

原型

当试图找到一个对象的某个属性时,如果该对象没有这个属性,那么就会到它的proto(即它的构造函数的 prototype)去寻找。如果它的proto还是没有则继续在 f.proto.proto(也即 F.prototype.proto)上寻找。如果直到最上层也没有找到,则返回undefined

1
2
3
4
5
6
7
8
9
10
11
// 构造函数
function Foo(name, age) {
this.name = name
}
Foo.prototype.alertName = function () {
alert(this.name)
}
// 创建示例
var f = new Foo('zhangsan')
// 测试
f.alertName()

执行 f.alertName()时,因为该对象没有 alertName()方法,所以就会到它的proto(即 Foo.prototype)上去寻找。此时找到了,调用该原型链上的 alertName()方法。没有找到则继续查找。

原型链

上述找的过程,是一个链式的结构,叫做“原型链”。所有原型链的最上层都是 null(即 Object.prototype.proto === null)。

原型链中的 this

所有从原型链中得到的方法,在 this 执行时,都指向了当前触发这个方法的对象。

作用域与闭包

执行上下文

  • 在一段 javsscript 脚本执行之前,要先解析代码,解析的时候,会创建一个全局执行上下文环境,先把即将执行代码中的变量和函数的声明拿出来存放在这个环境中:变量暂时赋值为 undefined,函数则声明好可以使用。这一步完成之后,才正式开始执行代码。

  • 一个函数在调用时,会创建函数执行上下文环境,相对于全局执行上下文环境,在函数执行上下文环境中,增加了 this,arguments 和函数的参数。

this 在执行时才能确定其值,定义的时候不能确认。

1
2
3
4
5
6
7
8
9
10
11
12
console.log(a)
var a = 100

fn('zhangsan')
function fn(name) {
age = 20
console.log(a, name, age)
var age
}

console.log(b); // Uncaught ReferenceError: b is not defined
b = 100;

上述代码执行过程:

  1. 创建全局执行上下文
  2. 申明变量 a(此时变量 a 赋值为 undefined)、函数 fn
  3. 开始执行代码: console.log(a) // undefinedfn('zhangsan') // 100 zhangsan 20console.log(b) // 报错,因为全局执行环境中没有找到关于b的申明

fn('zhangsan')调用时,其执行过程如下:

  1. 创建一个函数执行上下文
  2. 声明变量 age(此时变量 age 赋值为 undefined)
    1. 沿着作用域链查找变量 a,若在全局作用域中还没有找到,才会报错:a is not defined并停止执行程序
    2. 在 arguments(arguments 是类数组)中找到 name
  3. 开始执行代码

作用域

在 es6 之前,javascript 中没有块级作用域的概念。例如:

1
2
3
4
if (true) {
var name = 'tom'
}
console.log(name) // tom

作用域就是一个独立的地盘,让变量不会泄露、暴露出去。JS 没有块级作用域,只有全局作用域和函数作用域。

jQuery、Zepto 等库的源码,所有的代码都会放在(function(){….})()中。因为放在里面的所有变量,都不会被外泄和暴露,不会污染到外面,不会对其他的库或者 JS 脚本造成影响。这是函数作用域的一个体现。

1
2
3
4
5
6
7
var a = 100
function fn() {
var a = 200
console.log('fn', a)
}
console.log('global', a)
fn()

ES6 中开始加入了块级作用域,使用let定义变量即可。

1
2
3
4
if (true) {
let name = 'tom'
}
console.log(name) // 报错,因为在全局作用域中没有name的声明。

作用域链

执行某个函数作用域中的一段代码时,当一个变量的申明没有在当前作用域中找到,会一层一层向上级查找,这一层一层的关系,就是作用域链。若在全局作用域中还没有找到,才会报错。

1
2
3
4
5
6
7
var a = 100
function fn() {
var b = 200
console.log(a)
console.log(b)
}
fn()

上述代码中,console.log(a)要得到 a 变量,但是在当前的作用域中没有定义 a 变量。像这种在当前作用域中没有定义但在作用域链中定义的变量,就是自由变量

自由变量将在作用域链中寻找,但是依据的是函数定义时候的作用域链,而不是执行时候的。

闭包

闭包主要有两个执行场景:

  • 函数作为返回值返回
  • 函数作为参数传递
1
2
3
4
5
6
7
8
9
function F1() {
var a = 100
return function () {
console.log(a)
}
}
var f1 = F1()
var a = 200
f1() // 100
1
2
3
4
5
6
7
8
9
10
11
12
function F1() {
var a = 100
return function () {
console.log(a)
}
}
function F2(f1) {
var a = 200
console.log(f1())
}
var f1 = F1()
F2(f1) // 100