函数就是将某一段代码进行封装,并为这段代码起个名子,通过这个名子就可以调用这段代码,使用函数可以实现代码复用、便于修改;
JavaScript 有三种声明函数的方法
函数声明式是使用 function关键字 来定义函数; function关键字后面是函数名,函数名后面是一对圆括号,里面是传入函数的参数。函数体放在大括号里面
语法
function 函数名(形参){
// 函数体
}
举例说明
function paly(str){
return `我最喜欢玩${str}`
}
采用变量赋值的方法,写一个匿名函数赋值给变量,这又称作函数表达式(因为等号右侧只能是表达式)
语法
const 变量名 = function(){
// 函数体
}
举例说明
const play = function(str){
return `我最喜欢玩${str}`
}
注意事项
采用函数表达式声明函数时,function关键字后面不带有函数名。如果加上函数名,该函数名只在函数体内部有效,在函数体外部无效;
const play = function toplay(str){
console.log('toplay1', toplay)
return `我最喜欢玩${str}`
}
play() // f toplay(str){...}
console.log('toplay2', toplay) // Error toplay is not defined
使用构造函数声明函数,参数中最后一个参数为函数体,除了最后一个参数之外的参数为函数的参数(不推荐使用);
语法
new Function(形参···,函数体)
示例
const paly = new Function('str','return `我最喜欢玩${str}`')
console.log(paly('游戏')) // 我最喜欢玩游戏
调用函数时,要使用圆括号运算符。圆括号之中,可以加入函数的参数;
函数名(实参)
函数声明式具有函数提升->可以在声明之前调用
console.log('play', paly('游戏')) // play 我最喜欢玩游戏
function paly(str){
return `我最喜欢玩${str}`
}
上述代码等价于 将函数声明提升到当前
作用域的最前面;
function paly(str){
return `我最喜欢玩${str}`
}
console.log('play', paly('游戏'))
函数表达式没有函数提升,必须先声明后使用;
但是使用var定义变量过程中具有变量提升
console.log('play', play) // undefined
console.log('play', play('游戏')) // Error play is not a function
var play = function(str){
return `我最喜欢玩${str}`
}
上述代码等价于
var play
console.log('play', play) // undefined
console.log('play', play('游戏')) // Error play is not a function
play = function(str){
return `我最喜欢玩${str}`
}
使用let与const定义变量接收函数表达式则没有变量提升;
console.log('play', play) // Error Cannot access 'play' before initialization
console.log('play', play('游戏'))
let play = function(str){
return `我最喜欢玩${str}`
}
console.log('play', play) // Error Cannot access 'play' before initialization
console.log('play', play('游戏'))
const play = function(str){
return `我最喜欢玩${str}`
}
优先级:函数提升>变量提升
;若是函数名与变量名相同,提升过程中不会被覆盖
,但是重新赋值会被覆盖
;
举例说明·
console.log(bar); // function bar() { console.log(123); }
console.log(bar()); // 123 undefined
var bar = 456;
function bar() {
console.log(123);
}
console.log(bar); // 456
bar = 789;
console.log(bar); //789
console.log(bar()) // bar is not a function
/*
解析说明-[1]函数提升优先级大于变量提升,并且在提升过程中不会被变量提升覆盖
var bar = function() {
console.log(123);
}
var bar
console.log(bar);
console.log(bar());
解析说明-[2]变量赋值会将函数覆盖
bar = 456;
console.log(bar);
bar = 789;
console.log(bar);
console.log(bar())
*/
如果同一个函数被声明了多次,后一个函数就会覆盖
前一个函数的声明;与此同时需要注意由于函数声明式具有函数提升,因此第一次声明在任何时候都是无用的!!!
function test(){
console.log(111)
}
test() // 222
function test(){
console.log(222)
}
test() // 222
无论何时调用都是222
上面代码等价于
function test(){
console.log(111)
}
function test(){
console.log(222)
}
test() // 222
test() // 222
函数涉及到的数据有两个方向:
形参与实参
可以实现将函数外的数据传递给函数内部。函数运行的时候,有时需要提供外部数据,不同的外部数据会得到不同的结果,这种外部数据就叫参数;
function square(x) {
return x * x;
}
square(2) // 4
square(3) // 9
形参与实参之间就是赋值的关系;
修改属性/元素将会影响原始值
; // 简单类型
function square(x) {
x = 222
}
const num = 111
square(num)
console.log('num', num) // 111
// 引用类型
function square(x) {
x.age = 22
}
const obj = {
name:'chaochao',
age:18
}
square(obj)
console.log('obj', obj) // {name: 'chaochao', age: 22}
在函数调用过程中,实数是可以省略的,省略的参数的值就变为undefined
function square(x) {
return x * x;
}
console.log(square()) // NaN
但是,没有办法只省略靠前的参数,而保留靠后的参数。如果一定要省略靠前的参数,只有显式传入undefined;
function f(a, b) {
return a;
}
f( , 1) // SyntaxError: Unexpected token ,(…)
f(undefined, 1) // undefined
如果有同名的参数,则取最后出现的那个值;
function f(a, a) {
console.log(a);
}
f(1, 2) // 2
function f(a, a) {
console.log(a);
}
f(1) // undefined
由于 JavaScript 允许函数有不定数目的参数,所以需要一种机制,可以在函数体内部读取所有参数。这就是arguments对象的由来;
arguments是一个对应于传递给函数的参数的伪数组;
[1]除箭头函数外每个函数内部都有一个arguments对象
[2]作用是存储函数调用时传入的每一个参数(实参);
<script>
function getArg (a, b, c) {
console.log(arguments) //{0:1,1:2,length:2}
}
getArg(1, 2)
</script>
<script>
function getArg (a) {
console.log(arguments) //{0:1,1:2,length:2}
}
getArg(1, 2)
</script>
[3]arguments.callee的值是函数本身;
[4]arguments对象一般是用于在函数实参不确定的情况下使用的!
[5]案例
var length = 10
function fn () {
console.log(this.length)
}
var obj = {
method: function (fn) {
fn() // window调用---相当于window.length=10
arguments[0]() //arguments[0]等同于arguments.fn ---相当于对象调用 arguments.length=2
}
}
obj.method(fn, 1)
在最外层定义的是全局变量,可以在任何位置使用;
在函数中声明的变量称为局部变量,只能在本变量声明的函数内部使用;
其中的具体关系可以看作用域与作用域链
var a = 18
f1()
function f1(){
var b = 9
console.log(a)
console.log(b)
var a = '123'
}
运行结果为 undefined 9;
上述代码存在变量提升、函数提升,在运行过程中的代码如下
var a
function f1(){
var b
var a
b = 9
console.log(a)
console.log(b)
a = '123'
}
f1()
打印a—>由于当前作用域中定义了a,获取当前作用域中a的值;
function f1(){
var a=b=c=9
console.log(a)
console.log(b)
console.log(c)
}
f1()
console.log(c)
console.log(b)
console.log(a)
结果为9 9 9 9 9 Error a is not defined
由于javascript存在隐士赋值,并且隐士赋值的变量为全局变量
,详情可见作用域与作用域链;
上述代码在运行过程中的结果为
var b
var c
function f1(){
var a
a = 9
b = 9
c = 9
console.log(a)
console.log(b)
console.log(c)
}
f1()
console.log(c)
console.log(b)
console.log(a)
return关键字概念
(1)在返回值为复杂数据类型的时候—每调用一次分配一次空间,若多次调用造成空间浪费的问题;
(2)举例说明
//创建一个函数
function Per() {
let per = {
name: '牛牛',
age: 23
}
return per;
}
//调用函数接收函数的返回值(对象)
let per1 = Per();
let per2 = Per();
//比较对象的值
console.log(per1 == per2); //false
<script>
//创建一个函数
function Per () {
let per = {
name: '牛牛',
age: 23
}
function cursor () {
return per
}
return cursor
}
let cursor = Per() //得到一个闭包函数
//调用函数接收函数的返回值(对象)
let per1 = cursor()
let per2 = cursor()
//比较对象的值
console.log(per1 == per2) //true
</script>
this关键字详解
箭头函数是函数声明式与函数表达式的变形!
箭头函数是函数表达式的一个变形,语法比箭头函数更加整洁;
声明箭头函数:(形参)=>{函数体}
举例说明
// 函数声明
function getSum1(a,b){
return a+b
}
// 箭头函数
const getSum2 = (a,b)=>{ return a+b }
console.log(getSum1(1,2), getSum2(1,2)) // 3 3
若是仅有一个形参,可以省略();
const getSum2 = a=>{ return a }
若是函数体内仅有一行代码,可以省略 {}; 若是函数体内仅有一行代码且有return,需要将return一起省略
const getSum2 = a=>a
如果函数返回的是对象,若是想省略函数体外面的{},需要在函数体外面加()
const getSum2 = a=>{a:a}
getSum2(1) // undefined
原因:解析的时候,会将{}解析为函数体外的{},不会解析为对象
箭头函数是函数表达式的一个变形,函数表达式是没有函数提升的,箭头函数也没有寒暑假提升;
sum(1,2) // Cannot access 'sum' before initialization
const sum = (a,b)=>{
console.log(a,b)
}
函数提升中:只有函数声明定义的函数具有函数提升;
箭头函数没有自己的this指向,其本质是通过作用域链获取上一级的this指向;
举例说明
// 定时器里的函数为箭头函数,this要看上一级函数->Person的this指向->指向p
function Person(){
this.age = 0;
setInterval(() => {
this.age++;
console.log(p.age)// 依次递增
}, 1000);
}
var p = new Person();
// 定时器里面的函数为函数表达式,this指向window
function Person(){
this.age = 0;
setInterval(function(){
this.age++;
console.log(p.age) // 一直为0
}, 1000);
}
var p = new Person();
因此箭头函数不能修改this指向
[1]使用call、apply、bind方法调用一个函数时,传递的第一个参数总会被忽略;
const obj = {
a:111,
b:222
}
const fun = ()=>{
this.a++
return this.a
}
console.log(fun()) // NaN
// apply的第一个参数被忽略,fun函数内的this还是指向window
console.log(fun.apply(obj)) // NaN
const obj = {
a:111,
b:222
}
function fun(){
this.a++
return this.a
}
console.log(fun()) // NaN
// apply修改fun内部函数的this指向为obj
console.log(fun.apply(obj)) // 112
[2]箭头函数不能通过new关键字调用,会报错fun is not a constructor
const fun = ()=>{
console.log('arguments',arguments) //arguments is not defined
}
fun()
构造函数也称为构造器,通常在创建对象(Array,Function,Object)时会调用;
Es5中的构造函数就是一个普通函数,在表现形式上看与普通函数没有什么区别,只是在调用形式上有所区分;
使用new 关键字调用的函数被成为构造函数;
为作区分,一般函数名首字母大写;
举例说明
function CreateStu(name,age){
this.name = name
this.age = age
this.play =function(){
console.log('我最喜欢学校!')
}
}
let s1 = new CreateStu('chaochao', 18)
let s2 = new CreateStu('niuniu', 23)
console.log(s1,s2, s1.play == s2.play) // false
发现每一次调用都会重新创建一个方法(引用类型),若是所有实例对象都具有的属性和方法–>可以添加在原型中;
function CreateStu(name,age){
this.name = name
this.age = age
}
CreateStu.prototype.play = function(){
console.log('我最喜欢学校!')
}
let s1 = new CreateStu('chaochao', 18)
let s2 = new CreateStu('niuniu', 23)
console.log(s1,s2, s1.play == s2.play) // true
new关键字的作用
ES6 为了使得构造函数的写法更接近传统语言的写法,引入了 Class(类)这个概念;ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
class 类名{
constructor(){
// 相当于es5的构造函数
}
}
空的
constructor()方法会被默认添加;需求:现在创建一个构造函数用于创建 学生 每个学生有 姓名、年龄属性以及 say 函数,还有一个标示,表明是‘学生’
使用Es5的构造函数进行创建
function Student(name,age){
this.name = name
this.age = age
this.attr = '学生'
this.say = function(){
console.log(`大家好,我叫${name},今年${age}岁`)
}
}
let s1 = new Student('chaochao', 18)
let s2 = new Student('niuniu', 23)
s1.say() // 大家好,我叫chaochao,今年18岁
s2.say() // 大家好,我叫niuniu,今年23岁
console.log(s1.say == s2.say) // false
使用Es5创建实例化对象,默认情况下会将 属性与方法添加在实例化对象身上
。
每一次创建一个实例化对象都会创建一个函数,不同实例化对象的方法引用地址不同!
若是想要添加在原型对象上,需要使用如下代码
function Student(name,age){
this.name = name
this.age = age
}
Student.prototype.say = function(){
// 实例化对象调用 -> this指向的是实例化对象
console.log(`大家好,我叫${this.name},今年${this.age}岁`)
}
Student.prototype.attr = '学生'
let s1 = new Student('chaochao', 18)
let s2 = new Student('niuniu', 23)
s1.say() // 大家好,我叫chaochao,今年18岁
s2.say() // 大家好,我叫niuniu,今年23岁
console.log(s1,s2,s1.say == s2.say) // true
需要注意的是,属性只有每个实例化对象都相同才能添加在原型对象上,否则会被覆盖。
使用Es6构造函数进行创建
class Student{
constructor(name,age){
this.name = name
this.age = age
this.attr = '学生'
}
say1(){
console.log(`大家好,我叫${this.name},今年${this.age}岁`)
}
say2 = () {
console.log(`大家好,我叫${this.name},今年${this.age}岁`)
}
}
let s1 = new Student('chaochao', 18)
let s2 = new Student('niuniu', 23)
s1.say()
s2.say()
console.log(s1,s2, s1.say == s2.say) // true
在类中直接定义在{}中的方法默认会添加在该构造函数的原型对象上
;通过赋值的形式定义方法是将方法添加在实例化对象上面
了
在类中,若是有些变量是常量,不需要在构造器(constructor)中去声明,可直接在类中声明即可。 -> 如上需求每个学生都要有一个标识为学生
class Student{
attr = '学生'
constructor(name,age){
this.name = name
this.age = age
}
say(){
console.log(`大家好,我叫${this.name},今年${this.age}岁`)
}
}
let s1 = new Student('chaochao', 18)
let s2 = new Student('niuniu', 23)
s1.say()
s2.say()
console.log(s1,s2, s1.say == s2.say) // true
attr属性默认会添加在实例化对象身上
目前我们在类中定义的属性、声明的方法,都会被实例继承,若是不想属性、方法被实例继承,要在前面加static关键字—> 表示该方法/属性为静态方法/属性(是构造函数的属性/方法
),不被实例所继承;
静态属性/方法仅能被当前类/自类访问;
举例说明
class Foo {
static classMethod() {
return 'hello';
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod() // TypeError: foo.classMethod is not a function
class MyClass {
static myStaticProp = 42;
constructor() {
console.log(MyClass.myStaticProp); // 42
}
}
const m = new MyClass()
console.log('myStaticProp',m.myStaticProp) // undefined
私有属性的属性名之前使用#
表示;
class IncreasingCounter {
#count = 0;
get value() {
console.log('Getting the current value!');
return this.#count;
}
increment() {
this.#count++;
}
}
上面代码中,#count
就是私有属性,只能在类的内部使用(this.#count
)。如果在类的外部使用,就会报错。
const counter = new IncreasingCounter();
counter.#count // 报错
counter.#count = 42 // 报错
上面示例中,在类的外部,读取或写入私有属性#count
,都会报错。
另外,不管在类的内部或外部,读取一个不存在的私有属性,也都会报错。这跟公开属性的行为完全不同,如果读取一个不存在的公开属性,不会报错,只会返回undefined
。
class IncreasingCounter {
#count = 0;
get value() {
console.log('Getting the current value!');
return this.#myCount; // 报错
}
increment() {
this.#count++;
}
}
const counter = new IncreasingCounter();
counter.#myCount // 报错
上面示例中,#myCount
是一个不存在的私有属性,不管在函数内部或外部,读取该属性都会导致报错。
注意,私有属性的属性名必须包括#
,如果不带#
,会被当作另一个属性。
class Point {
#x;
constructor(x = 0) {
this.#x = +x;
}
get x() {
return this.#x;
}
set x(value) {
this.#x = +value;
}
}
上面代码中,#x
就是私有属性,在Point
类之外是读取不到这个属性的。由于井号#
是属性名的一部分,使用时必须带有#
一起使用,所以#x
和x
是两个不同的属性。
这种写法不仅可以写私有属性,还可以用来写私有方法。
class Foo {
#a;
#b;
constructor(a, b) {
this.#a = a;
this.#b = b;
}
#sum() {
return this.#a + this.#b;
}
printSum() {
console.log(this.#sum());
}
}
上面示例中,#sum()
就是一个私有方法。
另外,私有属性也可以设置 getter 和 setter 方法。
class Counter {
#xValue = 0;
constructor() {
console.log(this.#x);
}
get #x() { return this.#xValue; }
set #x(value) {
this.#xValue = value;
}
}
上面代码中,#x
是一个私有属性,它的读写都通过get #x()
和set #x()
操作另一个私有属性#xValue
来完成。
私有属性不限于从this
引用,只要是在类的内部,实例也可以引用私有属性。
class Foo {
#privateValue = 42;
static getPrivateValue(foo) {
return foo.#privateValue;
}
}
Foo.getPrivateValue(new Foo()); // 42
上面代码允许从实例foo
上面引用私有属性。
私有属性和私有方法前面,也可以加上static
关键字,表示这是一个静态的私有属性或私有方法。
class FakeMath {
static PI = 22 / 7;
static #totallyRandomNumber = 4;
static #computeRandomNumber() {
return FakeMath.#totallyRandomNumber;
}
static random() {
console.log('I heard you like random numbers…')
return FakeMath.#computeRandomNumber();
}
}
FakeMath.PI // 3.142857142857143
FakeMath.random()
// I heard you like random numbers…
// 4
FakeMath.#totallyRandomNumber // 报错
FakeMath.#computeRandomNumber() // 报错
上面代码中,#totallyRandomNumber
是私有属性,#computeRandomNumber()
是私有方法,只能在FakeMath
这个类的内部调用,外部调用就会报错。
class People{
constructor(name, age){
this.name = name
this.age = age
}
}
const p1 = new People('chaochao', 18)
p1.age = 20
console.log('p1', p1) // {name: 'chaochao', age: 20}
如上案例中,我们可以随意去修改实例化对象的属性或方法。这样对于数据来说是非常不安全的,如对于age来说不能为负数…
像一些有限制的数据 我们希望不能被随意更改,此时就用到了set
以及get
方法。
get 属性名(){
return xxx
}
set 属性名(){
// 赋值xxx
}
当通过点语法去赋值时并不会直接赋值而是调用同名的set方法,当读取时也是直接调用同名的get方法。但是需要注意一点,若是需要使用get与set方法时属性名不要与方法名相同,举例说明
class People{
constructor(name, age){
this.name = name
this.age = age
}
get age(){
console.log('age', this.age)
return this.age
}
set age(value){
console.log('value', value)
this.age = value
}
}
const p1 = new People('chaochao', 18) // 进入死循环 error: Maximum call stack size exceeded
在调用constructor函数进行赋值时,this.age = age 就会调用age的set方法,而set函数里面的this.age = value又会调用age的set方法,就会陷入死循环了。
class People{
constructor(name, age){
this.name = name
this._age = age
}
get age(){
return this._age
}
set age(value){
this._age = value > 0 ? value : this._age
}
}
const p1 = new People('chaochao', 18)
这样就没有问题了!
tips: 属性名和方法名不重复即可!
{}内部赋值
还是在constructor
中赋值都是添加给实例化对象
的属性函数声明式
声明的函数是添加在原型对象
上,若是通过函数表达式
方式声明的函数是添加在实例化对象
上的static
关键字表示当前属性/方法为静态属性/方法,静态属性/方法是构造函数
本身的属性,仅能被当前类所访问;#
表示当前属性/方法为私有属性。私有属性/方法仅能在当前类的内部
使用,在类的外部使用会被错。class Person {
// 属性:无论是在{}内部赋值还是在constructor中赋值都是添加给实例化对象的属性
// 属性[1] 每个人都有共同的特点都生活在地球中,不需要实例化对象传递过来,因此可以直接声明在{}中
area = '地球圈'
// 属性[2] 每个人都有不同的名字、性别、年龄,需要实例化对象的时候传递过来,因此声明在constructor中
constructor(name, age, sex ){
this.name = name
this.age = age
this.sex= sex
}
// 方法[1]:直接定义在{}中的方法是添加在 构造函数的原型对象上的
lookthis(){
console.log('this',this)
}
// 方法[2]:通过赋值的形式定义方法是将方法是将方法添加在实例化对象上面了(与变量赋值相同)
hello = function(){
console.log(`大家好,我是${this.name},我${this.age}岁了`)
}
// 若是我们想要将属性和方法添加给构造函数而非实例化对象,需要使用 static 关键字
static attr = '创建一个person对象'
static mth = function (){
console.log('我是一个创建person对象的构造函数')
}
// 若是仅想要属性与方法在类的内部使用就以 # 开头
#value ='月球没有?'
}
查看属性const p1 = new Person('chaochao', 18, '女')
console.log('p1',p1)
Class 可以通过extends
关键字实现继承,让子类继承父类的属性和方法;
ES6 规定,子类必须在constructor()方法中调用super()
,否则就会报错(上述代码就报错了);原因是ES6 的继承机制是先将父类的属性和方法,加到一个空的对象上面,然后再将该对象作为子类的实例
,也就是说只有调用super方法,才能生成一个继承父类的this对象,否则无法生成;
若是子类没有constructor方法,子类会隐士生成一个constructor方法并隐士调用super方法;
若是显示调用 super,则必须将需要的值传递给super
class People{
constructor(name,age){
this.name = name
this.age = age
}
play(){
console.log('直立行走')
}
}
class Stu extends People{
constructor(name,age,sex){
super(name,age)
this.sex= sex
}
}
const s1 = new Stu('chaochao', 18, '女')
console.log('s1',s1)
若是显示定义了构造函数,但是却没有将值传递到super中将无法通过this获取对应的值
class People{
constructor(name,age){
this.name = name
this.age = age
}
play(){
console.log('直立行走')
}
}
class Stu extends People{
constructor(name,age,sex){
super()
this.sex= sex
}
}
const s1 = new Stu('chaochao', 18, '女')
console.log('s1',s1)
这也意味着新建子类实例时,父类的构造函数必定会先运行一次;
class People{
constructor(name,age){
this.name = name
this.age = age
console.log(11111111111)
}
play(){
console.log('直立行走')
}
}
class Stu extends People{
constructor(name,age,id){
super(name,age,id);
this.id = id
console.log(2222222222)
}
}
const s1 = new Stu('chaochao', 18, 222)
发现在创建s1对象时,先打印了11111111111,又打印了22222222222;
原因
只有调用super()
之后,才可以使用this
关键字,否则会报错;
若是想在子类中调用父组件的方法可以通过super.父组件方法名
来调用,举例说明
class Animal{
constructor(name){
this.name = name
}
say(){
console.log(`${this.name}在叫`)
}
}
class Dog extends Animal{
say(){
super.say() // 打印旺财在叫
}
}
const dog1 = new Dog('旺财')
dog1.say()
父类的属性和方法,除了私有属性和私有方法外都会被子类继承;
class People{
#p = 'people'
constructor(name,age){
this.name = name
this.age = age
console.log(11111111111)
}
play(){
console.log('直立行走')
}
}
class Stu extends People{
constructor(name,age,id){
super();
this.id = id
console.log(this.#p) //Private field '#p' must be declared in an enclosing class
}
}
const s1 = new Stu()
// s1.#p // Private field '#p' must be declared in an enclosing class
可以发现父类的私有属性 无论是在子类的{}里面还是外面都无法获取;若是我们想获取父类的某个属性该怎么办呢->可以使用get和set方法;
class People{
#p = 'people'
constructor(name,age){
this.name = name
this.age = age
console.log(11111111111)
}
play(){
console.log('直立行走')
}
getp(){
return this.#p
}
}
class Stu extends People{
constructor(name,age,id){
super();
this.id = id
console.log('#p', this.getp()) // #p people
}
}
const s1 = new Stu()
可以通过Object.getPrototypeOf()
方法获取子类的父类;
class People{
}
class Stu extends People{
constructor(name,age,id){
super();
}
}
console.log(Object.getPrototypeOf(Stu))
console.log(Object.getPrototypeOf(People))
可以判断一个类是否继承了另一个类;
class People{
}
class Stu extends People{
constructor(name,age,id){
super();
}
}
console.log(Object.getPrototypeOf(Stu) == People) // true