1. 函数进阶
1.1. 定义函数的三种方式
1.函数声明 function:可以先调用再声明,因为预解析(把函数声明、变量声明进行提升)
function fn() {
//函数体
conle.log(1);
}
2.函数表达式:不可以先调用再声明,因为预解析只是把变量声明提升了,赋值留在原位。
var fn2 = function() {
conle.log(2);
}
fn(2);
3.函数也是对象,函数也是通过构造函数new出来的 Function(大写的)(了解,工作当中不会使用到)
var fn3 = new Function(arg1, arg2, arg3..., bodyFn);
参数:
- 参数的个数是若干个
- 参数的类型都是字符串类型的
- 除了最后一个参数bodyFn外,其他参数都是创建的函数的形参
- 最后一个参数bodyFn 是创建的函数的函数体
代码演示:
var fn4 = new Function('n1', 'n2', 'alert(n1 + n2)');
// 等价于以下代码:
var fn4 = function(n1, n2) {
alert(n1 + n2);
}
综合代码演示:
<body>
<script>
// 定义函数的三种方式
/*// 1. 函数声明 function
// 可以先调用在声明,因为预解析(把函数声明、变量声明进行提升)
fn();
function fn(){
// 函数体
console.log(1);
}
// 2. 函数表达式
// 不可以先调用在声明,因为预解析只是把变量声明提升了,赋值留在原位
fn2();
var fn2 = function () {
console.log(2);
}*/
// 3. 函数也是对象,函数也是通过构造函数new出来的 Function
// 语法: var fn3 = new Function(arg1, arg2, ...., bodyFn);
// 参数:
// 1. 参数的个数是若干个
// 2. 参数的类型都是字符串类型的
// 3. 除了最后一个参数bodyFn外,其他参数都是创建的函数的形参
// 3. 最后一个参数bodyFn是创建的函数的函数体
var fn4 = new Function("n1", "n2", "alert(n1 + n2)");
// 等价于以下代码
/*var fn4 = function(n1, n2){
alert(n1 + n2)
}*/
// console.log(fn4);
fn4(1, 2);
</script>
</body>
1.2 函数也是对象
1.2.1-函数的作用域链
1.2.2-函数的原型链
例:
function Person(){}
// 底层是通过 Function new 出来的
// var Person = new Function(); //Person是实例对象,是函数
Person的原型链:
Person ==> Function.prototype ==> Object.prototype ==> null
代码演示:
<body>
<script>
function Person(){}
// 底层是通过 Function new 出来的
// var Person = new Function(); // Person是实例对象,是函数
// Person的原型链:
// Person ==> Function.prototype ==> Object.prototype ==> null;
// console.log(Function.prototype.__proto__);
// console.log(Person.prototype);
console.dir(Function.prototype); // js中唯一的原型对象是个函数类型的
// 在Function.prototype中,看到有call、apply、bind三个方法
// 问函数是否可以使用call方法?可以,根据原型链来查找使用的。
</script>
</body>
1.2.3-Function.prototype常用成员
- call:调用函数,重新指向this
- apply:调用函数,重新指向this
- bind:重新指向this,返回一个新的函数,不调用
1.2.4-完整版原型链
核心的点:函数的双重身份,函数是函数,也是对象
1.函数作为构造函数来使用去创造对象
function Person(){
}
var p = new Person();
绘制原型三角关系:
- 构造函数:Person
- 原型对象:Person.prototype
- 实例对象:p
2.把函数当实例看,函数也是构造函数 Function 创建
底层就是 var Person = new Function();
绘制原型三角关系:
- 构造函数:Function
- 原型对象:Function.prototype
- 实例对象:Person
3.把Object考虑进来 ==> 当构造函数来使用
var obj = new Object();
绘制原型三角关系:
- 构造函数:Object
- 原型对象:Object.prototype
- 实例对象:obj
4.把Object当成实例对象来使用
任何函数底层都是 Function 创建的,都是Function的实例对象
底层 var Object = new Function();
绘制原型三角关系:
- 构造函数:Function
- 原型对象:Function.prototype
- 实例对象:Object
5.Function 当实例对象来看,谁把 Function 创建出来的
var Function = new Function();
绘制原型三角关系:
- 构造函数:Function
- 原型对象:Function.prototype
- 实例对象:Function
绘制完整版原型链的目的是辅助大家理解js中对象的继承关系:
小结:
- 任何对象的原型链上都有Object.prototype
- 任何函数的原型链上都有 Function.prototype
- 函数是一等公民
- typeof 函数==> function
- Function.prototype ==> 原型对象的类型是个函数
- Function 自己生自己
- 函数的双重身份
是函数,也是对象
是函数,有prototype属性
是对象,有 _ _ proto_ _ 属性
所以说函数有prototype属性,也有 _ _ proto_ _ 属性
综合代码演示:
<body>
<script>
// 从__proto__和prototype来深入理解JS对象和原型链
// https://github.com/creeperyang/blog/issues/9
// 王福朋 - 博客园
// https://www.cnblogs.com/wangfupeng1988
// 面试题
/*console.log(Function instanceof Function);
console.log(Function instanceof Object);
console.log(Object instanceof Object);
console.log(Object instanceof Function);*/
// 完整版原型链
// 核心点:函数的双重身份,函数是函数,也是对象
// 1. 函数作为构造函数来使用,去创建对象
function Person() {
}
var p = new Person();
// 绘制原型三角关系
// 构造函数:Person
// 原型对象:Person.prototype
// 实例对象:p
// 2. 把函数当实例对象来看,函数也是构造函数Function创建
// 底层就是 var Person = new Function();
// 绘制原型三角关系
// 构造函数:Function
// 原型对象:Function.prototype
// 实例对象:Person
// 3. 把Object考虑进来 ==> 当构造函数来使用
var obj = new Object();
// 绘制原型三角关系
// 构造函数:Object
// 原型对象:Object.prototype
// 实例对象:obj
// 4. 把Object当成实例对象来使用
// 任何函数底层都是Function创建的,都是Function的实例对象
// 底层 var Object = new Function();
// 绘制原型三角关系
// 构造函数:Function
// 原型对象:Function.prototype
// 实例对象:Object
// console.log( Object.__proto__ === Function.prototype );
// 5. Function 当实例对象来看,谁把Function创建出来的
// var Function = new Function();
// 绘制原型三角关系
// 构造函数:Function
// 原型对象:Function.prototype
// 实例对象:Function
// console.log(Function.__proto__ === Function.prototype);
// 小结:
// 1. 任何对象的原型链上都有Object.prototype
// 2. 任何函数的原型链上都有Function.prototype
// 3. 函数是一等公民
// 1. typeof 函数 ==> function
// 2. Function.prototype ==> 原型对象的类型是个函数
// 3. Function自己生自己
// 4. 函数的双重身份
// 是函数,也是对象
// 是函数,有prototype属性
// 是对象,有__proto__属性
// 所以说函数有prototype属性,也有__proto__属性
// console.log( Function.prototype === Function.prototype ); // true
// console.log( Object.__proto__ === Function.prototype ); // true
// console.log( Function.prototype.__proto__ === Object.prototype ); // true
// console.log( Object.prototype.__proto__ === Object.prototype ); // false
// console.log( Object.__proto__.__proto__ === Object.prototype ); // true
</script>
</body>
练习题1:(看图就可以完全答出来)
console.log( Function.prototype === Function.prototype ); // true
console.log( Object.__proto__ === Function.prototype ); // true
console.log( Function.prototype.__proto__ === Object.prototype ); // true
console.log( Object.prototype.__proto__ === Object.prototype ); // false
console.log( Object.__proto__.__proto__ === Object.prototype ); // true
1.2.5-练习题
<body>
<script>
/*function Person() {}
var p = new Person()
console.log(p.constructor); // Person
console.log(Person.constructor); // Function*/
// 2.
// 构造函数Function.prototype 是否在Function实例对象的原型链上
// console.log(Function instanceof Function); // true
// console.log(Function instanceof Object); // true
// console.log(Object instanceof Object); // true
// console.log(Object instanceof Function); // true
console.log( Function instanceof Array ); // false
console.log( Array instanceof Function ); // true
</script>
</body>
1.2.6-instanceof
instanceof:实例对象
语法:
对象 instanceof 构造函数
作用:
- 字面上理解:对象是否是构造函数的实例对象,如果是的,就返回 true
- 从原型链角度来理解:
- 构造函数的prototype属性是否在对象的原型链上,如果在 返回true,否则返回false
代码演示:
<body>
<script>
// instanceof
// instance 实例对象
// 语法:对象 instanceof 构造函数
// 作用:
// 字面上理解:对象是否是构造函数的实例对象,如果是的,返回true
// 从原型链角度来理解:
// 构造函数的prototype属性是否在对象的原型链上,如果在返回true,否则返回false
var arr = [10, 20];
// arr的原型链:
// arr ==> Array.prototype ==> Object.prototype ==> null;
console.log( arr instanceof Array ); // true
console.log( arr instanceof Object ); // true
</script>
</body>
1.3 作用域
- 变量可以起作用的区域(就是说一个变量声明定义好了,就可以在哪些区域内使用)
- 在js中,有全局作用域和函数作用域(词法作用域,静态作用域)
函数作用域:当函数声明定义好了,其函数作用域就定下来了,其作用域链也定下来了
作用域链:任何函数可以形成作用域,函数嵌套在另外一个函数中,那么外层函数也有自己的作用域,这样从里到外直到全局作用域,形成的链式结构叫做作用域链。
每一个函数的作用域链:都是从当前函数作用域出发,从里到外形成
代码演示:
<body>
<script>
// 作用域:变量可以起作用的区域(就是说:一个变量声明定义好了,可以在哪些区域内使用)
// 在js中,有全局作用域和函数作用域(词法作用域、静态作用域)
// 函数作用域: 是当函数声明定义好了,其作用域就定下来了。
function fn() {
var num = 10;
function inner(){
function bar(){
}
}
}
console.log(num);
// 作用域链:任何函数可以形成作用域,函数嵌套在另外一个函数中,外层函数也有自己的作用域,这样从里到外,直到全局作用域,形成的链式结构叫做作用域链。
// 每一个函数作用域链:都是从当前函数作用域出发,从里到外形成
// bar函数的作用域链: bar函数作用域 ==> inner函数作用域 ==> fn函数作用域 ==> 全局作用域
// inner函数的作用域链: inner函数作用域 ==> fn函数作用域 ==> 全局作用域
</script>
</body>
1.3.1-变量的搜索原则
沿着作用域链来查找
- 首先会在当前作用域内查找是否有声明该变量,如果有就返回变量的值
- 如果没有,就去外层作用域中查找
- 如果外面也没有,就沿着作用域链继续往外找,直到全局作用域,如果有就返回
- 如果还没有,报错。(属性值找不到就是undefined,变量找不到就报错)
代码演示:
<body>
<script>
// 变量的搜索原则 ==> 沿着作用域链来查找
// 1. 首先会在当前作用域内查找是否有声明该变量,如果有就返回变量的值
// 2. 如果没有,就去外层作用域中查找,如果有就返回变量的值
// 3. 如果也没有,就沿着作用域链继续往外找,直到全局作用域,,如果有就返回变量的值
// 4. 如果还没有,报错。
var num = 888;
function outer(){
function inner() {
var num;
function fn(){
console.log(num);
}
fn()
}
inner()
}
outer()
</script>
</body>
1.3.2-练习题
<body>
<script>
// 1.
/*var num = 10;
fn1();
function fn1() {
console.log(num); // undefined
var num = 20;
console.log(num); // 20 20
}
console.log(num); // 10 10*/
// 代码在执行之前,先要经过预解析
// 函数内的代码在执行之前也要经过预解析
// 预解析
/*function fn1() {
var num; // undefined
console.log(num); // undefined
num = 20; // 修改的是局部num为20
console.log(num); // 20
}
var num;
num = 10; // 修改全局num为10
fn1();
console.log(num); // 10*/
// 2.
/*var num = 10;
fn1();
function fn1() {
console.log(num); // 找的是全局num 10
num = 20; // 修改的是全局的num 为 20
console.log(num); // 20
}
console.log(num); // 20*/
// 3. 经典
/*var num = 123;
// 核心的点:
// 函数写好了,其函数作用域就定下来了,也就是函数作用域链就定下来了。
// 变量的查找遵循变量的搜索原则(沿着作用域链来查找)
function f1() {
console.log(num);
}
function f2() {
var num = 456;
f1();
}
f2(); // 打印啥?*/
// 修改以上代码
/*var num = 123;
function f1(num) {
// 函数形参的理解:是函数内部声明定义的局部变量
// var num = 实参; // 456
console.log(num); // 456
}
function f2() {
var num = 456;
f1(num);
// f1(num)调用是传递的参数是实参,实际的值,找,找的是f2的局部num的值
// 等价于 f1(456)
}
f2(); // 打印啥?*/
// 4
var num1 = 10;
var num2 = 20;
function fn(num1) {
// var num1 = 实参undefined;
// var num3; // 预解析
num1 = 100; // 局部的num1 为 100
num2 = 200; // 修改全局的num2 为 200
num3 = 300; // 局部的num3 为 300
console.log(num1); // 100
console.log(num2); // 200
console.log(num3); // 300
var num3;
}
fn();
console.log(num1); // 10
console.log(num2); // 200
console.log(num3); // error
// 考虑的点:
// 预解析(函数内的代码也要预解析)
// 函数内的形参
// 函数作用域什么时候定下来 ==> 函数写好之后就定下来了。
</script>
</body>
几种特殊的this指向
- 定时器中的this指向了window,因为定时器的function最终是由window来调用的。
- 事件中的this指向的是当前的元素,在事件触发的时候,浏览器让当前元素调用了function
1.4 函数的四种调用模式
分析this指向的问题:
- 任何函数都有属于自己的this指向。
- this指向是动态灵活的,只有当函数调用的时候,才能确定下来this的指向
- 如何去分析this指向:
- 看这个this属于哪个函数
- 看这个函数是如何调用的 ==> 就能决定下来this指向谁
函数的四种调用模式:
第一种:函数调用模式
这种调用模式,函数内的this指向window。
第二种:构造函数调用模式
new + 构造函数():这种调用模式,构造函数的this指向新创建出来的实例对象
第三种:方法调用模式
对象.方法名():这种调用模式,方法内的this指向调用方法的那个对象
谁调用,this指向谁
是点语法和中括号语法,都是方法调用模式
第四种:上下文调用模式 ==> call()、apply()、bind() 这三个方法
综合代码演示:
<body>
<script>
// 函数的四种调用模式 ==> 分析this指向的
// 1. 任何函数都有属于自己的this指向。
// 2. this的指向是动态灵活的,只有当函数调用的时候,才能确定下来this的指向
// 3. 如何去分析this指向
// 1. 看这个this属于哪个函数
// 2. 看这个函数是如何调用的 ==> 就能决定下来this指向谁。
// 函数的四种调用模式
// 1. 函数调用模式
/*function fn(){
console.log(this);
}
// 函数名(); 这种调用模式属于函数调用模式,函数内的this指向window
fn();*/
// 2. 构造函数调用模式
/*function Person(){
this.name = "lw";
console.log(this);
}
// new 构造函数();这种调用模式属于构造函数调用模式,构造函数中的this指向新创建出来的实例对象
var p = new Person();*/
// 3. 方法调用模式
var obj = {
a: "abc",
fn: function () {
console.log(this);
}
}
// 对象.方法名(); 这种调用模式属于方法调用模式,方法内的this指向调用方法的那个对象
// 谁调用,this指向谁
// 是点语法和中括号语法,都是属于方法调用模式
obj.fn();
obj["fn"](); // [] 中是字符串或数值
// 练习题:
function foo(){
console.log(this);
}
var arr = [10, 20, foo];
// 方法调用模式 ==> foo是被arr数组调用的,所以foo内的this指向了arr
// 怎么写的就怎么来分析
// arr[2](); // arr[2] ==> foo() 思考方式不对
/*var arr = {
0: 10,
1: 20,
2: foo
}
arr[2]();*/
//4. 上下文调用模式 ==> call apply bind
</script>
</body>
1.4.1-this练习题
<body>
<script>
// 1
/*var age = 38; // 全局变量 ==> 等价于 window的属性 window.age = 38;
var obj = {
age: 18,
getAge: function() {
console.log(this.age);
}
}
var f = obj.getAge; // 仅仅是把getAge方法赋值给了变量f
// 函数调用模式,函数内的this,this指向window
f(); // 怎么写的怎么分析*/
// 2.
/*var age = 38;
var obj = {
age: 18,
getAge: function() {
// 方法内的this指向obj
console.log(this.age); // 18
function foo() {
// 1. 任何函数都有属于自己的this
// foo内的this和getAge方法内的this不是一个,之间没有关系
console.log(this.age); // 38
}
// 函数调用模式,
foo();
}
}
// 方法调用模式
obj.getAge();
obj["getAge"]();*/
// 3
var length = 10
function fn() {
console.log(this.length)
}
var obj = {
length: 5,
method: function(fn) {
// 函数调用模式 fn函数内的this指向window
fn(); // 10
// arguments 伪数组 实参列表 ==>类似于这样 [fn, 10, 5]
// 方法调用模式,fn方法被arguments调用,fn内的this指向arguments
// console.log(arguments);
arguments[0]();
}
}
obj.method(fn, 10, 5);
// 10 3
</script>
</body>
1.4.2-上下文调用模式
作用:可以自己去取指定this指向
call()、apply()、bind() 三个方法,任何函数都可以去使用以上三个方法
1.4.2.1-call()方法
1.除了小括号可以调用函数外,还可以使用call()调用函数
演示:
function fn() {
conle.log(111);
}
fn();
fn.call();
2.call的第一个参数可以用来修改函数内的this指向
演示:
function fn(2) {
conle.log(2);
conle.log(this);
}
fn2(); // 2 window
fn.call([10, 20, 30]); // 2 [10, 20, 30]
3.call的参数是若干个
第一个参数 ==> 用来修改this指向
除了第一个参数之外的所有其他参数 ==> 都是用来给函数传递实参的
演示:
function fn3(n1, n2) {
conle.log(this);
conle.log(n1 + n2);
}
// fn3(10, 20)
fn3.call([], 100, 200); //
作用:
- 调用函数
- 第一个参数来改this指向
- 给函数传递实参 ==> 除第一个以外的所有参数
综合代码演示:
<body>
<script>
// 上下文调用模式, 可以自己去指定this指向
// call apply bind三个方法
// 任何函数都可以去使用以上三个方法。
// call方法
// 1. 除了小括号可以调用函数外,还可以使用call调用函数
/*function fn(){
console.log(1111);
}
fn();
fn.call();*/
// 2. call的第一个参数可以用来修改函数内的this指向
/*function fn2(){
console.log(2);
console.log(this);
}
// fn2(); // 2 window
fn2.call( {name: "lw"} );*/
// 3. call的参数是若干个,
// 第一个参数 ==> 用来修改this指向
// 除了第一个参数之外的所有其他参数 ==> 都是用来给函数传递实参的
function fn3(n1, n2){
console.log(this);
console.log(n1 + n2);
}
// fn3(10, 20);
fn3.call([], 100, 200);
// call的作用
// 1. 调用函数
// 2. 第一个参数来改this指向
// 3. 给函数传递实参 ==> 除第一个以外的所有参数
</script>
</body>
1.4.2.2-方法借用(调)模式
上下文调用模式又称方法借用(调)模式
代码演示:
<body>
<script>
// 上下文调用模式又称方法借用(调)模式
var dfg = {
car: "加长版林肯",
liaomei: function () {
console.log("hello 啦, 妹子,哥有 " + this.car + ", 走啊,带你回家啊!!!");
}
}
dfg.liaomei();
var boge = {
car: "小黄车"
}
// boge.liaomei(); // error
// boge找dfg去借用liaomei方法
dfg.liaomei.call(boge);
dfg.liaomei.apply(boge);
// 等价于boge.liaomei()的效果
// 可以理解为第一个参数去借用了方法。
// 1. call可以调用liaomei方法
// 2. call的第一个参数把liaomei方法内的this修改指向boge
</script>
</body>
1.4.2.3-伪数组与数组
伪数组与数组最大的区别:伪数组不能使用数组的方法,但可以让伪数组去借用数组的方法
伪数组定义:是个对象,有数字下标还有length属性,所以可以和数组一样进行循环,但是伪数组不能使用数组的方法。
常见的伪数组:
- arguments实参列表
- querySelectorAll()、getElementByTagName() 获取到的元素
- jq对象
例:
// 需求:给obj伪数组添加 2: '波哥'
var obj = {
0: '班班',
1: '大飞哥',
length: 2
}
// 这是常规做法(第一种方法)
// obj[2] = '波哥';
// 还需要手动维护length
// obj.length++;
// conle.log(obj);
// Array.prototype.push:是从数组中的原型对象中去拿到push
// [].push ==> 从空数组上去获取到push方法
// 第二种:
// 数组的原型对象:
// Array.prototype.push.call(obj, '波哥'); //效果等价于:obj.push('波哥')
// 第三种:
;[].push.call(obj, '波哥', '啦啦', 'kuqi');
conle.log(obj);
注意点:
-
[].push 简化写法,一定要注意: {} 和 [] 一定要使用 ; 分号隔开,否则语法报错
-
数组的push方法不仅有添加功能,内部还会自动的去维护length属性
代码演示:
<body>
<script>
// 伪数组与数组
// 伪数组与数组最大的区别:伪数组不能使用数组的方法,但可以让伪数组去借用数组的方法。
// 伪数组定义:是个对象,有数字下标和length属性,所以可以和数组一样进行循环,但是伪数组不能使用数组的方法。
// 常见的伪数组
// 1. arguments 实参列表
// 2. querySelectorAll() getElementsByTagName() 获取到的元素
// 3. jq对象
// 需求:给obj伪数组添加 2: "波哥"
var obj = {
0: "班班",
1: "大飞哥",
length: 2
}
/*// 常规做法:
obj[2] = "波哥";
// 还需要手动维护length
obj.length++;
console.log(obj);*/
/*var arr = [10, 20, 30];
// arr.0 error
arr[0]*/
// obj.push("波哥"); // 直接使用无法,但是可以借用push方法
// 找数组去要push方法
// 数组的原型对象
// Array.prototype.push ==> 是从数组的原型对象中去拿到push
// [].push ==> 从空数组上去获取到push方法
// Array.prototype.push.call(obj, "波哥", "xm", "xh", "lw");
// 简化以上写法
;[].push.call(obj, "波哥", "xm", "xh", "lw", "aa", "bb");
// 效果等价于:obj.push("波哥", "xm", "xh", "lw");
console.log(obj);
// 注意点:
// 1. [].push 简化写法,一定要注意:{} 和 [] 一定要使用; 隔开,否则语法错误
// 2. 数组的push方法不仅有添加功能,内部还会自动的去维护length属性
</script>
</body>
1.4.2.4-伪数组借用数组的方法
代码演示:
<body>
<script>
var obj = {
0: "班班",
1: "大飞哥",
2: "波哥",
3: "xm",
length: 4
}
// 把obj伪数组里面的每一项拼接成字符串 "班班-大飞哥-波哥-xm"
var str = [].join.call(obj, "❥(^_-)❥");
// 等价于:obj.join("-");
console.log(str);
</script>
</body>
1.4.2.5-apply方法
call方法和apply方法作用是一样的,写法上有区别
一样的参数:
- 可以调用函数
- 可以修改this指向
- 可以给函数传递实参
apply的语法
apply(thisArg, [实参列表])
- thisArg用来修改函数内的this指向
- 实参列表:是个数组或者伪数组,里面的就是传递给函数的所有实参
apply的平铺性:apply会把第二个参数里面的每一项取出来作为实参给函数传递过去。
代码演示:
<body>
<script>
// apply方法
// call方法和apply方法作用是一样的,写法上有区别
// 一样的作用:
// 1. 可以调用函数
// 2. 可以修改this指向
// 3. 可以给函数传递实参 (写法上有区别)
// apply的语法
// apply(thisArg, 实参列表)
// 1. thisArg 来修改函数内的this指向
// 2. 实参列表 是个数组或伪数组,里面的就是传递给函数所有的实参
/*function fn(){
console.log(1);
}
fn();
fn.call();
fn.apply();*/
/*function fn2(){
console.log(2);
console.log(this);
}
fn2.call({name: "call"});
fn2.apply({name: "apply"});*/
function fn3(n1, n2){
console.log(this);
console.log(n1 + n2);
console.log(arguments);
}
// fn3.call([], 1, 2, 3, 4, 5, 6);
fn3.apply({}, [1, 2, 3, 4, 5, 6]);
// apply的平铺性:apply会把第二个参数里面的每一项取出来作为实参给函数传递过去。
</script>
</body>