JS的函数篇(4.3W字)
栏目: JavaScript · 发布时间: 5年前
内容简介:本系列将从以下专题去总结:暂时会对以上四个专题去总结,现在开始Part3:函数高级。下图是我这篇的大纲。
本系列将从以下专题去总结:
- JS基础知识深入总结
- 对象高级
- 函数高级
- 事件对象与事件机制
暂时会对以上四个专题去总结,现在开始Part3:函数高级。下图是我这篇的大纲。
3.1 this的使用总结
this
是在函数执行的过程中自动创建的一个指向一个对象的内部指针。确切的说, this
并不是一个对象,而是指向一个已经存在的对象的指针,也可以认为是 this
就是存储了某个对象的地址。
this
的指向不是固定的,会根据调用的不同,而指向不同的地方。 此章节的讲解思路是这样的:
3.1.1 搞懂this指向的预备知识
-
对在全局作用域中定义的变量和函数的进一步认识
永远记住:只要是在全局作用域声明的任何变量和函数默认都是作为window对象的属性而存在的.
理解完以上的这句话,我们在来说明一下其中的区别,这是很多人没有关注过的。
console.log(window.a); //undefined console.log(a); //报错!a is not defined 复制代码
注解:也就是在未对变量(
a
)进行声明时,就会出现以上结果。首先明确一点,就是全局变量a
是window
的属性。所以,我们从这里就可以发现,什么时候undefined
,什么时候报错?——那就是如果是访问一个对象的属性时,它没有声明赋值,那就是undefined
;如果访问一个变量,它没有声明赋值,那就是报错。好,现在回头过来,我们看全局作用域变量和函数的认识。
<script type="text/javascript"> var num = 10; //全局作用域声明的变量 function sum () { //全局作用域声明的函数 alert("我是一个函数"); } alert(window.num); // 10 window.sum(); // 我是一个函数 // 在调用的时候window对象是可以省略的。 </script> 复制代码
-
构造函数和非构造函数的澄清
在JavaScript中构造函数和非构造函数没有本质的区别。 唯一的区别只是调用方式的区别。
- 使用new 就是构造函数
- 直接调用就是非构造函数
看一个示例代码:
<script type="text/javascript"> function Person () { this.age = 20; this.sex = "男"; } //作为构造函数调用,创建一个对象。 这个时候其实是给p添加了两个属性 var p = new Person(); alert(p.age + " " + p.sex); //作为普通函数传递,其实是给 window对象添加了两个属性 //任何函数本质上都是通过某个对象来调用的,如果没有直接指定就是window,也就是Window可省略 Person(); alert(window.age + " " + window.sex); </script> 复制代码
3.1.2 第一个方向:全局作用域中的this指向
全局作用域中使用 this
,也就是说不在任何的函数内部使用 this
,那么这个时候 this
就是指的 Window
<script type="text/javascript"> //全局作用域中的this //向this对象指代的对象中添加一个属性 num, 并让属性的值为100 this.num = 100; // 因为this就是window,所以这时是在修改属性num的值为200 window.num = 200; alert(this === window); // true this就是指向的window对象,所以是恒等 alert(this.num); //200 alert(window.num); //200 </script> 复制代码
3.1.3 第二个方向:函数中的this
函数中 this
又可分为构造函数和非构造函数的 this
两个概念去理解。
-
非构造函数中的
this
指向非构造函数中
this
指向的就是 调用这个方法的那个对象
示例1:
<script type="text/javascript"> function test() { alert(this == window); this.age = 20; } test(); //其实是 window.test(); 所以这个时候test中的this指向window </script> 复制代码
示例2:
<script type="text/javascript"> var p = { age : 20, sex : "男", sayAge: function (argument) { alert(this.age); } } p.sayAge(); //调用对象p的方法sayAge() 所以这个时候this指的是 p 这个对象 </script> 复制代码
示例3:
<script type="text/javascript"> var p = { age : 20, sex : "男", sayAge: function (argument) { alert(this.age); alert(this === p); //true } } var again = p.sayAge; //声明一个变量(方法),把p的方法复制给新的变量 //调用新的方法: 其实是window.again(). //所以 方法中的this指代的是window对象,这个时候age属性是undefined // this和p也是不相等的。 again(); </script> 复制代码
综上: this
的指代和代码出现的位置无关,只和调用这个方法的对象有关。
- 构造方法中的this指向
构造方法中的 this
指代的要未来要创建的那个对象。
示例1:
<script type="text/javascript"> function Person () { this.age = 20; return this; //作为构造函数的时候,这个行代码默认会添加 } var p1 = new Person(); //这个时候 Person中的this就是指的p1 var p2 = new Person(); //这是时候 Person中的this就是知道p2 </script> 复制代码
多了解一点:其实用 new
调用构造函数的时候,构造函数内部其实有个默认的 return this
; 这就是为什么 this
指代那个要创建的对象了。
3.1.4 第三个方向: 改变this的指向(显式绑定)
在JavaScript中,允许更改 this
的指向。 通过 call
方法或 apply
方法
函数A可以成为指定任意对象的方法进行调用 。函数A就是函数对象,每个函数对象中都有一个方法 call
,通过 call
可以让你指定的对象去调用这个函数A。
ECMAScript 规范给所有函数都定义了 call
与 apply
两个方法。 call
和 apply
是放在Function的原型对象上的,而不是Object原型对象上!
<script type="text/javascript"> var age = 20; function showPropertyValue (propertyName) { alert(this[propertyName]); } //使用call的时候,第一个参数表示showPropertyValue中的this的执行,后面的参数为向这个函数传的值。 //注意一点:如果第一个参数是null,则this仍然是默认的指向。 showPropertyValue.call(null, "age"); showPropertyValue.call(this, "age"); showPropertyValue.call({age:50}, "age") </script> 复制代码
3.1.5 call / apply / bind 的详解
在 this
的指向中有第三个方向就是通过 call
/ apply
去改变 this
的指向,这个JavaScript中一个独特的使用形式,其他语言并没有。那么,我们就在这里顺带讲一下 call
、 apply
以及 bind
的用法。
本小节将从三个方面讲解: 1: apply
和 call
的区别 2: apply
和 call
的用法 3: call
和 bind
的区别
3.1.5.1 apply 和 call 的区别
ECMAScript 规范给所有函数都定义了 call
与 apply
两个方法,它们的应用非常广泛,它们的作用也是一模一样,只是传参的形式有区别而已。
简单来说,假设有一个函数A,我们调用函数A会直接去 A()
,那么如果是 A()
这样直接调用的话,函数体A里面的 this
就是 window
了。而我们可以通过 call
(或 apply
)去调用,比如: A.call()
.这样子调用就可以指定A中的this到底是哪个对象。
用 call
来做比对,里面有两个参数,参数一就是重新指定其中的 this
是谁,参数2是属性名。而事实上, call
与 apply
也就是参数二的不同这个差异。
apply
apply
方法传入两个参数:一个是作为函数上下文的对象,简单来说,重新指定函数中的 this
是谁。另外一个是作为函数参数所组成的数组,是传入一个数组。
var obj = { name : 'ya LV' } function func(firstName, lastName){ console.log(firstName + ' ' + this.name + ' ' + lastName); } func.apply(obj, ['A', 'B']); // A ya LV B 复制代码
可以看到, obj
是作为函数上下文的对象,也就是说函数 func
中 this
指向了 obj
这个对象。本来如果直接调用 func()
,那么函数体中的 this
就是指的是 window
。但是现在有了参数一,就是重新指定 this
,这个 this
就是参数一的 obj
这个对象。参数 A 和 B 是放在数组中传入 func
函数,分别对应 func
参数的列表元素。
call
call
方法第一个参数也是作为函数上下文的对象。与 apply
没有任何区别。但是后面传入的是一个参数列表,而不是单个数组。
var obj = { name: 'ya LV' } function func(firstName, lastName) { console.log(firstName + ' ' + this.name + ' ' + lastName); } func.call(obj, 'C', 'D'); // C ya LV D 复制代码
对比 apply
我们可以看到区别,C 和 D 是作为单独的参数传给 func
函数,而不是放到数组中。
对于什么时候该用什么方法,其实不用纠结。如果你的参数本来就存在一个数组中,那自然就用 apply
,如果参数比较散乱相互之间没什么关联,就用 call
。
补充一个使用 apply
的例子。比如求一个数组的最大值?
明确JavaScript中没有返回一个数组中最大值的函数。但是,有一个函数 Math.max
可以返回任意多个数值类型的
参数中的最大值, Math.max
函数入参并不支持数组,只能是将多个参数逐个传入,用逗号分隔。
这个时候如果要可以用Math.max函数,且传参数可以是一个数组。我们自然而然会想到所有函数都定义了 call
和 apply
的方法,我们可以配合 apply
或 call
来实现,又因为 call
传参数并不是一个数组。所有我们就选择出 Math.max
函数加上 apply
就可以实现我们的目的。
原本只能这样用,并不能直接用数组,见示例:
let max = Math.max(1, 4, 8, 9, 0) 复制代码
有了 apply
,就可以这么调用:
let arr = [1, 4, 8, 9, 0]; let max = Math.max.apply(null, arr); 复制代码
在调用 apply
的时候第一个参数给了一个 null
,这个是因为没有对象去调用这个方法,我们只需要用这个方法帮我们运算,得到返回的结果就行,所以就直接传递了一个 null
过去。
3.5.1.2 apply 和 call 的用法
apply
和 call
的用法可以分为三个:改变 this
的指向,借用别的对象的方法,调用函数。
- 1.改变 this 指向
var obj = { name: 'ya LV' } function func() { console.log(this.name); } func.call(obj); // ya LV 复制代码
这个在前一小节有讲到,所以我们就简单的再来看看。所谓“熟能生巧”,一样东西,一个知识点,每看一次会有不同的体会,可能这次看的过程让你有更深刻的思考,这就是进步。 call
方法的第一个参数是作为函数上下文的对象,这里把 obj
作为参数传给了 func
,此时函数里的 this
便指向了 obj
对象。此处 func
函数里其实相当于:
function func() { console.log(obj.name); } 复制代码
另外注意下 call
的一些特别用法,很奇葩的 this
指向。稍微注意下,有点印象就好。
function func() { console.log(this); } func.call(); //window func.call(undefined); //window func.call(null); //window func.call(1); //Number {1} 这种情况会自动转换为包装类Number 就相当于下面一行代码 func.call(new Number(1)); //Number {1} 复制代码
- 2.借用别的对象的方法
var Person1 = function () { this.name = 'ya LV'; } var Person2 = function () { this.getname = function () { console.log(this.name); } Person1.call(this); } var person = new Person2(); person.getname(); // ya LV 复制代码
从上面我们看到, Person2
实例化出来的对象 person
通过 getname
方法拿到了 Person1
中的 name
。因为在 Person2
中, Person1.call(this)
的作用就是使用 Person1
对象代替 this
对象,那么 Person2
就有了 Person1
中的所有属性和方法了,相当于 Person2
继承了 Person1
的属性和方法。 不理解的话我们再来慢慢看,我们说 A.call ( 参数一)
这样的形式就是重新指定 函数A
中的 this
是 ‘参数一’
这个对象,那么我们来看看 Person2
函数体中的 Person1.call(this)
这条语句,其中这条语句的 this
是指 Person2
这个对象。现在就是把 Person1
函数的 this
重新指向为 Person2
,是不是有了 Person2.name='ya LV'
。
- 3.调用函数
apply
、 call
方法都会使函数立即执行,因此它们也可以用来调用函数。这个我们在这节的一开始就有说,比如 A()
和 A.call()
都是调用函数A。
function func() { console.log('ya LV'); } func.call(); // ya LV 复制代码
3.5.1.3 call 和 bind 的区别
在 EcmaScript5
中扩展了叫 bind
的方法,在低版本的 IE 中不兼容。它和 call
很相似,接受的参数有两部 分,第一个参数是是作为函数上下文的对象,第二部分参数是个列表,可以接受多个参数。
它们之间的区别有以下两点。
- 区别1.bind 的返回值是函数
var name='HELLO' var obj = { name: 'ya LV' } function func() { console.log(this.name); } //将func的代码拷贝一份,并且永远改变其拷贝出来的函数中的this,为bind第一个参数所指向的对象。把这 份永远改变着this指向的函数返回给func1. var func1 = func.bind(obj); //bind方法不会立即执行,是返回一个改变上下文this的函数,要对这个函数调用才会执行。 func1(); //ya LV //可以看到,现在这份改变this之后拷贝过来的函数,this的指向永远是bind()绑定的那个,不管之后去call 重新指向对象,func1 都不会改变this的指向。永远!可知,bind比call优先级还高。 func1.call({name:'CALL'}); //ya LV //又从func重新拷贝一份永远改变this指向对象为{name:'LI SI'}这个对象的函数,返回给func2. var func2 = func.bind({name:'LI SI'}); func2(); //LI SI //注意,这里是拷贝一份func2(而不是func)的代码,而func2之前已经绑定过去永远改变this的指向了,所以这 里并不去改变!还是会输出原来的最先bind的this指向对象。 var func3 = func2.bind({name:'ZHANG SAN'}); func3(); //LI SI //上面对func最初的函数进行了多次绑定,绑定后原函数 func 中的 this 并没有被改变,依旧指向全局对象 window。因为绑定bind的过程是拷贝代码的一个过程,而不是在其自身上修改。window.name = HELLO func(); //HELLO 复制代码
bind
方法不会立即执行,而是返回一个改变了上下文 this
后的函数。而原函数 func
中的 this
并没有被改变,依旧指向全局对象 window
。
- 区别2.参数的使用
function func(a, b, c) { console.log(a, b, c); } var func1 = func.bind(null,'yaLV'); func('A', 'B', 'C'); // A B C func1('A', 'B', 'C'); // yaLV A B func1('B', 'C'); // yaLV B C func.call(null, 'yaLV'); // yaLV undefined undefined 复制代码
call
是把第二个及以后的参数作为 func
方法的实参传进去,而 func1
方法的实参实则是在 bind
中参数的基础上再往后排。也就是说, var func1 = func.bind(null,'yaLV');
bind
现有两个参数,第一个是指向,第二个实参是 'yaLV'
,那么就是先让 func
中的 a='yaLV'
,然后没排满就是让 func1('A', 'B', 'C')
; 这个参数依次排,现在 b='A'
, c='B'
, 形参已经排完了。也就是输出 yaLV A B
。
在低版本浏览器没有 bind
方法,我们也可以自己实现一个。
if (!Function.prototype.bind) { Function.prototype.bind = function () { var self = this, // 保存原函数 context = [].shift.call(arguments), // 保存需要绑定的this上下文 args = [].slice.call(arguments); // 剩余的参数转为数组 return function () { // 返回一个新函数 self.apply(context[].concat.call(args[].slice.call(arguments)); } } } 复制代码
3.1.6 习题与案例
习题1
<script type="text/javascript"> var name='window_dqs'; var obj={ name:'obj_dqs', showName:function(){ console.log(this.name); }}; function fn(){ console.log(this); } function fn2(){ this.name='fn_dqs'; } //因为obj去调用,this就是obj obj.showName(); //obj_dqs //因为借调,而此时借调的对象是this,而this在全局作用域上就是指window,所以找window.name obj.showName.apply(this); //window_dqs //因为借调的对象是一个函数对象,那么this就是指函数对象,this.name就是函数名 obj.showName.apply(fn2); //fn2 </script> 复制代码
习题2
<script type="text/javascript"> var name='window_dqs'; function fn(){ this.name='fn_dqs'; this.showName=function(){ console.log(this.name); } console.log(this); } function fn2(){ this.name='fn_pps'; this.showName=function(){ console.log(this.name); } console.log(this); } var p=new fn(); fn2.apply(p); p.showName(); var obj={}; fn2.apply(obj); obj.showName(); </script> 复制代码
结果是:
var p=new fn();输出fn { name: 'fn_dqs', showName: [Function] } fn2.apply(p);输出fn { name: 'fn_pps', showName: [Function] } p.showName();输出fn_pps var obj={}; fn2.apply(obj);输出Object{name: "fn_pps"showName: ƒ ()__proto__: Object··} obj.showName();输出fn_pps 复制代码
习题3
<script type="text/javascript"> var name='window_dqs'; var obj={ name:'json_dqs', showName:function(){ console.log(this.name); return function(){ console.log(this.name); } } } var p=obj.showName(); obj.showName()(); p.call(obj); </script> 复制代码
结果是:
json_dqs json_dqs window_dqs json_dqs 复制代码
面试题1
代码片段1:
var name = "The Window"; var object = { name: "My Object", getNameFunc: function (){ return function (){ return this.name; }; } }; console.log(object.getNameFunc()); //ƒ (){return this.name;} console.log(object.getNameFunc()()); //The Window 复制代码
代码片段一没有闭包。有嵌套,但没有用外部函数的变量或函数。是使用 this
的。 this
与调用方式有关。
理解:看 object.getNameFunc()
是对象.方法() 返回的是一个函数,这个函数还未执行。js中 this
是动态的,所以函数没有执行,并不确定函数里的 this
是指的是谁?那么现在再对返回的函数加个(),也就是 object.getNameFunc()()
,调用执行,把最后一个括号和最后一个括号前当做两个部分,前面是函数名,后面一个括号是调用。相当于 test()
,这个时候 this
就是 window
。故这样调用的函数this就是指的 window
,故 window.name=The Window
.
代码片段二: 对于片段一我们的本意是不是想输出 My Object
。那么怎么改造,通过 that=this
去操作。
var name2 = "The Window"; var object2 = { name2: "My Object", getNameFunc: function () { var that = this; //缓存this return function () { return that.name2; }; } }; console.log(object2.getNameFunc()); //ƒ (){return that.name2;} console.log(object2.getNameFunc()()); //My Object 复制代码
代码片段二是有闭包的,有嵌套函数。内部函数有使用外部函数的变量 that
。外部和内部函数有执行。
理解:首先还是看 object2.getNameFunc()
返回一个函数,注意这个函数中没有 this
,在调用 object2.getNameFunc
时,我们有执行一句 var that = this
;也就是把 this
给 that
,这个时候 this
是指的是 object2
。再次调用 object2.getNameFunc()()
时就是执行 object2.getNameFunc()
返回来的函数”。 that.name2=object2.name2
;实质上是闭包,使用了外部函数的 that
变量。
代码片段三(对片段二的改造):
var name3 = "The Window"; var object3 = { name3: "My Object", getNameFunc: function () { return function () { return this.name3; }.bind(this); } }; console.log(object3.getNameFunc()); //ƒ (){return this.name3;} console.log(object3.getNameFunc()()); //My Object 复制代码
理解:与“代码片段二”一样,只是片段二是通过 that=this
去改变 this
的值,而片段三是通过 bind
绑定 this
的值。看 bind(this)
这里的 this
就是指这条语句 object3.getNameFunc()
调用的对象 object3
.所以通过这个手段去把 this
指向了前面的对象 object3
.再去调用返回的函数时,那么 this.name3=object3.name3
。
面试题2
<script> var myObject = { foo: "bar", func: function() { var self = this; console.log(this.foo); //bar console.log(self.foo); //bar (function() { console.log(this.foo); //undefined 此时的this是window console.log(self.foo); //bar 闭包可以看到外部的局部变量 }()); //匿名函数自执行,是window上调用这个函数。 } }; myObject.func(); //那么如何修改呢?使得在自执行函数中的this.foo就是我们想要的bar呢? //提供两种方法: //case1:用call去指向this是谁 var myObject = { foo: "bar", func: function() { var self = this; console.log(this.foo); // bar console.log(self.foo); // bar (function() { console.log(this.foo); // bar console.log(self.foo); // bar }.call(this)); //myObject.func();这样调用func(),那么func()中的this就是前面的对象myObject。 } }; myObject.func(); //case2:用bind去绑定this,但要注意bind是返回一个函数,故要bind(this)(),后一个括号表示函数调用。把bind(this)将拷贝一份并改变this的指向的函数执行。 var myObject = { foo: "bar", func: function() { var self = this; console.log(this.foo); // bar console.log(self.foo); // bar (function() { console.log(this.foo); // bar console.log(self.foo); // bar }.bind(this)()); } }; myObject.func(); </script> 复制代码
面试3考察 this
的指向: 难点:数组(类数组)中的元素当做函数调用时的 this
指向 也就是,如果是调用数组(类数组)中的元素, 元素函数中的 this
是这个数组(类数组)。
<script> var length = 10; function fn(){ console.log(this.length); } var obj = { length: 5, method: function (fn){ // [fn, 1, 2] fn(); // 10 arguments[0](); // 3 } }; obj.method(fn, 1, 2); /*obj.method(fn, 1, 2);传实参fn过去,此时fn拿到函数的地址值拷贝给形参fn,在执行fn()这里调用是相当window调用fn,this指的是window。而不是obj不要感觉是在obj里面就是,迷惑大家的。this的指向永远跟调用方式有关。 *另外,arguments[0]();调用时,这个时候是类数组中的元素调用,那么这时的this是类数组本身,所以,数组.length是不是输出类数组的长度。 *如果是调用数组(类数组)中的元素, 元素函数中的this是这个数组(类数组).为什么呢?看以下两个例子: * */ //例子1: var obj = { age : 100, foo : function (){ console.log(this); } } var ff = obj.foo; ff(); //window obj.foo(); //{age: 100, foo: ƒ} obj["foo"](); //{age: 100, foo: ƒ} //上面的这个例子没有问题吧。很自然的。 //例子2: var arr = [ function (){ console.log(this); },function (){ } ]; var f = arr[0]; f(); //window /*arr.0()--类似于这么写把,只是数组不允许这样的语法--*/ arr[0](); //输出数组本身:(2) [ƒ, ƒ] 。故验证一句话:如果调用数组(类数组)中的元素时,那么这时的this是数组(类数组)本身。 </script> 复制代码
3.2 原型与原型链
一个特别经典的总结:
从 a.b
就可以看出作用域与作用域链,原型与原型链的知识。 (详见Part1的1.1.3)
3.2.1 五张图理解原型与原型链
构造函数创建对象我们先使用构造函数创建一个对象:
function Person() { } var person = new Person(); person.name = 'name'; console.log(person.name) // name 复制代码
在这个例子中, Person
就是一个构造函数,我们使用new创建了一个实例对象 person
。
很简单吧,接下来进入正题: 【prototype】
任何的函数都有一个属性 prototype
,这个属性的值是一个对象,这个对象就称为这个函数的原型对象。但是一般情况,我们只关注构造函数的原型。比如:
function Person() { } // 虽然写在注释里,但是你要注意:prototype是函数才会有的属性 Person.prototype.name = 'name'; var person1 = new Person(); //person1是Person构造函数的实例 var person2 = new Person(); //person2是Person构造函数的实例 console.log(person1.name) // name console.log(person2.name) // name 复制代码
其实,函数的 prototype
属性指向了一个对象,这个对象正是调用该构造函数而创建的实例的原型,也就是这个例子中的 person1
和 person2
的原型。实例其实是通过一个不可见的属性 [[proto]]
指向的。
你可以这样理解:每一个JavaScript对象( null
除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型”继承”属性。
让我们用一张图表示构造函数和实例原型之间的关系:
那么我们该怎么表示实例与实例原型,也就是person1
person2
和
Person.prototype
之间的关系呢,这时候我们就要讲到第二个属性:
[[proto]]
当使用构造函数创建对象的时候, 新创建的对象会有一个不可见的属性[[proto]], 他会指向构造函数的那个原型对象。事实上,每一个JavaScript对象(除了null)都具有的一个不可见属性,叫[[proto]],这个属性会指向该对象的原型。
为了证明这一点,我们可以在火狐或者谷歌中输入:
function Person() { } var person1 = new Person(); console.log(person1.__proto__ === Person.prototype); //true 复制代码
于是我们更新下关系图:
既然实例对象和构造函数都可以指向原型,那么原型是否有属性指向构造函数或者实例呢?指向实例倒是没有,因为一个构造函数可以生成多个实例,但是原型指向构造函数倒是有的,这就要讲到第三个属性:【 constructor 】,每个原型都有一个constructor
属性指向关联的构造函数。
为了验证这一点,我们可以尝试:
function Person() { } console.log(Person === Person.prototype.constructor); //true 复制代码
所以再更新下关系图:
综上我们已经得出:
function Person() { } var person1 = new Person(); //对象的__proto__属性: 创建对象时自动添加的, 默认值为构造函数的prototype属性值(很重要) console.log(person1.__proto__ == Person.prototype) // true console.log(Person.prototype.constructor == Person) // true // 顺便学习一个ES5的方法,可以获得对象的原型 console.log(Object.getPrototypeOf(person1) === Person.prototype) //true 复制代码
了解了构造函数、实例原型、和实例之间的关系,接下来我们讲讲实例和原型的关系: 【实例与原型】 。当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。
举个例子:
function Person() { } //往Person对象原型中添加一个属性 Person.prototype.name = 'name'; //创建一个person1实例对象 var person1 = new Person(); //给创建的实例对象person1添加一个属性 person1.name = 'name of this person1'; //查找person1.name,因为本身实例对象有,那么就找到了自身实例对象上的属性和属性值 console.log(person1.name) // name of this person1 //删除实例对象的属性和属性值 delete person1.name; //查找属性name,在实例对象自身上找不到,通过proto指向往原型链上找,在原型对象中找到 console.log(person1.name) // name 复制代码
在这个例子中,我们设置了 person1
的 name
属性,所以我们可以读取到为 name of this person1
,当我们删除了 person1
的 name
属性时,读取 person1.name
,从 person1
中找不到就会从 person
的原型也就是 person.__proto__ == Person.prototype
中查找,幸运的是我们找到了为 name
,但是万一还没有找到呢?原型的原型又是什么呢?
var obj = new Object(); obj.name = 'name' console.log(obj.name) // name 复制代码
所以原型对象是通过 Object
构造函数生成的,结合之前所讲的一句很重要的话,几乎就是涵盖原型与原型链知识的始终的一句话,那就是:实例对象的 proto
指向构造函数的 prototype
。也就是说, Person.prototype
这个原型对象(实例原型)是通过 Object
这个构造函数new出来的,也就是Person.prototype这个原型对象是 Object
的实例,所以这个实例会有 proto
属性指向 Object
构造函数的原型对象 Object.prototype
。
这里呢插入一句总结出来的话,逆推顺推都是可行的,那就是:实例通过 proto
这个属性指向其构造函数的原型对象。所以我们再更新下关系图:
Object.prototype
的原型呢?
null
,嗯,就是
null
。所以查到
Object.prototype
就可以停止查找了。所以最后一张关系图就是:
那【 原型链 】是啥 ? 那就是由 proto
这个属性进行查找的一个方向这就是一条原型链。图中由相互关联的原型组成的链状结构就是原型链,也就是蓝色的这条线,都是通过 proto
属性进行查找的。
那么访问一个对象的属性时,怎么通过原型链去查找属性或方法呢 ? 先在自身属性中查找,找到返回。如果没有, 再沿着 proto
这条链向上查找, 找到返回。如果最终没找到, 返回 undefined
。
3.2.2 从代码中看原型与原型链
解析上图:首先n
,
s
都是全局变量,然后通过对象字面量的方法去创建了一个对象。然后有一个构造函数(之所以是构造函数,是因为后面代码有
new
的操作),这个构造函数就会有函数声明提前,当构造函数声明时,就会去在内存中创建一个
person
的函数对象,这个函数对象里
只有 prototype
属性 ,去指向
person
的函数原型对象。要注意,现在还没有去执行里面的代码,只是函数声明时创建了一个
person
的函数对象。后面就是new的实例对象,新
new
出来的实例对象
P2
P3
就会在内存中分配一块内存去把地址值给它,现在才会去执行构造函数中的代码。所以只有
P2
P3
才会 有
name
age
speak
属性和方法。这些新
new
出来的实例对象就会有一个不可见的属性
proto
,去指向这个原型对象。而最终这个
person
的函数原型对象会有指向一个
object
的原型对象,再上去其实就是
null
。这就一层一层往上走就是原型链,因为原型链,我们才会有继承的特性。
注几点:
- 从上图的图示中可以看到,创建
P2
P3
实例对象虽然使用的是Person
构造函数,但是对象创建出来之后,这个P2
P3
实例对象其实已经与Person
构造函数(函数对象)没有任何关系了,P2
P3
实例对象的[[ proto ]]
属性指向的是Person
构造函数的原型对象。 - 如果使用
new Person()
创建多个对象,则多个对象都会同时指向Person
构造函数的原型对象。 - 我们可以手动给这个原型对象添加属性和方法,那么
P2
P3
····这些实例对象就会共享这些在原型中添加的属性和方法。也就是说,原型对象相当于公共的区域,所有的同一类的实例都可以去访问到原型对象。 - 如果我们访问
P2
实例对象 中的一个属性gender
,如果在P2
对象中找到,则直接返回。如果P2
对象中没有找到,则直接去P2
对象的[[proto]]
属性指向的原型对象中查找,如果查找到则返回。(如果原型中也没有找到,则继续向上找原型的原型---原型链)。 - 读取对象的属性值时: 会自动到原型链中查找
- 设置对象的属性值时: 不会查找原型链, 如果当前对象中没有此属性, 直接添加此属性并设置其值。比如通过
P2
对象只能读取原型中的属性name
的值,并不能修改原型中的属性name
的值。P2.gender= "male"
; 并不是修改了原型中的值,而是在P2
对象中给添加了一个属性gender
。 - 方法一般定义在原型中, 属性一般通过构造函数定义在对象本身上。
另外看看,原型与原型链的三点关注:
- 函数的显示原型指向的对象默认是空Object实例对象(但Object不满足)
console.log(Fn.prototype instanceof Object) // true console.log(Object.prototype instanceof Object) // false console.log(Function.prototype instanceof Object) // true 复制代码
- 所有函数都是Function的实例(包含Function)
console.log(Function.__proto__===Function.prototype) //true 复制代码
- Object的原型对象是原型链尽头
console.log(Object.prototype.__proto__) // null 复制代码
3.2.3 探索 instanceof
instanceof
是如何判断的?
-
表达式:
A instanceof B
-
如果B构造函数的原型对象(
B.prototype
)在A实例对象的原型链(A.proto.proto·····
沿着原型链)上, 返回true
, 否则返回false
。(见下图) -
也就是说A实例对象的原型链上可能会有很多对象,只要B构造函数的原型对象有一个是在其原型链上的对象即可返回
true
。 -
反过来说也一样,实例对象A是否可以通过
proto
属性(沿着原型链,A.proto.proto·····
)找到B.prototype
(B的原型对象),找到返回true
,没找到返回false
. -
注1:对实例对象的说明,事实上,实例对象有两种。一种是我们经常说的
new
出来的实例对象(比如构造函数Person
,new
出来p1
p2
...,这些都是实例对象),另外一种就是函数,函数本身也是实例,是Function
new
出来的。但我们一般说的实例对象就是指new
出来的类似于p1
p2
这些的实例对象。
Function
是通过 new
自己产生的实例(Function.proto===Function.prototype)
案例1:
//一个构造函数Foo function Foo() { } //一个f1实例对象 var f1 = new Foo() //翻译:f1是Foo的实例对象吗? //还记得我说过,一个实例对象通过proto指向其构造函数的原型对象上。 //深入翻译:f1这个实例对象通过proto指向是否可以找到Foo.prototype上呢? console.log(f1 instanceof Foo) // true //这行代码可以得出,沿着proto只找了一层就找到了。 console.log(f1.__proto__ === Foo.prototype); // true //翻译:f1是Object的实例对象吗? //深入翻译:f1这个实例对象通过proto指向是否可以找到Object.prototype上呢? console.log(f1 instanceof Object) // true //这两行代码可以得出,沿着proto找了两层才找到。事实上,f1.__proto__找到了Foo.prototype(Foo构造函数原型上),再次去.__proto__,找到了Object的原型对象上。见下图。 console.log(f1.__proto__ === Object.prototype); // false console.log(f1.__proto__.__proto__ === Object.prototype); // true 复制代码案例2:
//这个案例的实质还是那句话:一个实例对象通过proto属性指向其构造函数的原型对象上。 //翻译:实例对象Object是否可以通过proto属性(沿着原型链)找到Function.prototype(Function的原型对象) console.log(Object instanceof Function) // true //以上结果的输出可以看到下图,Object.__proto__直接找到一层就是Function.prototype.(Object created by Function)可知Object构造函数是由Function创建出来的,也就是说,Object这个实例是new Function出来的。 console.log(Object instanceof Object) // true //很有意思。上面我们已经知道Object这个实例是new Function出来的。也就是Object.proto指向Function.prototype。有意思的是,Function的原型对象又是Object原型对象的一个实例,也就是Function.prototype.proto 指向 Object.prototype .很有意思吧,见下图很更清楚这个“走向”。 console.log(Function instanceof Function) // true //由这个可知,可以验证我们的结论:Function是通过new自己产生的实例。 Function.proto===Function.prototype console.log(Function instanceof Object) // true //Function.proto.proto===Function.prototype (找了两层) //定义了一个Foo构造函数。由下图可知,Foo.proto.proto.proto===null function Foo() {} console.log(Object instanceof Foo) // false //这条语句要验证的是,Object是否可以通过其原型链找到Foo.prototype。 // Object.proto.proto.proto=null 并不会找到Foo.prototype。所以,返回FALSE。 复制代码看上图,再引申出一个问题:函数是对象。那你觉得函数包含的大?还是对象大呢? 如上图,对象是由函数创造的。
(Object created by Function)
也就是说,对象是
new Function
得到的。 继续翻译,对象是实例
Function
是构造函数。 继续翻译,对象这个实例有不可见属性
proto
指向
Function
构造函数的原型对象
(Function.prototype)
。 故,函数与对象的关系是:函数更大,它包含对象。 这个我个人觉得很重要,务必理解透。
3.2.4 一些概念的梳理
- 所有函数都有一个特别的属性:
-
prototype
: 显式原型属性
-
- 所有实例对象都有一个特别的属性:
-
__proto__
: 隐式原型属性
-
- 显式原型与隐式原型的关系
{} __proto__
- 原型链
__proto__ __proto__
3.2.5 习题与案例
面试1:
/*阿里面试题*/ ①function Person(){ ②getAge = function (){ console.log(10) } ③return this; } ④Person.getAge = function (){ console.log(20); } ⑤Person.prototype.getAge = function (){ console.log(30); } ⑥var getAge = function (){ console.log(40) } ⑦function getAge(){ console.log(50) } Q1:Person.getAge() // 20 Q2:getAge() // 40 Q3:Person().getAge() // 10 Q4:getAge() // 10 Q5:new Person().getAge() // 30 Q6:new Person.getAge(); // 20 复制代码
整体代码块①定义了构造函数 Person
②是在构造函数中有一个未声明的变量,这个变量是引用变量,内容为地址值。指向一个函数对象。又因为,未使用严格模式下,在函数中不使用 var
声明的变狼都会成为全局变量。(注意这里不是属性,是全局变量)同时也要注意,这里②和③的语句在解析到这里后并没有执行。执行的话就要看有没有 new
(作为构造函数使用),或者有没有加()调用(作为普通函数使用)。 ③返回一个 this
。这个 this
是谁现在还不知道。需要明白js中的 this
是动态的,所以根据上一节 this
的总结才定位到 this
到底是谁。 ④ Person.getAge
是典型的“对象.属性(方法)”的形式,所以它是给 Person
函数对象上添加一个 getAge
的方法。等同于:
function Person.getAge(){ console.log(20); } 复制代码
函数名其实就是变量名。 ⑤在构造函数的原型中添加了 getAge
的方法 ⑥这里也是给一个全局变量赋值一个地址值,使其指向一个函数对象。注意,这里var的变量会声明提前。与代码块②区别,这里当解析完后, getAge
已经指向一个函数对象啦。可以看做:
function getAge(){ console.log(40) } 复制代码
⑦定义一个函数,函数也会声明提前。在栈内存有 getAge
,内容值为一个地址值,指向一个函数对象。
Q1:对象.属性方法()。代码块④产生的结果。 Q2:调用函数,全局作用域里的。那只有代码块⑥产生结果。 Q3: Person().getAge()
。先看前面一部分 Person()
,把 Person
当做一个普通函数调用,执行 Person
函数体对全局变量 getAge
进行定义并重新指向,也就是 Person()
执行了代码块②而覆盖了代码块⑥的操作。又返回this,根据 Person()
这种调用方式,可知this就是 window
。所以就是“window.gerAge()”,因被覆盖了,所以这行代码执行结果是代码块②产生。 Q4: getAge()
相当于 window.getAge()
; 还是上一个语句的结果,代码块②产生结果。 Q5: new Person()
先看这部分,就是new出来一个实例,你可以想成 p1
,那么 p1.getAge();
p1
是一个 Person
的实例, p1
中有不可见的 [[proto]]
属性,指向 Person
的原型对象。那么 p1.getAge ()
,现在 p1
本身找,找不到就沿着原型链( proto
指向链)去找,好找到了原型对向中有,因为代码块⑤产生作用。 Q6: new Person.getAge();
可以把 Person.getAge
看成一个对象,去 new
它,是不是类似于我们平常 var p1=new Person()
;这样的操作,所以我们把 Person.getAge
看做一个构造函数去 new
它。由上面对代码块④的理解,可以看做那样的函数,所以结果就是代码块④产生的结果。
面试题2:
function A () { } A.prototype.n = 1; var b = new A(); A.prototype = { n: 2, m: 3 }; var c = new A(); console.log(b.n, b.m, c.n, c.m); //1 undefined 2 3 //见下图: 复制代码面试题3:连续赋值问题
//与上题的区别在于如何理解a.x的执行顺序 <script> var a = {n: 1}; var b = a; a.x = a = {n: 2}; //先定义a.x再去从右往左赋值操作。 console.log(a.x); // undefined 对象.属性 找不到 是返回undefined 变量找不到则报错! console.log(b); // {n :1, x : {n : 2}} </script> //见下图分析 复制代码
面试题4:
//构造函数F function F (){}; Object.prototype.a = function(){ console.log('a()') }; Function.prototype.b = function(){ console.log('b()') }; //new一个实例对象f var f = new F(); f.a(); //a() f.b(); //报错,找不到 F.a(); //a() F.b(); //b() 复制代码
2.3 执行上下文与执行上下文栈
2.3.1 变量提升与函数提升
变量声明提升
var undefined
console.log(c); //报错,c is not defined. console.log(b); //undefined var b=0; c=4; console.log(c); //4 意外的全局变量->在ES5的严格模式下就会报错。 console.log(b); //0 复制代码
函数声明提升
- 通过function声明的函数, 在之前就可以直接调用
- 值: 函数定义(对象)
- 注1:函数声明(Function Declaration)和函数表达式(Function Expression)是有微妙的区别,要明确他们两是Javascript两种类型的函数定义,两个概念上是并列的。也就是说定义函数的方式有两种:一种是函数声明,另一种就是函数表达式。
- 注2:函数表达式并不会声明提升。
先有变量提升, 再有函数提升
案例一:
var a = 3; function fn () { console.log(a); //undefined var a = 4 } fn(); //上面这段代码相当于 var a = 3; function fn () { var a; console.log(a); //undefined a = 4 } fn(); 复制代码
案例二:
console.log(b) //undefined 变量提升 fn2() //可调用 函数提升 fn3() //不能调用,会报错。 fn3是一个函数表达式,并不会函数提升,实际上他是变量提升。 var b = 3 function fn2() { console.log('fn2()') } var fn3 = function () { console.log('fn3()') } 复制代码
问题: 变量提升和函数提升是如何产生的?An:因为存在全局执行上下文和函数执行上下文的预处理过程。所以我们就来学习下一节的执行上下文。
2.3.2 执行上下文
代码分类(位置)
- 全局代码
- 函数(局部)代码
执行上下文分为全局执行上下文和函数执行上下文
全局执行上下文
- 步骤1:在执行全局代码前将
window
确定为全局执行上下文对象(虚拟的) - 步骤2:对全局数据进行预处理(收集数据)
-
var
定义的全局变量==>值为undefined
, 并添加为window
的属性 -
function
声明的全局函数==>赋值(fun
), 添加为window
的方法 -
this
==>赋值(window
)
-
- 步骤3:开始执行全局代码
//全局执行上下文 console.log(a1); //undefined console.log(a2); //undefined a2(); //也会报错,a2不是一个函数 console.log(a3); //ƒ a3() {console.log('a3()')} console.log(a4) //报错,a4没有定义 console.log(this); //window var a1 = 3; //函数表达式,实际上是变量提升。而不是函数提升。 var a2 = function () { console.log('a2()') }; function a3() { console.log('a3()') } a4 = 4; 复制代码
函数执行上下文
- 步骤1:在调用函数, 准备执行函数体之前, 创建对应的函数执行上下文对象(虚拟的, 存在于栈中。栈会分为全局变量栈和局部变量栈,局部变量栈可以理解为是一个封闭的内存空间。虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它。)
- 步骤2:对局部数据进行预处理(收集数据)
- 形参变量==>赋值(实参)==>添加为执行上下文的属性
-
arguments
==>赋值(实参列表), 添加为执行上下文的属性 -
var
定义的局部变量==>undefined
, 添加为执行上下文的属性 -
function
声明的函数 ==>赋值(fun
), 添加为执行上下文的方法 -
this
==>赋值(调用函数的对象)
- 步骤3:开始执行函数体代码
//函数执行上下文 function fn(a1) { console.log(a1); //2 实参对形参赋值 console.log(a2); //undefined 函数内部局部变量声明提升 a3(); //a3() 可调用 函数提升 console.log(arguments); //类数组[2,3] console.log(this); //window var a2=3; function a3() { console.log("a3()"); } } fn(2,3); //执行,不执行不会产生函数执行上下文 复制代码
全局执行上下文和函数执行上下文的生命周期
全局 : 准备执行全局代码前产生, 当页面刷新/关闭页面时死亡
函数 : 调用函数时产生, 函数执行完时死亡
2.3.3 执行上下文栈
- 执行上下文栈流程理解:
window window
<script type="text/javascript"> //1. 进入全局执行上下文 var a = 10; var bar = function (x) { var b = 5; foo(x + b) //3. 进入foo执行上下文 }; var foo = function (y) { var c = 5; console.log(a + c + y) }; bar(10); //2. 进入bar函数执行上下文(注:函数执行上下文对象在函数调用时产生,而不是函数声明时产生) </script> 复制代码
以上这种情况整个过程产生了3个执行上下文 调用一次函数产生一个执行上下文 如果在上面代码最后一行的 bar(10)
,再调用一次 bar(10)
,那么就会产生5个上下文。 因为第一个bar (10)
产生一个函数上下文 在 bar
函数中调用 foo
,又产生一个函数执行上下文。 那么现在又调用 bar(10)
,与上面一个样会产生两个上下文,加起来4个函数执行上下文。 最后加上 window
的全局变量上下文,一共五个执行上下文。
window
的全局上下文,然后执行
bar()
会把
bar
函数执行上下文压入栈中。
bar
中调用
foo
,把
foo
函数执行上下文压入栈中,
foo
函数执行完毕,释放,便会把
foo
函数执行上下文
pop
(推出来)。逐渐
bar
执行完毕,
pop
出
bar
函数执行上下文,最后只剩下
window
上下文。
注解2:假设一个情况: f1()
函数中会调用 f2()
和 f3()
函数。那么在当前时刻栈中可最多达到几个上下文? An: 当 f1()
执行,会先调用 f2()
,调用完后, f2()
已经完成了使命,它的生命周期就结束了,所以栈 就会释放掉他,在执行 f3()
,所以栈中也就最多三个上下文。 f3()
f1()
window
.
注解3:假设另一个情况: f1()
函数中会调用 f2()
, f2()
中会调用 f3()
函数。那么在当前时刻栈中可最多达到几个上下文 ? An: 当 f1()
执行,会先调用 f2()
,执行 f2()
时要调用 f3()
,所以,栈中可达到4个上下文。 f3()
f2()
f1()
window .
2.3.4 习题与案例
面试题1:执行上下文栈
- 依次输出什么?
- 整个过程中产生了几个执行上下文?
<script type="text/javascript"> console.log('global begin: '+ i); //undefined 变量提升 var i = 1; foo(1); function foo(i) { if (i == 4) { return; } console.log('foo() begin:' + i); foo(i + 1); console.log('foo() end:' + i); } console.log('global end: ' + i) //1 全局变量i,其他的函数中的i当执行结束后就销毁了。 复制代码
执行结果:
global begin: undefined foo() begin:1 foo() begin:2 foo() begin:3 foo() end:3 foo() end:2 foo() end:1 global end: 1 复制代码
一共产生5个上下文: 分析见下图,我画的很清楚了。这张图画了12min。主要就是入栈出栈,在出栈前,回溯原来的那个函数,那个函数执行上下文还在,如果还有 没有执行完的语句 会在这个时候执行。当剩余的语句已经执行完了,那么这个函数的执行上下文生命周期结束,释放出栈。想想我们递归调用去求阶乘的例子,思想是一样的。
面试题2:变量提升和函数提升(执行上下文)
function fn(a){ console.log(a); // 输出function a的源码,a此时是函数a var a = 2; function a(){ } console.log(a); // 2 } fn(1); 复制代码
考察点: 声明提前 难点: 函数优先
调用一开始, 就会先创建一个局部变量a, (因为a是形参), 然后把实参的值1赋值给 a
, a= 1
几乎在同一时刻,那么一瞬间,开始处理函数内变量提升和函数提升 此时, a
因为函数提升已经变成了 a = function(){}
以上这些过程都是函数执行上下文的预处理过程 接下来,才是正式执行内部函数的代码。 console.log(a);
此时输出的就是function源码 ƒ a(){}
结尾的输出语句便输出 a = 2
。
测试题1: [考查知识点]先预处理变量, 后预处理函数
function a() {} //函数提升 var a; //变量提升 //先预处理变量, 后预处理函数。也就是,函数提升会覆盖变量提升。 console.log(typeof a); //function 复制代码
测试题2:[考查知识点] 变量预处理, in操作符 (在window上能不能找到b,不管有没有值)
if (!(b in window)) { var b = 1; //在ES6之前没有块级作用域,所以这个变量b 相当于window的全局变量 } console.log(b); //undefined 复制代码
测试题3: [考查知识点]预处理, 顺序执行这个题笔者认为出的相当好。混乱读者的视角。当然再次强调,面试题是专门命题出来考查的,实际开发上可能有些不会这么用。但主要作用就是深入理解。
var c = 1; function c(c) { console.log(c); var c = 3; } c(2); //报错。 c is not a function //这个题包含了变量和函数声明提升的问题,就是等价于以下的代码: var c; //变量提升 function c(c) { //函数提升,覆盖变量提升 console.log(c); var c = 3; //函数内部的局部变量(在栈内存的封闭内存空间里,外面看不到) } c=1;//开始真正执行代码var c = 1 console.log(c); c(2); //c is not a function c是一个变量,值为number类型的数值.怎么可以执行? 复制代码
2.4 作用域与作用域链
2.4.1 作用域
1.理解:
- 作用域:就是一块"地盘",一块代码区域, 在编码时就确定了, 不会再变化(见下图解)定义函数变量时触发了作用域。执行结束完成作用域生命周期结束。
- 作用域链:多个嵌套的作用域形成的由内向外的结构, 用于查找变量(见下图解)
2.分类:
ES6
3.作用
- 作用域: 隔离变量, 可以在不同作用域去定义同名的变量,不会造成冲突。不同作用域下同名变量不会有冲突。例如,在全局中有一个变量
b
,那么在函数体中能不能有变量b
,当然可以,这就是分隔变量。 - 作用域链: 查找变量
给个案例:
var a = 10, b = 20 function fn(x) { var a = 100, c = 300; console.log('fn()', a, b, c, x) function bar(x) { var a = 1000, d = 400 console.log('bar()', a, b, c, d, x) } bar(100) bar(200) } fn(10); 复制代码
输出结果:
fn() 100 20 300 10 bar() 1000 20 300 400 100 bar() 1000 20 300 400 200 复制代码
4.作用域的图解如下:
2.4.2 作用域与执行上下文
1.区别1
- 全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时。
- 全局执行上下文是在全局作用域确定之后, js代码马上执行之前创建。
- 函数执行上下文是在调用函数时, 函数体代码执行之前创建。
2.区别2
- 作用域是静态的, 只要函数定义好了就一直存在, 且不会再变化。
- 执行上下文是动态的, 调用函数时创建, 函数调用结束时就会自动释放(不是通过垃圾回收机制回收)。
3.联系
- 执行上下文(对象)是从属于所在的作用域
- 全局上下文环境==>全局作用域
- 函数上下文环境==>对应的函数作用域
4.作用域与执行上下文图解如下:
2.4.3 作用域链
1.理解
- 多个上下级关系的作用域形成的链, 它的方向是从下向上的(从内到外)
- 查找变量时就是沿着作用域链来查找的
2.查找一个变量的查找规则
- a.在当前作用域下的执行上下文中查找对应的属性, 如果有直接返回, 否则进入b
- b.在上一级作用域的执行上下文中查找对应的属性, 如果有直接返回, 否则进入c
- c.再次执行2的相同操作, 直到全局作用域, 如果还找不到就抛出找不到的异常
3.作用域链的图解如下:
2.4.4 习题与案例
面试题1:作用域
<script type="text/javascript"> var x = 10; function fn() { console.log(x); //10 } function show(f) { var x = 20; f(); } show(fn); </script> 复制代码
记住: 作用域是代码一编写就确定下来了,不会改变。产生多少个作用域? n+1
. n
就是多少个函数, 1
就是指的是 window
。查找变量就是沿着作用域查找,而作用域是一开始就确定了,与哪里调用一点关系都没有。 见图解:
<script type="text/javascript"> var fn = function () { console.log(fn) //output: ƒ () {console.log(fn)} } fn() var obj = { fn2: function () { console.log(fn2) //报错,fn2 is not defined console.log(this.fn2)//输出fn2这个函数对象 } } obj.fn2() </script> 复制代码
报错原因:因为首先在这个匿名函数作用域找,找不到去上一层全局找,没找到,报错。找 fn2
是沿着作用域查找的! 输出 fn2
这个函数对象的原因:如果要找到 obj
属性 fn2
,则用 this.fn2()
,让其用 this
这个指针指向 obj
,在 obj
这个对象中找 fn2
属性。
面试题3:考察连续赋值隐含的含义
(function(){ var a = b = 3; })(); console.log(a); // 报错 a is not defined console.log(b); // 3 复制代码
理解:首先,赋值从右向左看。 b = 3
因为没var 所以相当于在全局作用域中添加 b
,赋值为 3
。现在。看前面 var a=
的部分,a有var,那么a就是局部变量放在栈内存的封闭内存空间上。 var a=b
, b
是变量,是基本数据类型的变量。它的内存中的内容值就是基本数据类型值,故拷贝一份给 a
。局部变量中 a=3
匿名函数自执行,会有一个函数执行上下文对象。当函数执行完成,就会把执行上下文栈弹出这个上下文对象。就再也访问不到。
所以,在函数自执行结束后,再执行输出 a
的语句。 a
压根就找不到,根本没定义。 b
因为是全局变量还是可以找到滴。
注意扩展,这里只有在非严格模式下,才会把 b
当做全局变量。若在ES5中严格模式下,则会报错。
面试4:考虑返回值问题
function foo1(){ return { bar: "hello" } } function foo2(){ return { bar: "hello" } } console.log(foo1()); // 返回一个对象 {bar:'hello'} console.log(foo2()); // undefined 复制代码
解释:因为 foo2
函数 return
后面少了分号,在js引擎解析时,编译原理的知识可知,在词法分析会 return
后面默认加上分号,所以,后面那个对象压根不执行,压根不搭理。所以啊,当 return
时返回的就是 undefined
。
面试5:函数表达式的作用域范围
<script> console.log(!eval(function f() {})); //false var y = 1; if (function f(){}){ y += typeof f; } console.log(y); // 1undefined </script> 复制代码
2.5 闭包
2.5.1 引入闭包
Code 1:
<button>测试1</button> <button>测试2</button> <button>测试3</button> <script type="text/javascript"> var btns = document.getElementsByTagName('button') //遍历加监听 for (var i = 0,length=btns.length; i < length; i++) { var btn = btns[i] btn.onclick = function () { alert('第'+(i+1)+'个') } } </script> 复制代码
输出结果:不管点击哪个 button
,都是输出“第4个”。因为 for
循环一下就执行完了,可是 btn.onclick
是要等到用户事件触发的,故这个时候 i
是 3
.永远输出“第4个”。 一些细节问题:在这个过程中,产生了多少个 i
?一个 i
, i
是全局变量啊。 事件模型的处理: 当事件被触发时,该事件就会对此交互进行响应,从而将一个新的作业(回调函数)添加到作业队列中的尾部,这就是js关于异步编程最基本的形式。 事件可以很好的工作于简单的交互,但将多个分离的异步调用串联在一起就会很麻烦,因为你必须要追踪到每个事件的事件对象(例如上面的 btn
).此外你还要确保所有的事件处理程序都能在事件第一次触发之前被绑定完毕。例如,若 btn
在 onclick
被绑定前点击,那么就不会有任何的事情发生。因此,虽然在响应用户交互或类似的低频功能时,事件很有用,但它面对更复杂的需求时仍然不够灵活。 所以,从这个例子不仅仅是对遍历加监听/闭包等理解。从这里也可以说明事件对象和事件机制的问题。所以,在ES6中会有 promise
和异步函数进行更多更复杂需求上的操作。
Code 2 通过 对象.属性
保存 i
:
<button>测试1</button> <button>测试2</button> <button>测试3</button> <script type="text/javascript"> var btns = document.getElementsByTagName('button') //遍历加监听 for (var i = 0,length=btns.length; i < length; i++) { var btn = btns[i] //将btn所对应的下标保存在btn上(解决方式1) btn.index = i btn.onclick = function () { alert('第'+(this.index+1)+'个') } } </script> 复制代码
这个时候就是我们想要的结果,点哪个 i
, button
就输出第几个。
Code 3 通过ES6的块级作用域 let
:
<button>测试1</button> <button>测试2</button> <button>测试3</button> <script type="text/javascript"> var btns = document.getElementsByTagName('button') //遍历加监听 for (let i = 0,length=btns.length; i < length; i++) { //(解决方式二) var btn = btns[i] btn.onclick = function () { alert('第'+(i+1)+'个') } } </script> 复制代码
在ES6中引入块级作用域,使用 let
即可。
Code 4 利用闭包解决
<button>测试1</button> <button>测试2</button> <button>测试3</button> <script type="text/javascript"> var btns = document.getElementsByTagName('button') //利用闭包 for (var i = 0,length=btns.length; i < length; i++) { //这里的i是全局变量 (function (j) { //这里的j是局部变量 var btn = btns[j] btn.onclick = function () { alert('第'+(j+1)+'个') //这里的j是局部变量 } })(i); //这里的i是全局变量 } </script> 复制代码
for
循环里有两个函数, btn.click
这个匿名函数就是一个闭包。它访问了外部函数的变量。 产生几个闭包?3个闭包(外部函数执行几次就产生几个闭包)。每个闭包都有变量 j
,分别保存着 j=0
, j=1
, j=2
的值。故可以实现这样的效果。之前之所以出问题,是因为都是用着全局变量的i,同一个i值。 闭包有没有被释放?没有,一直存在。我们知道闭包释放,那就是让指向内部函数的引用变量为 null
即可。但是此时 btn.onclick
一直引用这内部函数(匿名函数),故其闭包不会被释放。 闭包应不应该被释放?不应该。因为一个页面的一个 button
是要一直存在的,页面显示过程中, button
要一直关联着这个闭包。才能让每点击 button1
就 alert
(第1个)这样的结果。不可能让 button
点击了一次就失效吧。那么假设要释放这些闭包,那就让 btn.onclick=null
即可。 闭包的作用?延长局部变量 j
的生命周期。
2.5.2 理解闭包(Closure)
1.如何产生闭包?
- 当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时, 就产生了闭包。
- 注1:若外部函数有变量
b
,而内部函数中没有引用b
,则不会产生闭包。
2.闭包到底是什么?
闭包是指有权访问另一个函数作用域中的变量的函数。
可以理解为:
包含了那个局部变量的容器(不一定是对象,类似对象)
他被内部函数对象引用着
怎么判断闭包存在否?最终就是判断函数对象有没有被垃圾回收机制。
- 使chrome`可以调试查看闭包的存在
- 理解一: 闭包是嵌套的内部函数(绝大部分人)
- 理解二: 包含被引用变量(函数)的对象(极少数人)
- 若理解二请注意: 闭包存在于嵌套的内部函数中。
- 口语:首先明白闭包的本质上是一个对象,保存在内部函数中的对象,这个对象保存着被引用的变量。
- 在后台执行环境中,闭包的作用域包含着它自己的作用域、包含函数的作用域和全局作用域。
3.产生闭包的条件?
- 函数嵌套
- 内部函数引用了外部函数的数据(变量/函数)
- 执行外部函数(内部函数可以不执行)
案例1:
function fn1 () { var a = 2 var b = 'abc' function fn2 () { //执行函数定义就会产生闭包(不用调用内部函数) console.log(a) //引用了外部函数变量,若里面没有引用任何的外部函数变量(函数)则不会产生闭包 } // fn2() 内部函数可以不执行,也会产生闭包。只要执行了内部函数的定义就行。但若是函数表达式呢? } fn1(); //外部函数要执行哦,否则不会产生闭包 复制代码
案例2:
function fun1() { var a = 3 var fun2 = function () { console.log(a) } } fun1() //这样子通过函数表达式定义函数,若没有在里面调用内部函数,则不会产生闭包。 复制代码
案例3:
function fun1() { var a = 3 var fun2 = function () { console.log(a) } fun2() } fun1() //这样子通过函数表达式定义函数,但在里面调用了内部函数,那么这个情况是可以产生闭包的。 复制代码
函数表达式不同于函数声明。函数声明要求有名字,但函数表达式不需要。没有名字的函数表达式也叫做匿名函数(anonymous function),匿名函数有时候也叫拉姆达函数,匿名函数的 ·name· 属性是空字符串。
以上这些案例,只是辅助理解。并没有实际上的应用,下面就来说说闭包实际可以应用的地方。
2.5.3 常见的闭包
- 将函数作为另一个函数的返回值
- 将函数作为实参传递给另一个函数调用
案例1:将函数作为另一个函数的返回值
function fn1() { var a = 2 function fn2() { a++ console.log(a) } return fn2 } var f = fn1() f() // 3 f() // 4 复制代码
深入理解: 问题一:有没有产生闭包? An:条件一,函数的嵌套。外部函数 fn1
,内部函数 fn2
,条件一满足;条件二,内部函数引用了外部函数的数据(变量/函数)。 a
就是外部函数的数据变量。条件二满足。条件三,执行外部函数。 var f = fn1()
其中的 fn1()
是不是执行了,外部函数执行了。赋值给 f
变量是因为外部函数 fn1
在执行后返回一个函数,用全局变量 f
来保存其地址值。条件三满足。综上所述,产生了闭包。
问题二:产生了几个闭包? 产生了一个闭包。我们根据上一节的知识可知:执行函数定义就会产生闭包。那么执行函数定义是不是只要执行外部函数即可,因为外部函数一执行,就会有函数上下文对象,就会函数声明提前,也就是执行了函数定义。那么,这个时候执行了几次外部函数?是不是一次。执行了一次外部函数,也就是声明函数提前了一次,执行函数定义这个操作做了一次,故只产生了一个闭包。也可得出结论,外部函数执行几次,就产生几个闭包。跟内部函数执行几次没有关系(前提,在可以生成闭包的情况下)
问题三:调用内部函数为什么可以读出a的最新值? 从结果可以知道, f()
, f()
是不是在调用了两次内部函数,从输出的结果看, a
每次输出最新值。这就可以知道,在执行内部函数的时候, a
并没有消失。记住这点,这就是闭包的本质作用。
问题四:那么如果我现在在以上代码最后(分别输出 3
, 4
语句后面)继续加入
var h =fn1(); h(); f(); 复制代码
这个时候会输出什么? An: h()
会输出 3
. f()
会输出 5
. 因为: var h = fn1()
又执行了一次,h接收返回值函数对象(内部函数),也就是在这个时候产生了新的一个闭包。当调用内部函数时, h()
,就会有新的函数上下文对象产生, a
值就会从初始值开始记录。当调用 f()
时,这个时候还是在上一个闭包的状态下,那个作用域并没有消失,故还在原先的基础上改变 a
值。
案例2. 将函数的实参传递给另一个函数调用(★★★)
function showDelay(msg, time) { setTimeout(function () { alert(msg) }, time) } showDelay('my name is ly', 2000) 复制代码
这个例子说明了,我们要使用闭包不一定要 return
出去。只要这个函数对象被引用着就行。 return
的话那我再外面用变量接收一下就引用着了。但是我使用定时器,定时器模块式在浏览器分线程运行着的,定时器这个回调函数就是定时器模块保存管理着。
深入理解: 问题一:有没有产生闭包? An:条件一,函数的嵌套。外部函数 showDelay
,内部函数定时器的回调函数,条件一满足;条件二,内部函数引用了外部函数的数据(变量/函数)。 msg
就是外部函数的数据变量,而在回调函数中用了。注意, time
不是哦, time
还是在外部函数用的,在内部函数中并没有用到外部函数的time变量。是因为 msg
变量才满足条件二。条件三,执行外部函数。 showDelay('my name is ly', 2000)
执行了,外部函数执行了。但是注意回调函数没有声明提升,故还要等 2000ms
后触发进行调用回调函数。这个时候内部函数才执行。条件三满足(这个类似于函数表达式情况,如果是函数表达式,那么不仅仅要外部函数要执行,内部函数表达式定义的函数也要有执行,只有这样才会出现闭包。如果是函数声明定义的函数,那么就会有函数执行上下文去创建,函数提升,故在执行函数定义的时候就会出现闭包)。综上所述,产生了闭包。
2.5.4 闭包的作用
1.使用函数内部的局部变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期) 本来,局部变量的生命周期是不是函数开始执行到执行完毕,局部变量就自动销毁啦。但是,通过闭包可以延长局部变量的生命周期,函数内部的局部变量可以在函数执行完成后继续存活在内存中。那就是通过闭包,具体怎样的内部机制见下。
2.让函数外部可以操作(读写)到函数内部的数据(变量/函数) 本来,函数内部是可以通过作用域链去由内向外去找数据(变量/函数),是可以访问到函数外部的。但是反过来是不行的,函数外部能访问到内部的数据(变量/函数)吗?
function fun1() { var a='hello world' } console.log(a); //报错 a is not defined //函数外部不能访问函数内部的数据 复制代码
所以,函数外部不能访问函数内部的数据。但是,可以通过闭包去访问到函数内部的数据。具体的内部机制又是怎样的呢?见下。
//详解闭包的作用(重要) function fn1() { var a = 2 function fn2() { a++ console.log(a) } function fn3() { a-- console.log(a) } return fn3 } var f = fn1() f() // 1 f() // 0 复制代码
问题1. 函数 fn1()
执行完后, 函数内部声明的局部变量是否还存在? An: 一般是不存在, 存在于闭包中的变量才可能存在。像 fn2
fn3
变量就自动销毁了。因为函数内的局部变量的生命周期就是函数开始执行到执行完毕的过程。那像 fn2
这个函数对象也会被垃圾回收机制回收,因为没有变量去引用(指向) fn2
函数对象。但是 fn3
这个对象还在,根本原因是因为语句 var f = fn1();
fn( )
执行完毕返回一个 fn3
的地址值并且赋值给全局变量f,那么全局变量f就会指向他,所以,这个 fn3
这个函数对象不会被垃圾回收机制回收。 但我在回答这个问题时,是说存在于闭包中的变量才可能存在。为什么可能呢?因为如果我把语句 var f = fn1();
改成 fn1()
,这个时候还是没有变量去引用,所以这时还是会被回收的。见下图。
fn3
函数对象一直会有引用,闭包就会存在。这时我只要将
f=null
,这个时候
fn3
函数对象就没有被
f
引用,所以会被垃圾回收机制回收,故此时这个闭包死亡。
问题2:在函数外部能直接访问函数内部的局部变量吗?An: 不能, 但我们可以通过闭包让外部操作它.
2.5.5 闭包的生命周期
- 产生: 在嵌套内部函数定义执行完时就产生了(不是在调用)--->针对的是用函数声明的内部嵌套函数。
- 死亡: 在嵌套的内部函数成为垃圾对象时。
function fn1() { //此时闭包就已经产生了(函数提升, 内部函数对象已经创建了) var a = 2 function fn2 () { a++ console.log(a) } return fn2 } var f = fn1() f() // 3 f() // 4 f = null //闭包死亡(包含闭包的函数对象成为垃圾对象) 复制代码
2.5.6 闭包的应用
闭包应用:
jQuery
闭包的应用之一:定义JS模块
-
要具有特定功能的js文件
-
将所有的数据和功能都封装在一个函数内部(私有的)(函数内部会有作用域与作用域链的概念,函数内部的数据就是私有的,外部访问不到。)
-
只向外暴露一个包含n个方法的对象(暴露多个行为)或函数(暴露一个行为)
-
模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
自定义JS模块一:
function myModule() { //私有数据 var msg = 'Hello world'; //操作数据的函数 function doSomething() { console.log('doSomething() '+msg.toUpperCase()) } function doOtherthing () { console.log('doOtherthing() '+msg.toLowerCase()) } //向外暴露对象(给外部使用的方法) return { doSomething: doSomething, doOtherthing: doOtherthing } } //怎么使用?在html页面中 var module = myModule() module.doSomething() module.doOtherthing() 复制代码
自定义JS模块二:
(function () { //私有数据 var msg = 'My atguigu' //操作数据的函数 function doSomething() { console.log('doSomething() '+msg.toUpperCase()) } function doOtherthing () { console.log('doOtherthing() '+msg.toLowerCase()) } //向外暴露对象(给外部使用的方法) window.myModule2 = { doSomething: doSomething, doOtherthing: doOtherthing } })(); //怎么使用?在html页面中 myModule2.doSomething() myModule2.doOtherthing() //这种自定义模块相对而言更好,因为不需要先调用外部函数,直接使用 myModule2.doSomething()更加方便。 复制代码
2.5.7 闭包的缺点和解决
1.缺点
- 函数执行完后, 函数内的局部变量没有释放, 占用内存时间会变长(是优点亦是缺点)
- 容易造成内存泄露(注意和内存溢出的区别,见1.5.3节)
2.解决
- 能不用闭包就不用
- 及时释放
<script type="text/javascript"> function fn1() { var arr = new Array[100000] function fn2() { console.log(arr.length) } return fn2 } var f = fn1() f() //这里是有闭包的,arr一直没有释放,很占内存。 //如何解决?很简单。 f = null //让内部函数成为垃圾对象-->回收闭包 </script> 复制代码
-
理解:
- 当嵌套的内部函数引用了外部函数的变量时就产生了闭包
- 通过chrome工具得知: 闭包本质是内部函数中的一个对象, 这个对象中包含引用的变量属性
-
作用:
- 延长局部变量的生命周期
- 让函数外部能操作内部的局部变量
-
写一个闭包程序
function fn1() { var a = 2; function fn2() { a++; console.log(a); } return fn2; } var f = fn1(); f(); f(); 复制代码
-
闭包应用:
jQuery
-
缺点:
- 变量占用内存的时间可能会过长
- 可能导致内存泄露
- 解决:
- 及时释放 :
f = null;
//让内部函数对象成为垃圾对象
- 及时释放 :
2.6.9 习题与案例
面试1:考察闭包
function foo(){ var m = 1; return function (){ m++; return m; } } var f = foo(); //这会形成一个闭包 (调用一次外部函数) var f1 = foo(); //这会形成一个闭包 (调用一次外部函数) /*不同闭包有不同作用域。同一个闭包可以访问其最新的值。--这句话知识一个表面现象,结合上面的案例去发现深入的步骤,这个过程是如何执行的?*/ console.log(f()); // 2 console.log(f1()); // 2 console.log(f()); // 3 console.log(f()); // 4 复制代码
面试2:闭包相关知识
<script> function fun(n, o){ console.log(o); //实则就是输出闭包中的变量值,n是闭包引用的变量。延长的是n的变量生命周期。 return { fun: function (m){ return fun(m, n); } } } /*注意以上代码段是有闭包的,return fun(m,n)中的n是用到了外部函数的变量n*/ //测试一: var a = fun(0); // undefined a.fun(1); // 0 执行这里实则是产生了新的闭包,但没有变量去指向这个内部函数产生的闭包故马上就消失啦。 a.fun(2); // 0 a.fun(3); // 0 /*最后三行语句一直用的闭包是fun(0)产生的闭包*/ //测试二: var b = fun(0).fun(1).fun(2).fun(3); //undefined 0 1 2 /*产生了四个闭包,也就是外部函数fun(n,o)调用过4次。*/ // 测试三: var c = fun(0).fun(1); // undefined 0 c.fun(2) // 1 c.fun(3) // 1 /*最后两行语句一直用的闭包是fun(0).fun(1)产生的闭包,故其语句*/ </script> 复制代码
面试3:写一个函数, 使下面的两种调用方式都正确
console.log(sum(2,3)); // Outputs 5 console.log(sum(2)(3)); // Outputs 5 复制代码
答案:
<script> function sum(){ if(arguments.length == 2){ return arguments[0] + arguments[1]; }else if(arguments.length == 1){ var first = arguments[0]; return function (a){ return first + a; } } } </script> 复制代码
此文档为吕涯原创,可任意转载,但请保留原链接,标明出处。
文章只在CSDN和掘金第一时间发布:
CSDN主页: blog.csdn.net/LY_code
掘金主页: juejin.im/user/5b220d…
若有错误,及时提出,一起学习,共同进步。谢谢。 :stuck_out_tongue_closed_eyes::stuck_out_tongue_closed_eyes::stuck_out_tongue_closed_eyes:
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- Python 拓展之特殊函数(lambda 函数,map 函数,filter 函数,reduce 函数)
- Python 函数调用&定义函数&函数参数
- python基础教程:函数,函数,函数,重要的事说三遍
- C++函数中那些不可以被声明为虚函数的函数
- 017.Python函数匿名函数
- 纯函数:函数式编程入门
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。