JavaScript高级内容笔记:原型链、继承、执行上下文、作用域链、闭包

最近在系统的学习JS深层次内容,并稍微整理了一下,作为备忘和后期复习,这里分享给大家,希望对大家有所帮助。如有错误请留言指正,tks。

了解这些问题,我先一步步来看,先从稍微浅显内容说起,然后引出这些概念。

本文只用实例验证结果,并做简要说明,给大家增加些印象,因为单独一项拿出来都需要大篇幅讲解。

1.值类型 & 引用类型

复制代码
function show(x) {
console.log(typeof(x)); // undefined
console.log(typeof(10)); // number
console.log(typeof('abc')); // string
console.log(typeof(true)); // boolean
 
console.log(typeof(function () { })); //function
 
console.log(typeof([1, 'a', true])); //object
console.log(typeof ({ a: 10, b: 20 })); //object
console.log(typeof (null)); //object
console.log(typeof (new Number(10))); //object
}
show();
复制代码

undefined, number, string, boolean 属于简单的值类型,不是对象;
函数、数组、对象、null、new Number(10)都是对象;
判断一个变量是不是对象非常简单。值类型的类型判断用typeof,引用类型的类型判断用instanceof。

2. 语法糖(糖衣语法)

var obj = { a: 10, b: 20 };
var arr = [5, 'x', true];

 语法糖其实是一种“快捷方式”,上面的代码是一种缩写,完整的写法是:

复制代码
var obj = new Object();
obj.a=10;
obj.b=20;
 
var arr= new Array();
arr[0]=5;
arr[1]='x';
arr[2]=true;
复制代码

  

3. 对象都是通过函数创建的,而函数又是一种对象

方法一:

复制代码
var fn = function() {
    alert(100);
};
fn.a = 10;
fn.b = function() {
    alert(123);
};
fn.c = {
    name: "典橙贸易",
    year: 2016
};
复制代码

fn是函数,a/b/c是fn创建的对象。

方法二:

复制代码
var obj = { a: 10, b: 20 };
var arr = [5, 'x', true];
 
其本质是:
var obj = new Object();
obj.a = 10;
obj.b = 20;
 
var arr = new Array();
arr[0] = 5;
arr[1] = 'x';
arr[2] = true;
 
而
console.log(typeof (Object)); // function
console.log(typeof (Array)); // function
 
复制代码

4. 构造函数

只有构造函数才有prototype属性。
通常我们自定义的函数都属于构造函数,所以都有此属性。
JS运行时环境内置的函数有些不是构造函数,比如alert和Math.sqrt等,就没有此属性。
注:构造函数是指有一个内部属性[[Construct]],通过new可以创建对象的那些函数。

5. prototype 与 __proto__

每个函数function都有一个prototype,即原型。
每个对象都有一个__proto__,即隐式原型。
只有构造函数才有prototype属性。
对象的__proto__指向的是创建它的函数的prototype。
函数的prototype都是被Object创建的,每个自定义函数创建之初,都会有一个prototype(它是如何被创建的,不得而知!)。
javascript_object_layout
例如:

复制代码
function Foo(){};
var foo = new Foo();
Foo.__proto__ === Function.prototype // 任何函数都是由Function创建的,所以任何函数的__proto__都指向Function的prototype
foo.__proto__ === Foo.prototype // 对象的__proto__指向的是创建它的函数的prototype
Foo.prototype.__proto__ === Object.prototype // 如果是自定义的函数,它的prototype.__proto__ 指向 Object.prototype,因为自定义函数的prototype本质上就是和语法糖 var obj = {} 是一样的,都是被Object创建。
Object.prototype.__proto__ === null // Object.prototype也是个对象,它的__proto__是null(这是个特例,切记切记)
 
foo.prototype == undefined //只有函数才有prototype属性,foo是对象
Object.prototype === (new Object()).__proto__ // 如上所述,Object本身是一个函数,(new Object())是对象
Function.prototype.__proto__ === Object.prototype // Function是一个函数,它的prototype是被Object创建,所以它的Prototype.__proto__指向创建Object.prototype
Function.__proto__ === Function.prototype //Function也是一个函数,函数是一种对象,也有__proto__属性。既然是函数,那么它一定是被Function创建。所以——Function是被自身创建的
Object.__proto__ === Function.prototype //Object是个函数,也是对象,它的__proto__指向的是创建它的函数的prototype
复制代码

参考:

http://www.cnblogs.com/wangfupeng1988/p/3978131.html
http://www.cnblogs.com/wangfupeng1988/p/3979290.html

6. instanceof原理

Instanceof运算符的第一个变量是一个对象,暂时称为A;第二个变量一般是一个函数,暂时称为B。
Instanceof的判断规则是:
  沿着A的__proto__这条线来找,如果B的prototype在这条线上,那么就返回true,否则返回false。

如:

function Foo(){}
var f1 = new Foo();
console.log(f1 instanceof Foo);    // true
console.log(f1 instanceof Object); // true

分析方法:
  f1的__proto__路线A:

(1) f1.__proto__ === Foo.prototype
(2) Foo.prototype.__proto__ === Object.prototype
(3) Object.prototype.__proto__ === null //到终点

结论:
  f1 instanceof Foo :A线路(1)中出现了Foo.prototype
  f1 instanceof Object :A线路(2)中出现了Object.prototype


参考:

http://www.cnblogs.com/wangfupeng1988/p/3979533.html

7. 原型链

访问一个对象的属性时,先在基本属性中查找,如果没有,再沿着__proto__这条链向上找,这就是原型链。
例如:

复制代码
function Foo(){}
var f1 = new Foo();
f1.a = 2;
f1.b = 3;
Foo.prototype.b = 6;
Foo.prototype.c = 7;
 
console.log(f1.a); // 2,a是f1的基本属性,直接访问没问题
console.log(f1.c); // 7,f1的基本属性中没有c,沿着原型链向上,f1.__proto__是Foo.prototype,而Foo.prototype有c,可以访问。
console.log(f1.b); // 3,b是f1的基本属性,直接访问没问题。可Foo.prototype还有一个b,但是不会显示,这种情况被称为“属性遮蔽”。
复制代码

判断属性是基本属性(也叫实例属性)还是原型链上的属性(也叫原型属性)使用hasOwnProperty,一般在for…in..循环中。for…in…会输出实例属性和原型属性。
属性列表参考实例属性 vs 原型的属性 vs 静态属性
如:

复制代码
var item;
for(item in f1){
    console.log(item); // a,b,c
}
 
for(item in f1){
    f1.hasOwnProperty(item) && console.log(item); // a,b
}
复制代码

8. 继承

在原型链的基础上,很容易理解继承。例如在原型链的实例中,f1.hasOwnProperty就是继承自Object.prototype。
过程如下:

  • f1基本属性中没有hasOwnProperty,沿原型链向上找:f1.__proto__是Foo.prototype;
  • Foo.prototype中也没有hasOwnProperty,继续沿原型链向上找:Foo.prototype.__proto__是Object.prototype;
  • Object.prototype中有hasOwnProperty,查找结束,允许访问hasOwnProperty。

由于所有的对象的原型链都会找到Object.prototype,因此所有的对象都会有Object.prototype的方法。这就是所谓的“继承”。
另外,每个函数都有的call,apply方法,和length,arguments,caller等属性也都是继承下来的。

9. 执行上下文/执行上下文环境

定义1:js在执行一段代码之前,会声明提前,赋值或赋初始值(即undefined)。所以同一代码段中,可以先访问,再定义,这就是“执行上下文”。
定义2:在执行代码之前,把将要用到的所有的变量都事先拿出来,有的直接赋值了,有的先用undefined占个空。

分4种情况:

第1种情况:变量、函数表达式——只提前声明,不提前赋值,默认赋值为undefined;

console.log(a); // undefined
var a=10;
console.log(fn1); // undefined
var fn1 = function(){};

第2种情况:this关键字——提前声明并赋值

console.log(this); // Window {external: Object, chrome: Object, document: document, __ctrip: Object, __SERVERDATE__: Object…}

第3种情况:函数声明——提前声明并赋值

console.log(fn2); // function fn2(){}
function fn2(){};

第4种情况:函数内部会创建自己独有的上下文环境

函数每被调用一次,都会产生一个新的执行上下文环境。因为不同的调用可能就会有不同的参数。
另外,每个函数在定义的时候(不是调用的时候),就已经确定了函数体内部自由变量的作用域。
如:

复制代码
var a = 10;
 
function fn() {
console.log(a); // a是自由变量,函数创建时,就确定了a要取值的作用域
}
 
function bar(f) {
var a = 20;
f(); // 10,到创建fn的地方找变量a的作用域,而不是调用它的当前作用域。
}
bar(fn);
复制代码

10. 执行上下文

如【执行上下文/执行上下文环境】所述,js代码中,会有无数的函数调用和嵌套调用,会产生无数的上下文环境,这么多上下文环境如何管理,以及如何销毁而释放内存呢?这就需要【执行上下文栈】的参与了。

执行上下文栈:执行全局代码时,会产生一个全局上下文环境,并将该上下文环境压入栈,每次调用函数都又会产生执行上下文环境,并将该函数上下文环境也压入栈。当函数调用完成时,该函数上下文环境出栈,并消除该函数上下文环境以及其中的数据,再重新回到全局上下文环境。处于活动状态的执行上下文环境只有一个。其实这是一个压栈出栈的过程。
执行上下文栈
如:

复制代码
var a = 10, // 1. 进入全局上下文环境
fn,
bar = function(x) {
    var b = 5;
    fn(x + b); // 3. 进入fn函数上下文环境,并入栈,设为活动状态。该函数执行完毕后,bar函数上下文出栈,并及时销毁,释放内存
};
 
fn = function(y) {
    var c = 5;
    console.log(y + c);
}
 
bar(10); // 2. 进入bar函数上下文环境,并入栈,设为活动状态。该函数执行完毕后,bar函数上下文出栈,并及时销毁,释放内存
 
复制代码

参考:http://www.cnblogs.com/wangfupeng1988/p/3989357.html

11. this

情况1:构造函数,this就代表new出来的对象。

function Foo(){
    this.name = '典橙贸易';
    this.year = 2016;
    console.log(this); // Foo {name: "典橙贸易", year: 2016}
}
var f1 = new Foo(); // 所谓构造函数就是用来new对象的函数。

另外,如果在原型链中使用this(Foo.prototype中使用this),this仍然代表new出来的对象。

复制代码
function Foo(){
    this.name = '典橙贸易2016';
}
Foo.prototype.getName = function(){
    console.log(this); // Foo {name: "典橙贸易2016"}
}
var f1 = new Foo();
f1.getName();
复制代码

情况2:函数作为对象的一个属性,并且被对象直接调用时,this指向该对象。

复制代码
var obj = {
    name:'典橙贸易',
    fn:function(){
        console.log(this); // Object {name: "典橙贸易"}
        console.log(this.name); // 典橙贸易
    }
}
obj.fn(); // 直接调用
 
// 如果不是直接调用,this就指向window
var obj = {
    name: '典橙贸易',
    fn: function() {
        console.log(this); // Window {external: Object, chrome: Object, document: document, __ctrip: Object, __SERVERDATE__: Object…}
        console.log(this.name); // undefined
    }
}
var fn1 = obj.fn;
fn1();
复制代码

情况3:函数用call或者apply调用,this就是传入的对象的值

复制代码
var obj ={
    name:'典橙贸易'
}
var fn = function(){
    console.log(this); // Object {name: "典橙贸易"}
    console.log(this.name); // 典橙贸易
}
fn.call(obj);
// 或者
// fn.apply(obj);
复制代码

情况4:全局 & 调用普通函数,this是window

全局:

复制代码
console.log(this===window); // true
 
调用普通函数:
var name ="典橙贸易";
var fn = function() {
    console.log(this); // Window {external: Object, chrome: Object,     document: document, __ctrip: Object, __SERVERDATE__: Object…}
    console.log(this.name); // 典橙贸易
}
fn();
复制代码

注意:

复制代码
var obj = {
name:'典橙贸易',
fn: function(){
 
    function f() {
        console.log(this); // Window {external: Object, chrome: Object,     document: document, __ctrip: Object, __SERVERDATE__: Object…}
        console.log(this.name); // undefined
    }
    f(); // 函数f虽然是在obj.fn内部定义的,但是它仍然是一个普通的函数,this仍然指向window。
 
    }
}
obj.fn();
复制代码

12. 作用域

知识点1:javascript中没有块级作用域。

var i = 2;
if(i>1){
    var name ='典橙贸易';
}
console.log(name); // 典橙贸易
for循环的{}也是类似。

知识点2:javascript除了全局作用域之外,只有函数可以创建自己的作用域,称为函数作用域。

如经典的问题:

复制代码
<ul id = "list">
    <li> we </li>
    <li> sdf </li>
    <li> cx </li>
    <li> h </li>
    <li> z </li>
</ul>
复制代码
复制代码
var list = document.getElementById('list');
var e = list.getElementsByTagName('li');
var i = 0;

错误写法:
for (; i < e.length; i++) {
  e[i].onclick = function() {
    console.log(i);
  }
}

正确写法:
for (; i < e.length; i++) {
  (function(a) {
    e[i].onclick = function() {
      console.log(a);
    }
  })(i);
}
等效于:
for (; i < 5; i++) {
  var Fn = function(a) {
    e[i].onclick = function() {
      console.log(a);
    }
  }
  Fn(i);
}
复制代码

错误的原因:
  e[i].onclick 每次循环,只是赋值给onclick,而for循环是没有块级作用域的,所以i的值会不断累加,直到最大值5,故每次循环都会输出5。
正确的原因:
  由于函数可以创建自己的作用域,而且各个作用域间不会相互影响,所以每次循环Fn(i)都会创建一个特有的函数作用域提供给相应的onclick,而每个作用于中的a变量也不会相互影响。

知识点3:作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

复制代码
{
    var a = 1,
        b = 2;

    function fn1() {
        var a = 100,
            b = 200;

        function fn2() {
            var a = 1000,
                b = 2000;
        }
    }
} 
复制代码

三个作用域下都声明了“a和b”变量,但是他们不会有冲突。各自的作用域下,用各自的“a和b”。

13. 自由变量及其取值规则

在A作用域中使用的变量x,却没有在A作用域中声明(即在其他作用域中声明的),对于A作用域来说,x就是一个自由变量。
如:

var x=1;
function fn(){
    var b=2;
    console.log(x+b); // 这里的x就是自由变量
}

自由变量取值规则:要到创建 自由变量所在函数 的那个作用域中取值–是【创建】,而不是【调用】,也不是【父作用域】。

如:

复制代码
var x = 10;
function fn() {
    console.log(x); // fn创建了自由的函数作用域,所以无论什么地方调用它,都会输出10
}
function show(f) {
    var x = 20;
    (function() {
        console.log(x); // 要到x所在的匿名函数的作用域中找x,故输出20
        f(); // 要到创建fn函数的作用域中找x,故这里输出10,而不是20
    })();
}
show(fn);
复制代码

  

14. 作用域链

在自由变量的基础上理解作用域链更加容易。
在作用域Fn中使用一个变量A,如果Fn中没有定义A,则到创建Fn的那个作用域Fm中找,如果仍没有找到,则继续到创建Fm的作用域中找……依次类推,直至找到变量A或者到全局作用域中仍未找到 为止,这种跨越多个作用域的线路,就叫“作用域链”。
代码如下:

复制代码
{
    // var A = 4; // 第4步,这是全局作用域,找到则返回4,如果到这里仍未找到,就结束,报错"A is not defined"

    function Fw() {
        // var A = 3; // 第3步,在作用域Fw中找,找到则返回3,否则到创建Fw的全局作用域中找

        function Fm() {
            // var A = 2; // 第2步,在作用域Fm中找,找到则返回2,否则到创建Fm的Fw中找

            function Fn() {
                // var A = 1; // 第1步,在当前作用域找,找到则返回1,否则到创建Fn的Fm中找
                console.log(A);
            }
            return Fn();
        }
        return Fm();
    }
    Fw();
}
复制代码

注意:

  这里说的创建Fn的那个作用域,而不是调用Fn的那个作用域,也不是“父作用域”。详情参考【自由变量及其取值规则 > 自由变量取值规则】中的实例。

15. 闭包

要想理解闭包,前面的【作用域、自由变量、作用域链】三部分是基础。
闭包的两种形式:函数作为返回值函数作为参数被传递

第一,函数作为返回值

复制代码
function fn() {
    var max = 10;
    return function bar(x) {
        if (x > max) { // max是自由变量,取值规则,参考【自由变量及其取值规则】
            console.log(x);
        }
    }
}
var f1 = fn();
f1(15); // 15
复制代码

第二,函数作为参数被传递

复制代码
var max = 10,
    fn = function(x) {
        if (x > max) { // max是自由变量,取值规则,参考【自由变量及其取值规则】
            console.log(x);
        }
    };
(function(f) {
    var max = 100;
    f(15); // 15
})(fn)
复制代码

另外,
在【执行上下文栈】中说到,每个函数执行完毕,都会销毁其函数上下文环境,并清空数据。但是闭包函数执行完后,上下文环境不会被销毁。因为闭包中函数会返回或者作为参数被传递,在其他地方会被用到,一旦销毁,调用闭包的地方就无法再使用了。所以闭包会增加内容开销。

参考:

  http://www.cnblogs.com/wangfupeng1988/p/3994065.html

  http://blog.csdn.net/yueguanghaidao/article/details/9568071

  http://blog.csdn.net/lmj623565791/article/details/25076713

 
分类: JavaScript
原文地址:https://www.cnblogs.com/libin-1/p/6701555.html