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(它是如何被创建的,不得而知!)。
例如:
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
this在执行上下文中有着非常重要的作用,这里应该说一下。
了解this,需要记住一点:在函数中this到底取何值,是在函数真正被调用执行的时候确定的,函数定义的时候确定不了。
大致分为以下4种情况:
情况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