一、作用域
作用域就是变量和函数可以访问的范围,即作用域控制着变量和函数的可见性和生命周期。
1、全局作用域
任何地方都能访问到的对象拥有全局作用域。
1.1 不在任何函数内定义的变量拥有全局作用域。
1.2 未定义直接赋值的变量自动声明为拥有全局作用域。
1.3 window对象的内置属性拥有全局作用域。比如window.name。
2、局部作用域
局部作用域一般只在固定的代码片段内可访问到,最常见的例如函数内部,所以在一些地方会把这种作用域称为函数作用域。
3、块级作用域
ES6的块级作用域,ES5只有全局和局部作用域。ES6引入了块级作用域,明确允许在块级作用域中声明函数,let和const都涉及块级作用域。块级作用域指在if、switch语句、循环语句等语句块中定义变量,这意味着变量不能再语句块之外被访问。
① var不支持块级作用域
在if等语句块中,定义的变量从属于该块所在的作用域,与函数不同,它们不会创建新的作用域。
② let和const
为了解决块级作用域,ES6 引入了let 和 const关键字,可以声明一个块级作用域的变量。
4、上下文 VS 作用域
① 首先上下文和作用域是两个不同的概念。
② 每个函数调用都有与之相关的作用域和上下文,从根本来说,作用域是基于函数,而上下文是基于对象。
③ 作用域是和每次函数调用时变量的访问有关,并且每次调用都是独立的;上下文是关键字this的值,是调用当前可执行代码的对象的引用。
④ 执行上下文在运行时确定,随时可能改变;作用域在定义时就确定,并且不会改变。
5、「this」 上下文
this的值是通过当前执行上下文中保存的作用域来获取的。
① 上下文通常是取决于一个函数如何被调用,当函数作为对象的方法被调用时,this指向调用方法的对象。
var obj = {
test: function () {
console.log(this == object);
}
};
object.test(); // true
② 当调用一个函数时,通过new操作符创建一个对象的实例,以这种方式调用时,this指向新创建的实例对象。
function test () {
console.log(this);
}
test(); // window
new test(); // test
③ 当调用一个未绑定函数,this默认指向全局上下文或者浏览器中的window对象,然而如果函数在严格模式下被执行,this默认指向undefined。
6、执行上下文
① 当函数执行时,会创建一个称为执行上下文的内部对象(可理解为作用域),一个执行上下文定义了一个函数执行时的环境。(也就是说,执行上下文是由js引擎自动创建的对象, 包含对应作用域中的所有变量属性)。
② 函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会创建多个执行上下文。
③ 当JavaScript文件被浏览器载入后,默认最先进入的是一个全局的执行上下文。当在全局上下文中调用执行一个函数时,程序流就进入该被调用函数内,此时引擎就会为该函数创建一个新的执行上下文,并且将其压入到执行栈顶部(作用域链)。浏览器总是执行位于执行栈顶部的当前执行上下文,一旦执行完毕,该执行上下文就会从执行栈顶部弹出,并且控制权将进入其下的执行上下文。这样,执行栈中的执行上下文就会被依次执行并且弹出,直到回到全局的执行上下文。执行栈用于存储代码执行期间创建的所有上下文,具有LIFO(Last In First Out后进先出,也就是先进后出)的特性。
function f1() {
f2();
console.log(1);
};
function f2() {
f3();
console.log(2);
};
function f3() {
console.log(3);
};
f1();//3 2 1
二、作用域链
当声明一个函数时,局部作用域一级一级向上包起来,就是作用域链。
- 当执行函数时,总是先从函数内部寻找局部变量。
- 如果内部找不到,则会向创建函数的作用域寻找,依次向上。
function first () {
second();
function second () {
third();
function third () {
fourth();
function fourth () {
// do sth...
}
}
}
}
first();
a. 运行示例代码将会导致嵌套的函数被从上到下执行,一直到 fourth 函数,此时作用域链从上到下为: fourth, third, second, first, global。
b. fourth 函数能够访问全局变量和任何定义在first,second和third函数中的变量(和访问自己的变量一样)。
c. 一旦fourth函数执行完成,其就会从作用域链顶部移除,并且执行权会返回到third函数。这个过程一直持续到所有代码完成执行。
在JavaScript中,在函数里面定义的变量,可以在函数里面被访问,但是在函数外面无法被访问。在JavaScript中使用变量,JavaScript解释器首先会在当前作用域中搜索是否有该变量的定义,如有,就使用这个变量;如没有,就到父级作用域中查找该变量,由于变量提升,在实际开发的时候,推荐将变量写在最开始的地方。
三、原型
JavaScript中说一切皆是对象,当然这句话也不严谨,比如null和undefined就不是对象,除了这俩完全可以说JavaScript中一切皆对象。而JavaScript对象有一个叫做「原型」的公共属性,属性名是proto,这个原型属性是对另一个对象的引用,通过这个原型属性我们就可以访问另个对象的属性和方法。
无论何时,只要创建了一个函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向该函数的原型对象,所有原型对象都会获得一个constructor属性,这个属性包含一个指向prototype属性所在函数的指针。例如:
// 创建构造函数
function Person (name, age) {
this.name = name;
this.page = page;
this.sayName = function () {
console.log(this.name);
}
}
// 创建实例
const person1 = new Person('gali', 8);
const person2 = new Person('pig', 29);
如图,person1和person2都是构造函数Person()的实例对象,Person.prototype指向了Person函数的原型对象,而Person.prototype.constructor又指向Person函数。Person的每一个实例对象,都含有一个内部属性proto,该属性指向Person.prototype。所以
console.log(Person.prototype.constructor === Person); // true
console.log(person1.__proto__ === Person.prototype); // true
console.log(person2.__proto__ === Person.prototype); // true
使用hasOwnProperty()方法可以检测一个属性是存在于实例还是存在于原型中,这个方法只有给定属性存在于对象实例中才会返回true。
function Person(){
}
Person.prototype.name = 'zzx';
Person.prototype.age = 22;
Person.prototype.job = 'Programmer';
Person.prototype.sayName = function(){
console.log(this.name);
}
var person1 = new Person();
console.log(person1.hasOwnProperty('name')); //false
person1.name = 'yzy';
console.log(person1.hasOwnProperty('name')); //true
delete person1.name;
console.log(person1.hasOwnProperty('name')); //false
四、原型链
当访问对象上的属性时,会首先查找该对象上是否含有该属性,而当这个对象上没有这个属性时就会访问proto 索引看看它的原型对象上面有没有,如果还没有就继续沿着proto原型向上寻找,直到找到为止。这就是原型链。所有对象的原型尽头是Object.prototype,而Object.prototype的原型指向null。
我们日常开发中用到的绝大多数对象的proto基本不会直接指向Object.prototype,基本都是指向另一个对象,比如所有的函数的proto都指向Function.prototype,所有数组的proto都指向Array.prototype。
tim.__proto__ === Man.prototype;
tim.__proto__.__proto__ === Object.prototype;
tim.__proto__.__proto__.__proto__ === null;
var func = function () {};
func.__proto__ == Function.prototype; // true
Function.prototype.__proto__ === Object.prototype; // true
// JS中最乱伦的东西出现了, Function 是自己的老子
Function.__proto__ === Function.prototype; // true
Object.__proto__ === Function.prototype; // true
总结
- 所有的函数都继承自Function.prototype,Function、Object 都是函数,所以继承自Function.prototype。
- 所有的对象都直接或间接继承自Object.prototype,Function.prototype.proto === Object.prototype函数也是对象,所以函数最终继承自Object.prototype。
- Object.prototype 继承自 null,万剑归宗。
- proto和constructor属性 是对象所独有的;prototype属性数函数所独有的,因为函数也是一种对象,所以函数也拥有proto和constructor属性。
- prototype属性的作用就是让该函数所实例化的对象们都可以找到公用的属性和方法,即f1.proto === Foo.prototype
- constructor属性的含义就是指向该对象的构造函数,所有函数(此时看成对象了)最终的构造函数都指向Function。
五、常考示例
5.1 题1
var F=function(){};
Object.prototype.a=function(){
console.log('a()')
};
Function.prototype.b=function(){
console.log('b()')
}
var f = new F();
f.a() // a()
f.b() // f.b is not a function
F.a() // a()
F.b() // b()
如上,f 实例对象的原型指向 F的prototype属性;F的原型指向Function的prototype属性;Function.prototype的原型指向了Object.prototype;Object.prototype的原型指向了Null。
f.__proto__ === F.prototype
F.__proto__ === Function.prototype
F.prototype.__proto__ === Object.prototype
Function.prototype.__proto__ === Object.prototype
f 实例对象本身没有a属性,所以会在F.prototype上查找,原型上如果找到a属性,就继承了原型的a属性;若原型上没有a属性,会继续在原型上的原型上查找,直到为null对象,若还是没有该属性,则返回undefined。
综上,构造函数只是提供创建对象的模板,而不是原型对象。所以f.prototype指向的不是构造函数,而是构造函数的原型。
六、作用域链与原型链的区别
- 作用域链:当访问一个变量时,JS解释器首先会在当前作用域查找标识符,如果没有找到就到父级作用域去找,作用域链顶端是全局对象window,如果window都没有这个变量就报错。
- 原型链:当在对象上访问某属性时,首先会在这个对象本身查找该属性,如果没有就顺着原型链向上找,原型链顶端是null,如果全程都没找到则返一个undefined,而不是报错。