作用域和原型
什么是作用域?
它是一套规则,这套规则用来管理引擎如何在当前作用域及嵌套的子作用域中根据标识符名称进行变量的查找。
先介绍一下RHS和LHS
- 这是引擎的两种查找类型
- “R”和“L”分别代表赋值操作的右侧和左侧
- 案例
1 | function foo(a){//对a进行LHS引用2 |
- 当变量还没有声明的时候(在任何作用域都找不到该变量),这两种查询的行为是不一样的:LHS查询会在全局作用域中自己创建一个变量。RHS查询会抛出一个ReferenceError错误。
作用域的嵌套
当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。这一就形成了一条作用域链。
- 案例
1
2
3
4
5function foo(a){
console.log(a+b);
}
var b=2;
foo(2);//4 - 将作用域链比喻成一个建筑
词法作用域
- 词法作用域是由你写代码时将变量和块作用域写在哪里来决定的。
- 案例
JavaScript中有两个机制可以“欺骗”词法作用域
eval() 例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function foo(str, a) {
eval( str ); // 欺骗!
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 );
//严格模式
// function foo(str) {
// "use strict";
// eval( str );
// console.log( a ); // ReferenceError: a is not defined
// }
foo( "var a = 2");with() 例子:
1 | function foo(obj) { |
函数作用域
- 在任意代码片段外部添加包装函数,可以将内部的变量和函数定义“隐藏”起来,外部作用域无法访问包装函数内部的任何内容。
- 案例
1 | var a=2; |
- 不足之处:
- 必须声明一个foo函数,这个foo“污染”了所在作用域。
- 必须显示调用才能运行其中的代码
- 改进
1 | var a=2; |
匿名函数
- 优点:
- 书写起来简单快捷
- 缺点:
- 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。
- 如果没有函数名,当函数需要引用自身时只能使用已经过期的arguments.callee引用。(callee是arguments对象的一个属性。它可以用于引用该函数的函数体内当前正在执行的函数。)
- 匿名函数省略了对于代码可读性/可理解性很重要的函数名。
立即执行函数表达式
- IIFE(Immediately Invoked Function Expression):
(function foo(){...})()
- 函数名对IIFE来说不是必须的:
(function(){...}())
- 进阶用法:当作函数调用并传参数进去。
- 例子:
1
块作用域
块作用域将代码在函数中隐藏的信息扩展为在块中隐藏起来。
- 思考
1 | for(var i=0;i<10;i++){ |
- with:用with从对象创建出的作用域仅在with声明中而非外部作用域中有效。
- try/catch:例子:try.html
- let:let关键字可以将变量绑定到所在的任意作用域中。
- 垃圾回收
1 | function process(data){ |
- let循环
1 | for(let i=0;i<10;i++){ |
- const:定义一个该块的常量,不能修改值。例子:
1 | var person = function(name){ |
原型[prototype]
普通对象和函数对象
- JavaScript 中,万物皆对象!但对象也是有区别的。分为普通对象和函数对象。
1 | var o1 = {}; |
- 怎么区分,其实很简单,凡是通过new Function()创建的对象都是函数对象,其他的都是普通对象。f1,f2,归根结底都是通过new Function()的方式进行创建的。Function Object 也都是通过 New Function()创建的。
什么是prototype
?
在 JavaScript 中,每当定义一个对象(函数也是对象)时候,对象中都会包含一些预定义的属性。其中每个函数对象都有一个
prototype
属性,这个属性指向函数的原型对象。
1 | function Person() { |
- 只有函数对象才有
prototype
属性
什么是原型对象?
原型对象,顾名思义,它就是一个普通对象。从现在开始你要牢牢记住原型对象就是
Person.prototype
,如果你还是记不住,那就把它想想成一个字母 A:var A = Person.prototype;
1 | Person.prototype = { |
在默认情况下,所有的原型对象都会自动获得一个
constructor
(构造函数)属性,这个属性(是一个指针)指向prototype
属性所在的函数(Person)
原型对象(Person.prototype)是 构造函数(Person)的一个实例。
那原型对象是用来做什么的呢?举个例子:
1 | var person = function(name){ |
- 从这个例子可以看出,通过给
person.prototype
设置了一个函数对象的属性,那由person
实例(例中:zjh)出来的普通对象就继承了这个属性。所以原型对象的主要作用就是用于继承。 - 具体是怎么实现的继承,就要讲到下面的原型链了。
什么是原型链?
- JS在创建对象(不论是普通对象还是函数对象)的时候,都有一个叫做__proto__的内置属性,用于指向创建它的函数对象的原型对象prototype。例如:
console.log(zjh.__proto__ === person.prototype) //true
- 同样,person.prototype对象也有__proto__属性,它指向创建它的函数对象(Object)的prototype
console.log(person.prototype.__proto__ === Object.prototype) //true
- 继续,Object.prototype对象也有__proto__属性,但它比较特殊,为null
console.log(Object.prototype.__proto__) //null
- 我们把这个有__proto__串起来的直到Object.prototype.__proto__为null的链叫做原型链。
注
ReferenceError同作用域判别失败相关,而TypeError则代表作用域判别成功了,但是对结果的操作是非法或不合理的。
区分函数声明和表达式最简单的方法是看function关键字出现在声明这哦那个的位置(不仅仅是一行代码,而是整个声明中的位置)。如果function是在声明的第一个词,那么就是一个函数声明,否则就是一个函数表达式。
作用域理解基于《你不知道的Javascript 上卷》
原型理解基于文章原型与原型链