Day04 如何写好javascript | 青训营

90 阅读8分钟

[this关键字]  www.freecodecamp.org/chinese/new… 

[apply,bind,call三个方法区别]  www.freecodecamp.org/chinese/new… 

如何写好JavaScript

推荐两本书

image-20230731095424967

写好JS的三个原则

  • 各司其职:让HTML,css,Javascript 职能分离
  • 组件封装:好的UI组件具备正确性,扩展性,复用性
  • 过程抽象:应用函数编程式思想

各司其职

eg:需求是点击图标改变夜间模式或者白昼模式

image-20230731105211130

image-20230731105232418

三种实现方式

  • 用js控制body的background-color,color

    • const btn = document.getElementById( ' modeBtn ' );btn.addEventListener( 'click', (e) => {
      const body = document.body;
      if( e.target.innerHTML === '') {
      body. style.backgroundColor = 'black ' ;body. style.color = 'white ' ;o1
      e.target.innerHTML = ' ';
      }else {
      body.style.backgroundColor = 'white ' ;body . style.color = 'black ' ;
      e.target.innerHTML ='.';}
      });
      
  • 用js控制content的classlist,通过add,remove方法

    • <input type="checkbox" id="light">
          <div class="content">
              <label for="light" id="lab"><img src="..." class="sun" alt=""></label>
              <span>...</span>
          </div>
      
    • window.onload = function () {
          const but=document.getElementById("lab");
          const content=document.querySelector(".content");
          but.addEventListener("click",function(){
              if(!content.classList.contains("night")){
                  content.classList.add("night");
              }else{
                  content.classList.remove("night");
              }
          })
      }
      
  • 摒弃js,纯粹使用css伪类选择器,通过改变label的选中状态来添加样式

    • #light:checked ~ .content{
          background-color: black;
          color:white;
      }
      

    显然第二种或者第三种方式符合各司其职的特点

组件封装

  • 组件是指Web页面上抽出来一个个包含模版(HTML)、功能(JS)和样式(CSS)的单元。
  • 好的组件具备封装性、正确性、扩展性、复用性。

基本方法

  • 结构设计

  • 展现效果

  • 行为设计

    • API (接口功能)
    • Event (控制流) 自定义事件

重构(插件化)

解耦定义

  • 将控制流元素抽取成组件
  • 插件与组件之间通过依赖注入方式建立联系

下面是轮播图的实例

解耦

将轮播图插件化实现,将controls,arrow提取成函数,再注册到实体类里面

可以看到构造函数篇幅明显下降,通过registerPlugins方法注册插件

    registerPlugins(...plugins) {
        plugins.forEach(plugin=>plugin(this));
    }

将button,arrow提取成函数

function pluginController(slider){
    const controller = slider.container.querySelector('.slider-list_control');
​
        if (controller) {
            const buttons = controller.querySelectorAll('.slider-list_control-buttons,.slider-list_control-buttons--selected');
            controller.addEventListener('mouseover', evt => {
                const idx = Array.from(buttons).indexOf(evt.target);
                if (idx >= 0) {
                    slider.slideTo(idx);
                    slider.stop();
                }
            })
            controller.addEventListener('mouseout', evt => {
                slider.start();
            })
            slider.container.addEventListener('slide', evt => {
                const idx = evt.detail.index;
                const selected = controller.querySelector('.slider-list_control-buttons--selected');
             
                if (selected) {
                    selected.className = 'slider-list_control-buttons';
                }
                buttons[idx].className = 'slider-list_control-buttons--selected';
            })
        }
}

注册组件时

const slider1 = new slider('my-slider',1000);
slider1.registerPlugins(pluginController,pluginArrow);
slider1.start();

重构(模板化)

借助render函数,添加html结构

例如将button组件模块化:

注册组件的函数:

    registerPlugins(...plugins) {
        plugins.forEach(plugin=>{
            const pluginContainer=document.createElement('div');
            pluginContainer.className='slider-list_plugin';
            pluginContainer.innerHTML=plugin.render(this.options.images);
            this.container.appendChild(pluginContainer);
            plugin.action(this);
        });
    }

button的render函数,用于生成html模板

 render(images){
        const content=images.map(((image,i)=>{
            return `<span class="slider-list_control-buttons${i===0?'--selected':''}"></span>`
    })).join('');
        return `<div class="slider-list_control">${content}</div>`
    }

注册组件时

const slider1 = new slider('my-slider',{images:['https://p5.ssl.qhimg.com/t0119c74624763dd070.png','https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg','https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg','https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg'],cycle:2000});
slider1.registerPlugins(pluginController);
slider1.start();

补充知识点

javascript中的this关键字

this 上下文

在函数中使用时, *this *关键字仅指向它所绑定的对象。它回答了应该从何处获取某些值或数据的问题:

function alert() { 
 console.log( this.name + 'is calling'); 
}

在上面的函数中,this 关键字指向它绑定到的对象,因此它从那里获取 “name” 属性

但是你怎么知道函数绑定到哪个对象呢?你怎么知道 this 指向什么?

为此,我们需要详细了解函数是如何绑定到对象的。

绑定类型

  • 默认绑定
  • 隐式绑定
  • 显式绑定
  • 构造函数调用绑定

默认绑定

首先要记住的规则之一是,如果包含 this 引用的函数是一个独立函数,那么该函数将绑定到全局对象

隐式绑定

根据 JavaScript 中的绑定规则,只有当该对象在调用站点绑定到它时,函数才能使用该对象作为其上下文。这种形式的绑定叫作隐式绑定。

function alert() { 
 console.log( this.age + 'years old' ); 
}
​
var myObj {
  age: 22,
  alert: alert
 }
​
myObj.alert() // 22 years old

换句话说,第一个示例与此没有区别:

function alert() { 
 console.log( this.age + 'years old'); 
}
​
var myObj {
  age: 22
 }
​
myObj.alert() // 22 years old

在这种情况下,即使没有将 alert 定义为对象中的字段,运行 obj.alert() 也会首先在对象中插入 alert 作为方法,然后将其解析为 this 上下文。

在这两种情况下,我们都说 alert 隐式绑定到它的上下文。我们只是通过检查调用站点来做到这一点。

显式绑定

但是如果我们想强制一个函数使用一个对象作为它的上下文,而不在对象上放置一个属性函数引用呢?

我们有两个实用方法来实现这一点:call()apply()

function alert() { 
 console.log( this.age + 'years old'!'); 
}
​
var myObj {
  age: 22
 }
​
​
alert.call(myObj); // 22 years old

现在是有趣的部分。即使你将该函数多次传递给新变量(柯里化),每次调用都将使用相同的上下文,因为它已被锁定(显式绑定)到该对象。这被称为硬绑定

构造函数调用绑定

最后一种可能也是最有趣的绑定类型,new 绑定,与其他基于类的语言相比,它也突出了 JavaScript 的特点。

当使用前面的 new关键字调用函数时,也被称为构造函数调用,会发生以下情况:

  • 创建(或构建)一个全新的对象
  • 新构造的对象 [[Prototype]] 链接到构造它的函数
  • 新构造的对象被设置为该函数调用的 this 绑定

让我们在代码中看看这一点,以便更好地理解:

function giveAge(age) { 
 this.age = age; 
} 
var bar = new giveAge( 22 ); 
console.log( bar.age ); // 22

通过在其前面使用 new 调用 giveAge(..),我们构造了一个新对象,并将该新对象设置为 foo(..) 调用的 this。所以 new 是你可以绑定一个函数调用的 this 的最后一种方式。

高阶函数

once

事件只触发一次{onece:true}

btn.addEventListen('click',function,{once:true})

一个函数返回一个函数,则这个函数称为“高阶函数”

  • 以函数作为参数
  • 以函数作为返回值
  • 常用于作为函数装饰器
function once(fn){
    return function(...args){
        if(fn){
        const ret = fn.apply(this, args);
        fn =null;
        return ret;
            }
        }
    }

once函数:为了能够让“只执行一次"的需求覆盖不同的事件处理,我们可以将这个需求剥离出来。这个过程我们称为过程抽象。

节流函数 throttle

间隔多少秒触发函数

function throttle(fn,time=500){
    let timer;
    return function(...args){
        if(timer==null){
            fn.apply(this,args);
            timer=setTimeout(()=>{
                timer=null;
            },time)
        }
    }
}

防抖 debounce

超过多少秒没有改变,触发函数,永远只会调用最后一次

function debounce(fn,dur){
    let timer;
    dur=dur||100return function(...args){
        clearTimeout(timer);
        setTimeout(()=>{
            timer=fn.apply(this,args)
        },dur)
    }
}

延时调用 consumer

点击多少,会间隔多少秒慢慢调用直至完成

function consumer(fn,time){
    let task=[];
        timer;
    return function(...args){
        task.push(fn.bind(this,args));
        if(timer==null){
            timer=setInterval(()=>{
                task.shift().call(this)
                if(task.length<=0){
                    clearInterval(timer);
                    timer=null;
                }
            },time)
        }
    }
}

增加代码可读性,使用纯函数,其中高阶函数就是纯函数,受制于外部环境的是非纯函数

状态切换 toggle

声明式编程:更高的扩展性,如果用if-else实现,添加一种状态时就要再写一个分支(麻烦

function toggle(...actions){
    return function(...args){
        let action=actions.shift();
        actions.push(action);
        return action.apply(this,args);
    }
}
swither.onclick=toggle(
    evt=>evt.target.className='off';
    evt=>evt.target.className='on';
    evt=>evt.target.className='warn';
)

Function.prototype.bind()

bind() 方法会创建一个新函数,当这个新函数被调用时,它的 this 值是传递给 bind() 的第一个参数, 它的参数是 bind() 的其他参数和其原本的参数。

fun.bind(thisArg[, arg1[, arg2[, ...]]])
  • thisArg 当绑定函数被调用时,该参数会作为原函数运行时的 this 指向。当使用 new 操作符调用绑定函数时,该参数无效。
  • arg1, arg2, … (可选)当绑定函数被调用时,这些参数加上绑定函数本身的参数会按照顺序作为原函数运行时的参数。

Function.prototype.call()

call函数更改一个函数内部this的值,并且将传入的参数作为这个函数的执行参数。

call函数的语法如下:

func.call(thisObj, args1, args2, ...)

其中:

  • func 是通过不同this对象调用的函数
  • thisObj 是用来替换函数func内部 this关键字的对象或者值
  • args1, args2 args1, args2是参数,与改变后的this对象一起传递给调用的函数。

注意如果在不传入thisObj参数的情况下调用函数,JavaScript默认this值为全局对象。

Function.prototype.apply()

ApplyCall函数类似。callapply函数唯一的不同是传入的参数。

apply中,参数可以是一个数组的字面量或者一个新的数组对象。

apply函数的语法如下:

func.apply(thisObj, argumentsArray);

其中:

  • func 是通过不同this对象调用的函数
  • thisObj 是用来替换函数func内部 this关键字的对象或者值
  • argumentsArray 可以是参数数组、数组对象或者arguments关键字本身

如你所见,apply函数有不同的语法。

第一种语法很简单,你可以传入一个参数数组:

func.apply(thisObj, [args1, args2, ...]);

第二种语法可以传入一个新的数组对象:

func.apply(thisObj, new Array(args1, args2));

第三种语法可以传入arguments关键字:

func.apply(thisObj, arguments); 

arguments是函数中的一个特殊对象,包含传入函数的参数的值。你可以将这个关键字与apply函数一起使用,以接受任何数量的任意参数。

简单概括一下:

  • Call、apply和bind可以改变调用函数内部this关键字的上下文
  • 每个例子的调用方式不同 – apply通过一组数组执行,call执行结果类似但是参数由逗号隔开

New CustomEvent自定义事件

new CustomEvent(eventName, params);

创建一个自定义事件

const event=new CustomEvent('mock-event');

传递参数

这里值得注意,需要把想要传递的参数包裹在一个包含detail属性的对象

const event=new CustomEvent(eventName, { detail: params });

发起事件

调用dispatchEvent方法发起事件,传入你刚才创建的方法

window.dispatchEvent(event);

监听事件

document.body.addEventListener('show', (event) => { console.log(event.detail); });
// 触发
let myEvent = new CustomEvent('show', {
    detail: {
        username: 'zhangxinxu.com',
        userid: '20200820'
    }
});
document.body.dispatchEvent(myEvent);

结语

JavaScript是前端最基础的语言,在交互领域扮演重要角色,js代码不仅要完整,更要写的优雅,多个重复的代码,可以自己解耦封装成组件,js中很多函数,方法可以便捷开发,所以非常有必要看文档,红宝书是个很全的js学习书籍,可以尝试,一起加油!