作用域
根据红宝书中描述:一个变量的作用域(scope)是程序源代码中定义这个变量的区域。全局变量拥有全局作用域,在JavaScript代码中的任何地方都是有定义的。然而在函数内声明的变量只在函数体内有定义,它们是局部变量,作用域是局部性的。
JS中作用域类型
- 函数作用域
函数作用域是js中最常见的作用域,函数作用域给我们最直观的体会就是,内部函数可以调用外部函数中的变量。一层层的函数也就形成了嵌套的作用域即作用域链
var a = 10; //定义在全局作用域
function foo(){
a = 20;
var b = 30; //函数作用域
}
foo();
foo函数执行到a = 20时,会在当前函数作用域查找变量a,找不到则向上查找上一级作用域(此处是全局作用域),'如果一直找不到a,会在最外层定义一个a,这样a就成为了一个全局变量也就是之前讲到的未经过声明直接赋值的变量会转变为一个全局变量,也是我们在开发中不提倡的未经声明直接赋值的写法'
- 块作用域
ES6之前js中没有块级作用域,大括号'{}'限定不了作用域,如下
例1:
if(true){
var a = 20;
}
console.log(a); //20
例2:
for(var i = 0;i < 10;i++){
...
}
console.log(i); //10
上述两个例子,'{}'外仍可以访问到内部的变量a,i。在ES6中新增了let,使用let声明变量会将作用域限定在块级,因此将上述var换成let,在外层就获取不到内部的变量a,i。
作用域的一些应用
最小特权原则
这个原则指在软件设计中,应该最小限度的暴露必要内容,而将其他内容隐藏起来,比如某个模块或对象的API设计。也就是尽可能多的把部分代码私有化
函数可以产生自己的作用域,因此可以采用函数封装(函数表达式和函数声明都可以)的方法实现这一原则
(function foo(){
var a = 10;
console.log(a); //10
}())
console.log(a); //'error a is not defined'
顺便提一下如何区分函数声明和函数表达式:
如果function是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。函数声明和函数表达式最重要的区别是它们的名称标识符将会绑定在何处。函数表达式可以是匿名的,而函数声明不可以省略函数名(在js的语法中这是非法的)
立即执行函数表达式(IIFE)
函数表达式后面加上一个括号会立即执行,(function(){...}())和(function(){...})()写法都可以。
IIFE一个非常普遍的用法是把它们当作函数调用并传递参数进去
var arr = [];
for(var i = 0;i < 5;i++) {
(function(a){
arr[i] = function(){
console.log(a);
}
})(i)//将i当成参数传递给匿名函数
}
闭包
《你不知道的JavaScript》中描述:
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行
function f1(){
var outer = 'outer';
function f2(){
console.log(outer);
}
return f2;
}
var f3 = f1();
f3();
上述例子清晰的展示了闭包,正常来讲,当f1函数执行完,其作用域被销毁,然后垃圾回收器会释放内存空间。但这里闭包却将f1的作用域保留下来,f2依然保存着对f1作用域的引用,所以这里会打印outer变量的值
再看一个比较经典的闭包例子:
for(var i = 0;i < 10;i++) {
setTimeout(function(){
console.log(i)
}, 1000)
}
上述代码,我们原本希望其输出1-9,但实际上它输出的确实9次10。定时器中的回调执行的时候,for循环已经结束,这时i变量是10。而i变量是在全局作用域中,且定时器函数也是在全局作用域中执行,所以输出的是9次10。
我们可以利用上面立即执行函数创建私有作用域来解决这个问题
for(var i = 0;i < 10;i++) {
(function(j){
setTimeout(function(){
console.log(j)
},1000)
}(i))
}
或者使用let声明代替var创建块级作用域
for(let i = 0;i < 10;i++) {
setTimeout(function(){
console.log(i)
}, 1000)
}
原型
基础概念不做赘述,只针对难点进行梳理
prototype
prototype是一个属性,基本上每个函数都有这个属性。当我们声明一个函数时,这个函数就会有个prototype属性
function Person(){}
Person.prototype指向一个原型对象,使用原型对象可以让所有实例共享它包含的属性和方法而不必在构造函数中定义对象实例的信息
function Person(name){
this.name = name;
}
Person.prototype.sayHi = function(){
console.log(this.name);
}
var p1 = new Person('p1');
var p2 = new Person('p2');
//p1和p2可以共享原型对象上的sayHi方法
原型对象上有个属性constructor,对于上述例子原型对象的constructor属性就指向Person函数
通过构造函数创建的实例内部会包含一个指针(proto),该指针指向构造函数的原型对象。
下图清晰的展示了原型对象、实例、构造函数之间的关系

原型链
如果原型对象指向另一个实例对象,会是什么情况呢?此时原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含着一个指向另一个构造函数的指针。如果另一个原型又是另一个实例对象,那么层层递进,就构成了实例与原型的链条,这就是我们所说的原型链的概念。
概念比较拗口,通过代码实现加深理解,代码如下:
function Parent(){
this.name = 'parent'
}
Parent.prototype.getName = function(){
return this.name;
}
function Son(){
this.name = 'son'
}
//Son原型对象继承Parent的实例
Son.prototype = new Parent();
var son = new Son();
console.log(son.getName()); //son
以上代码定义了两个类型:Parent和Son。Parent原型对象上定义getName方法,Parent实例继承了getName方法。将Son原型指向Parent实例。这样Son的实例就继承了Son原型对象的getName方法
引申 instanceof
instanceof用来函数是否在某个对象的原型链上
function F(){}
var f = new F();
f instanceof F //true
//代码模拟实现instanceof
function instanceOf(left, right) {
left = left.__proto__;
where(true) {
if(left === null) {
return false;
}
if(left === right.prototype) {
return true;
}
left = left.__proto__;
}
}
最后,用一张全面的图展示原型 实例 构造函数之间的关系

扩展:js实现继承的几种方式
- 借用构造函数
function Parent(name){
this.name = name
}
function Son(name) {
Parent.call(this,name);
}
- 组合继承
使用原型链实现对原型的继承,通过借用构造函数实现实例属性的继承
function Parent(name){
this.name = name;
}
Father.prototype.getName = function(){
console.log(this.name)
}
function Son(name, age) {
Father.call(this,name);//借用构造函数
this.age = age;
}
Son.prototype = new Father();//原型链
- 原型继承
在函数内部,先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例.
function object(o) {
function F(){}
F.prototype = o;
return new F();
}
- 寄生式继承
function createObj(o) {
var clone = object(o);
clone.sayHi = function(){
console.log('hi');
}
return clone;
}
- 寄生组合式继承
function extend(son, parent) {
var prototype = object(parent.prototype);
prototype.constructor = son;
son.prototype = prototype;
}