JS高级--(原型和原型链,函数四种调用模式,this的指向,箭头函数)

327 阅读5分钟

原型

1.原型上存放函数

  1. 解决了同一个 say 浪费 内存的问题
  2. 解决了污染全局变量的问题
    function createStudent(name, age) {
      this.name = name;
      this.age = age;
    }
    // 将刚才的全局函数say 直接挂载到 构造函数的原型上 即可
    // prototype 是个对象 每一个构造函数都会内置有的. 我们称之为原型
    createStudent.prototype.say = function () {
      console.log(this.name);
    }
​
    const obj = new createStudent("悟能", 83);
    const obj1 = new createStudent("悟能1", 84);
​
    console.log(obj.say === obj1.say); // true 

2.原型解释

  • 原型的单词是 prototype, 原型的这个名字是行业内共同认可的名字。
  • 原型本质是一个对象,理解为 JavaScript 自动帮我们添加的,只要是构造函数,系统会默认的为构造函数关联一个对象,这个对象就称为构造函数的原型,写在原型中的成员,可以被构造函数所创建的实例调用
  • 原型是 JavaScript 自动帮我们在定义构造函数的时候添加的
  • 所有构造函数的实例,共享一个原型
  • 原型上一般是挂载函数

image-20200713153918181.png

3.构造函数的原型

<script>
  // 原型是什么:系统会为构造函数默认关联一个对象,这个对象就是构造函数的原型
  // 原型如何操作:
  // 与之前的对象的操作方式一样:添加成员,删除成员,遍历
  // 通过  构造函数.prototype 访问原型对象
  // 原型谁可以使用,如何使用:只要这个构造函数所创建的对象都是访问该构造函数的原型

  function Student(name, age) {
    // 添加属性
    this.username = name
    this.userage = age
  }
  // 为确知函数的原型添加成员
  Student.prototype.say = function() {
    console.log(this.username)
  }

  let obj = new Student('jack', 20)
  console.log(obj)
  let obj2 = new Student('rose', 20)
  console.log(obj2)
  // 通过构造函数创建的对象要吧访问原型中的成员
  console.log(obj.say === obj2.say)
  obj.say()
</script>

原型的关系

构造函数、实例、原型三者之间的关系

构造函数:构造函数就是一个函数,配合new可以新建对象。

实例:通过构造函数实例化出来的对象我们把它叫做构造函数的实例。一个构造函数可以有很多实例。

原型:每一个构造函数都有一个属性prototype,函数的prototype属性值就是原型。通过构造函数创建出来的实例能够直接使用原型上的属性和方法。

image.png

所有的构造函数都是Function的实例

Array 和 Person 和 Date 等都是 Function的实例

image-20200714203717916.png

Function 和 Object的关系

有人说 JavaScript 是作者花了7天时间写出来的产物 - 不完美

console.log(Object.prototype===Function.prototype.__proto__)

image-20200714204744760.png

Object的顶端呢?

接近顶峰了

 console.log(Object.prototype.__proto__ === null);

image-20200714205012513.png

5.原型链

1概念

任何一个对象,都有原型对象,原型对象本身又是一个对象,所以原型对象也有自己的原型对象,这样一环扣一环就形成了一个链式结构,我们把这个链式结构称为:原型链。

proto.png

  • 总结:Object.prototype是原型链的尽头,Object.prototype的原型是null。

完整原型三角关系

image.png

image.png

属性查找原则

如果是获取操作

  1. 会先在自身上查找,如果没有
  2. 则根据__proto__对应的原型去找,如果没有
  3. 一直找到Object.prototype,如果没有,那就找不到从而报错

5.继承

<script>
  function Person(name, age) {
    // 添加属性
    this.username = name
    this.userage = age
  }
  // 为确知函数的原型添加成员
  Person.prototype.say = function() {
    console.log(this.username)
  }

  function Student(name, age, gender) {
    this.username = name
    this.userage = age
    this.usergender = gender
  }
  // 缺点:繁琐
  // Student.prototype.say = Person.prototype.say
  // Student.prototype.hello = Person.prototype.hello
  // 缺点
  // 1.会将Student构造函数的原型进行覆盖,覆盖掉之前的内容
  // 2.修改对象的构造函数的指向
  Student.prototype = Person.prototype
  Student.prototype.constructor = Student

  // 实现继承:获取到Person构造函数原型中的成员添加到自身

  let stu = new Student('rose', 20, '女')
  let stu1 = new Student('tom', 20, '男')
  stu.say()
  stu1.say()
  console.log(stu.__proto__.constructor)

  let per = new Person()
  console.log(per.__proto__.constructor)
</script>

6.原型继承-一个细节

<script>
  function Person(name, age) {
    // 添加属性
    this.username = name
    this.userage = age
  }
  // 为确知函数的原型添加成员
  Person.prototype.say = function() {
    console.log(this.username)
  }

  function Student(name, age, gender) {
    this.username = name
    this.userage = age
    this.usergender = gender
  }

  // 重置原型对象实现继承
  Student.prototype = Person.prototype
  Student.prototype.constructor = Student

  // 实现继承:获取到Person构造函数原型中的成员添加到自身
  // 一个对象所指向的原型是:创建这个对象时,构造函数所指向的原型
  let stu = new Student('rose', 20, '女')
  stu.say()
</script>

7.继承另外一种实现方式

<script>
  function Person(name, age) {
    // 添加属性
    this.username = name
    this.userage = age
  }
  // 为确知函数的原型添加成员
  Person.prototype.say = function() {
    console.log('Person的', this.username)
  }
  Person.prototype.hello = function() {
    console.log(this.age)
  }

  function Student(name, age, gender) {
    this.username = name
    this.userage = age
    this.usergender = gender
  }
  Student.prototype.say = function() {
    console.log('Student的', this.username)
  }

  // 原型是一个对象
  for (key in Person.prototype) {
    // 判断 自身是否有同名键,如果有则跳过,没有才添加
    if (!Student.prototype[key]) {
      // if (!Student.prototype.hasOwnProperty(key)) {
      Student.prototype[key] = Person.prototype[key]
    }
  }

  let stu = new Student('rose', 20, '女')
  let stu1 = new Student('tom', 20, '男')
  stu.say()
  stu1.say()
  console.log(stu.__proto__.constructor)

  let per = new Person()
  console.log(per.__proto__.constructor)
</script>

二、作用域及作用域链

1.作用域的基本表现

let所创建的变量的作用域是从创建这个let变量到它所在的结构的}结束

作用域:变量起作用的区域,也就是说:变量定义后,可以在哪个范围内使用该变量。

<script>
  let num = 11 //全局变量
  function fn() {
    let num1 = 22 //局部变量,只有方法体内可以使用
    console.log(num) // 函数内部可以使用函数外部声明的变量,全局变量在任何地方都能访问到
    console.log(num1)
  }
  fn()
  console.log(num)
</script>

2.词法作用域

在js里只有全局作用域和函数作用域。

函数作用域是在函数定义的时候作用域就确定下来了,和函数在哪调用无关。

  • 我们只关注函数的定义位置而不关注函数的调用位置

    • 定义函数,函数的作用域就确定了
    • 以后函数做为参数,不影响函数的作用域
<script>
  // 词法作用域:函数一旦声明,作用域就确定好了
  // 函数的作用域只是声明有关,与调用无关,函数不关注在什么位置调用,只关注在什么位置声明
  var num = 123
  function f1() {
    console.log(num) // 123
  }

  function f2() {
    var num = 456
    f1()
  }
  f2()
</script>

3. var,let,const的区别

<script>
  // 能使用const就使用const
  // 不能使用const,就使用let
  // 不要使用var

  // let 的使用
  // 会有块级作用域
  // 块就是指 {   }
  // 作用域就是声明这个let变量的{}
  // 有效作用域:从let开始到它所在的}结束
  // 不会进行声明的提升,一定要先声明赋值再使用
  // if (1) {
  //   // console.log(age) // Cannot access 'age' before initialization
  //   let age = 20
  //   console.log(age)
  // }
  // console.log(age) // age is not defined

  // var:
  // 没有块级作用域的概念
  // 会进行声明的提升,将声明提升到当前作用域的最前面
  // var age
  // if (1) {
  //   console.log(age)
  //   age = 20
  //   console.log(age)
  // }
  // console.log(age)

  // const:定义常量:不能修改的变量:一般在模块中的成员都是常量
  // 1.基本特性和let一样
  // 2.声明赋值之后不能再修改
  // 3.只能在声明的同时进行赋值
  // if (1) {
  //   console.log(age)
  //   const age = 20
  // }

  // 常量名称一般全部大写
  const PI //  Missing丢失 initializer初始操作 in在 const常量 declaration声明
  PI = 3.14
  // PI = 3.15// Assignment赋值 to给 constant常量 variable变量
  console.log(pi)
</script>

4. var在业务处理中的细节

<body>
    <p>第1个p元素</p>
    <p>第2个p元素</p>
    <p>第3个p元素</p>
    <p>第4个p元素</p>

    <script>
      let ps = document.querySelectorAll('p')

      for (let i = 0; i < ps.length; i++) {
        // 同步任务
        ps[i].addEventListener('click', function() {
          // 异步任务
          ps[i].style.color = 'red'
        })
        console.log(i)
      }
    </script>
</body>

5. 作用域的练习

<script>
  // var num = 10
  // fn1()
  // function fn1() {
  //   var num
  //   // var会将声明提升到当前作用域(全局作用域,函数作用域)的最前面
  //   console.log(num) // undefined
  //   // 自己的作用域有则使用自己的,否则 往外找
  //   num = 20
  //   console.log(num) // 20
  // }
  // // 函数 外部不能直接访问函数内部所声明的成员
  // console.log(num) // 10

  // var num = 10
  // fn1()
  // function fn1() {
  //   // 自己的作用有域没有,则找外部作用域
  //   console.log(num) // 10
  //   // 自己的作用有域没有,则找外部作用域,修改了全局变量
  //   num = 20
  //   console.log(num) // 20
  // }
  // console.log(num) // 20

  // var num = 123
  // // num是一个形参,相当于方法的局部变量
  // // 函数的作用域只与函数的声明位置有关,与调用位置无关
  // function f1(num) {
  //   // 先使用自己的作用域的num
  //   console.log(num) // 456
  // }

  // function f2() {
  //   var num = 456
  //   f1(num)
  // }
  // f2()

  // var num1 = 10
  // var num2 = 20
  // function fn(num1) {
  //   var num3 // 提升到函数作用域的最前面
  //   num1 = 100 // 修改=的是形参,方法的局部变量,与全局的没有关系
  //   num2 = 200 // 自己没有num2,所以查找外层作用域,修改的是全局变量num2
  //   num3 = 300 // 修改了自己的num3
  //   console.log(num1) // 100
  //   console.log(num2) // 200
  //   console.log(num3) // 300
  // }
  // fn()
  // console.log(num1) // 10
  // console.log(num2) // 200
  // console.log(num3) // 报错
</script>

this与函数的四种调用模式

根据函数内部this的指向不同,可以将函数的调用模式分成4种

  1. 函数调用模式
  2. 方法调用模式
  3. 构造函数调用模式
  4. 上下文调用模式(借用方法模式)

函数调用模式

如果一个函数不是一个对象的属性时,就是被当做一个函数来进行调用的。此时this指向了window

function fn(){
  console.log(this);// 指向window 
}
fn();

方法调用模式

当一个函数被保存为对象的一个属性时,我们称之为一个方法。当一个方法被调用时,this被绑定到当前对象

const obj = {
  sayHi:function(){
    console.log(this);//在方法调用模式中,this指向调用当前方法的对象。
  }
}
obj.sayHi();

构造函数调用模式

如果函数是通过new关键字进行调用的,此时this被绑定到创建出来的新对象上。

function Person(){
  console.log(this);
}
Person();//this指向什么?
var p = new Person();//this指向什么?

调用模式真题

<script>
  // var声明的变量会挂载到全局window
  // let age = 38
  // var obj = {
  //   age: 18,
  //   getAge: function() {
  //     console.log(this)

  //     console.log(this.age) // 38
  //   }
  // }

  // var f = obj.getAge
  // f() // 函数调用模式,说法中的this指向window

  // var age = 38
  // var obj = {
  //   age: 18,
  //   getAge: function() {
  //     console.log(this.age) // 18
  //     function foo() {
  //       console.log(this.age) // 38
  //     }
  //     foo() // 函数调用模式 -- window
  //   }
  // }
  // obj.getAge() // 方法调用模式 -- obj

  var length = 10

  function fn() {
    console.log(this.length) // 10  3
  }
  var obj = {
    length: 5,
    method: function(fn) {
      fn() // 函数调用模式,this指向window
      // obj.a >> obj['a']
      arguments[0]() // 方法调用模式  arguments.0()
      // js的参数是通过内置的Arguments进行接收的,它是一个伪数组(对象)
      console.log(arguments)
    }
  }
  obj.method(fn, 10, 5, 123, 12, 323, 4)
</script>

继承构造函数

<script>
  function Person(name, age) {
    this.username = name // 房子
    this.userage = age // 车子
  }

  // 为确知函数的原型添加成员
  Person.prototype.say = function() {
    console.log(this.username)
  }

  function Student(name, age, gender) {
    // this.username = name // 房子
    // this.userage = age // 车子
    // 让父类构造函数帮我构建这两个成员
    // Person(name, age)// 函数调用模式,this指向window
    Person.call(this, name, age) 

    this.usergender = gender // 你自己
  }

  // 继承原型中的所有成员
  Student.prototype = Person.prototype
  Student.prototype.constructor = Student

  // 实现继承:获取到Person构造函数原型中的成员添加到自身

  let stu = new Student('rose', 20, '女')
  console.log(stu)

  console.log(window)

  // stu.say()
</script>

方法借用模式-上下文调用模式

也叫上下文模式,分为 apply 与 call,bind

call

call方法可以调用一个函数,并且可以指定这个函数的this指向

<script>
  let obj = {
    username: 'jack',
    say: function(age) {
      console.log(this, `我的年龄是:${age}`)
    }
  }
  obj.say(20) // this >> obj

  let newobj = {
    name: 'rose'
  }
  // // 使用call实现修改方法中的this的指向
  // // call是方法的方法
  // // 方法.call(this对象,参数....),让方法中this的指向从默认值改变为你所传入的对象
  // // console.dir(obj.say)
  obj.say.call(newobj, 30)
</script>

call的运用

将伪数组转成数组

let divs = document.querySelectorAll('div'); // 伪数组
// let divs = document.body.children;
console.log(divs);

function change(nodelist) {
    console.log(Object.prototype.toString.call(nodelist));
    return Array.prototype.slice.call(nodelist);

}

自定义一个push方法

<script>
  // 伪数组:本质是一个对象
  let fackArr = {
    0: 1,
    1: 3,
    2: 5,
    3: 19,
    length: 4
  }
  for (let i = 0; i < fackArr.length; i++) {
    console.log(fackArr[i])
  }

  // 我想向伪数组中添加一个成员
  // fackArr.push(100)
  // Array.prototype.push.call(fackArr, 200)
  ;[].push.call(fackArr,100)

  console.log(fackArr)
</script>

自定义一call方法

<script>
  Function.prototype.mycall123 = function(newobj) {
    // 正常情况下,如果想让一个方法中的this指向某个对象,最简单的方式是:某个对象.方法()
    // 谁调用方法,谁就是方法里面的this,现在是say在调用mycall123方法,所以mycall123方法中的this就是say
    console.log(this, typeof this, '-----')
    // this() == say()
    newobj.fn = this
    newobj.fn() // this >> newobj
    delete newobj.fn
  }

  let obj = {
    username: 'jack',
    say: function(age = 30) {
      console.log(this, `我的年龄是:${age}`)
    }
  }
  let newobj = {
    name: 'rose'
  }
  // 让say方法中的this从obj变成newobj
  obj.say.mycall123(newobj)
  // obj.say() // this >> obj
  // newobj.say() // this >> newobj

  // console.log(obj.say.__proto__.constructor) // Function
  console.log(newobj)
</script>

apply

就是apply()方法接受的是一个包含多个参数的数组。而call()方法接受的是若干个参数的列表

可以利用apply 将 刚才的call 的代码修改一下

const RichWumon = {
    name: "富婆",
    say: function () {
        console.log(this.name, " 我要重金求子");
    }
}
​
const obj = {
    name: "屌丝"
}
​
RichWumon.say();            // 富婆
RichWumon.say.apply(obj);   // 屌丝

apply应用

1.简化log方法

// 简化log方法
function log() {
    // 不需要改变this
    console.log.apply(console, arguments);
}

bind方法

bind() 方法创建一个新的函数, 可以绑定新的函数的this指向

var name = '张三';
function Fn(){
    this.age = 1;
    
    console.log(this.name + this.age);
}

Fn();			// 张三 1

// 返回值:新的函数
// 参数:新函数的this指向,当绑定了新函数的this指向后,无论使用何种调用模式,this都不会改变。
let obj = {
    name:'小强',
}
const newFn = Fn.bind(obj);
newFn();		// 小强 1

this的指向

  • 单独使用,this 指向全局对象

    console.log(this); 
    
  • 函数中的 this 指向全局对象

    function show(){
        console.log(this); 
    }
    ​
    show();
    
  • 在函数内部,this 的指向在函数定义的时候是不能确定的,只有函数执行的时候才能确定

    const a = 18;
    const obj = {
        a: 19,
        b: {
            a: 20,
            c: function () {
                console.log(this.a);    // 20
            }
        }
    }
    obj.b.c();
    
  • 在方法中,this 指代该调用方法的对象

const obj ={
	name:"小白",
	say:function(){
		console.log(this);		
	}
}
obj.say()

箭头函数

箭头函数-语法

<script>
  // 语法
  // function test(a, b) {
  //   let sum = a + b
  //   return sum
  // }
  // 1.箭头函数是匿名函数,所以不能单独存在
  // 2.一般用于  函数表达式,参数
  // let test = (a, b) => {
  //   let sum = a + b
  //   return sum
  // }
  // 如果箭头函数体只有一句,可以省略{},同时会返回函数体的结果(我们不用再写return)
  // 如果你自己添加了{},如果需要返回值则需要手动return
  // let test = (a, b) => a + b

  // 如果只有一个 参数,则可以省略(),否则 一定需要()
  let test = a => a + 10
  // 调用方法:和之前一样
  let sum = test(100, 20)
  console.log(sum)
</script>

箭头函数-特性

  • 箭头函数的this是确定的,况且永远不变
  • 箭头函数中的this指向 创建这个箭头函数所在对象 的上下文
<script>
  let obj = {
    name: 'jack',
    say: function() {
      return () => {
        // 箭头函数的this:指向 创建这个箭头函数  所在对象  的上下文
        console.log(this)
      }
    }
  }

  let fn = obj.say()
  // fn()
  let newobj = {
    name: 'rose'
  }
  newobj.fn12 = fn
  newobj.fn12()
  // fn.call(newobj)

  // obj.say() 方法调用模式 》》 window
  // let fn = obj.say
  // fn() // 函数调用模式 》》 window
  // let newobj = {
  //   name:'rose'
  // }
  // obj.say.call(newobj) // 上下文调用模式 》》 window
</script>