JavaScript中的闭包有什么实际用途?

时间:2010-04-28 09:39:20

标签: javascript closures terminology

我是trying我最难绕过JavaScript闭包。

通过返回内部函数,我可以访问其直接父级中定义的任何变量。

这对我有用吗?也许我还没有完全了解它。大多数examples I have seen online都没有提供任何真实世界的代码,只是模糊的例子。

有人能告诉我现实世界中使用的闭包吗?

这是一个吗?例如?

var warnUser = function (msg) {
    var calledCount = 0;
    return function() {
       calledCount++;
       alert(msg + '\nYou have been warned ' + calledCount + ' times.');
    };
};

var warnForTamper = warnUser('You can not tamper with our HTML.');
warnForTamper();
warnForTamper();

23 个答案:

答案 0 :(得分:206)

我使用了闭包来做类似的事情:

a = (function () {
    var privatefunction = function () {
        alert('hello');
    }

    return {
        publicfunction : function () {
            privatefunction();
        }
    }
})();

正如您所见,a现在是一个对象,方法publicfunctiona.publicfunction())调用privatefunction,它只存在于闭包内。您可以直接致电privatefunction(即a.privatefunction()),只需publicfunction()

它是一个最小的例子,但也许你可以看到它的用途?我们使用它来强制执行公共/私人方法。

答案 1 :(得分:130)

假设您要计算用户在网页上点击按钮的次数
为此,您在按钮的 onclick 事件触发功能以更新变量计数

<button onclick="updateClickCount()">click me</button>  

现在可能有很多方法,如:

1)您可以使用全局变量,并使用一个函数来增加计数器

var counter = 0;

function updateClickCount() {
    ++counter;
    // do something with counter
}

但是,缺点是页面上的任何脚本都可以更改计数器,而无需调用updateClickCount()

2)现在,您可能正在考虑在函数内声明变量:

function updateClickCount() {
    var counter = 0;
    ++counter;
    // do something with counter
}

但是,嘿!每次调用updateClickCount()函数时,计数器都会再次设置为1。

3)考虑嵌套函数

嵌套函数可以访问“上面”的范围。
在此示例中,内部函数updateClickCount()可以访问父函数countWrapper()中的计数器变量

function countWrapper() {
    var counter = 0;
    function updateClickCount() {
    ++counter;
    // do something with counter
    }
    updateClickCount();    
    return counter; 
}

如果您可以从外部获得updateClickCount()功能,并且您还需要找到一种方法仅执行counter = 0一次,而不是每次都这样做,这可以解决相反的困境。

4)关闭救援! (自我调用功能)

 var updateClickCount=(function(){
    var counter=0;

    return function(){
     ++counter;
     // do something with counter
    }
})();

自调用函数只运行一次。它将counter设置为零(0),并返回一个函数表达式。

这种方式updateClickCount成为一种功能。 “精彩”部分是它可以访问父范围内的计数器。

这称为 JavaScript闭包。它使函数可以拥有“ private ”变量。

counter受匿名函数范围的保护,只能使用add函数进行更改!

Closure的更生动的例子:

  <script>
    var updateClickCount=(function(){
    var counter=0;

    return function(){
    ++counter;
     document.getElementById("spnCount").innerHTML=counter;
    }
  })();
</script>

<html>
 <button onclick="updateClickCount()">click me</button>
  <div> you've clicked 
    <span id="spnCount"> 0 </span> times!
 </div>
</html>

答案 2 :(得分:63)

你给出的例子是一个很好的例子。闭包是一种抽象机制,允许您非常干净地分离关注点。您的示例是从语义(错误报告API)中分离检测(计数调用)的情况。其他用途包括:

  1. 将参数化行为传递给算法(经典的高阶编程):

    function proximity_sort(arr, midpoint) {
        arr.sort(function(a, b) { a -= midpoint; b -= midpoint; return a*a - b*b; });
    }
    
  2. 模拟面向对象的编程:

    function counter() {
        var a = 0;
        return {
            inc: function() { ++a; },
            dec: function() { --a; },
            get: function() { return a; },
            reset: function() { a = 0; }
        }
    }
    
  3. 实现奇异的流控制,例如jQuery的事件处理和AJAX API。

答案 3 :(得分:18)

是的,这是有用关闭的一个很好的例子。对warnUser的调用在其作用域中创建calledCount变量,并返回一个存储在warnForTamper变量中的匿名函数。因为仍然有一个使用calledCount变量的闭包,所以在函数退出时不会删除它,因此每次调用warnForTamper()都会增加作用域变量并提醒值。

我在StackOverflow上看到的最常见的问题是有人想要“延迟”使用在每个循环中增加的变量,但由于变量是作用域的,因此每个对变量的引用都将在循环结束后,导致变量的结束状态:

for (var i = 0; i < someVar.length; i++)
    window.setTimeout(function () { 
        alert("Value of i was "+i+" when this timer was set" )
    }, 10000);

这将导致每个警报显示相同的i值,该值在循环结束时增加。解决方案是创建一个新的闭包,一个单独的变量范围。这可以使用即时执行的匿名函数来完成,该函数接收变量并将其状态存储为参数:

for (var i = 0; i < someVar.length; i++)
    (function (i) {
        window.setTimeout(function () { 
            alert("Value of i was "+i+" when this timer was set" )
        }, 10000);
    })(i); 

答案 4 :(得分:13)

在JavaScript(或任何ECMAScript)语言中,特别是闭包在隐藏功能实现的同时仍然可以显示界面。

例如,假设您正在编写一类日期实用程序方法,并且您希望允许用户按索引查找工作日名称,但您不希望它们能够修改您在引擎盖下使用的名称数组。

var dateUtil = {
  weekdayShort: (function() {
    var days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
    return function(x) {
      if ((x != parseInt(x)) || (x < 1) || (x > 7)) {
        throw new Error("invalid weekday number");
      }
      return days[x - 1];
    };
  }())
};

请注意,days数组可以简单地存储为dateUtil对象的属性,但随后脚本的用户可以看到它们甚至可以根据需要更改它,甚至没有需要你的源代码。但是,由于它由匿名函数包含,它返回日期查找函数,因此只能通过查找函数访问它,因此它现在可以防篡改。

答案 5 :(得分:11)

我知道我在回答这个问题时已经很晚了,但它可能会帮助那些仍然在2018年寻找答案的人。

Javascript闭包可用于在您的应用中实施限制去抖动功能。

<强>节流

限制作为可以随时间调用函数的最大次数的限制。与&#34相同;每100毫秒最多执行一次此功能。&#34;

代码:

const throttle = (func, limit) => {
  let isThrottling
  return function() {
    const args = arguments
    const context = this
    if (!isThrottling) {
      func.apply(context, args)
      isThrottling = true
      setTimeout(() => isThrottling = false, limit)
    }
  }
}

<强>去抖

Debouncing对一个函数设置了一个限制,直到经过一段时间而没有调用它时才会被再次调用。和&#34;只有在没有被调用的情况下经过100毫秒才执行此功能。&#34;

代码:

const debounce = (func, delay) => {
  let debouncing
  return function() {
    const context = this
    const args = arguments
    clearTimeout(debouncing)
    debouncing = setTimeout(() => func.apply(context, args), delay)
  }
}

正如您所看到的,闭包有助于实现两个漂亮的功能,每个Web应用程序都应该提供这些功能,以提供流畅的UI体验功能。

我希望它会帮助别人。

答案 6 :(得分:6)

答案 7 :(得分:5)

闭包的另一个常见用途是将方法中的this绑定到特定对象,允许在其他地方调用它(例如作为事件处理程序)。

function bind(obj, method) {
    if (typeof method == 'string') {
        method = obj[method];
    }
    return function () {
        method.apply(obj, arguments);
    }
}
...
document.body.addEventListener('mousemove', bind(watcher, 'follow'), true);

每当发生mousemove事件时,都会调用watcher.follow(evt)

闭包也是高阶函数的重要组成部分,允许通过参数化不同部分,将多个相似函数重写为单个高阶函数的非常常见的模式。作为一个抽象的例子,

foo_a = function (...) {A a B}
foo_b = function (...) {A b B}
foo_c = function (...) {A c B}

变为

fooer = function (x) {
    return function (...) {A x B}
}

其中A和B不是语法单元而是源代码字符串(不是字符串文字)。

有关具体示例,请参阅“Streamlining my javascript with a function”。

答案 8 :(得分:5)

在这里,我有一个问候,我想多次说。如果我创建一个闭包,我可以简单地调用该函数来记录问候语。如果我不创建闭包,我必须每次都传递我的名字。

没有闭包(https://jsfiddle.net/lukeschlangen/pw61qrow/3/):

function greeting(firstName, lastName) {
  var message = "Hello " + firstName + " " + lastName + "!";
  console.log(message);
}

greeting("Billy", "Bob");
greeting("Billy", "Bob");
greeting("Billy", "Bob");
greeting("Luke", "Schlangen");
greeting("Luke", "Schlangen");
greeting("Luke", "Schlangen");

使用闭包(https://jsfiddle.net/lukeschlangen/Lb5cfve9/3/):

function greeting(firstName, lastName) {
  var message = "Hello " + firstName + " " + lastName + "!";

  return function() {
    console.log(message);
  }
}

var greetingBilly = greeting("Billy", "Bob");
var greetingLuke = greeting("Luke", "Schlangen");

greetingBilly();
greetingBilly();
greetingBilly();
greetingLuke();
greetingLuke();
greetingLuke();

答案 9 :(得分:4)

如果您对在面向对象意义上实例化类的概念感到满意(即创建该类的对象),那么您就很容易理解闭包。

以这种方式思考:当您实例化两个Person对象时,您知道实例之间不共享类成员变量“Name”;每个对象都有自己的“副本”。类似地,当您创建闭包时,自由变量(上例中的'calledCount')绑定到函数的'instance'。

我认为你的概念上的飞跃会因为warnUser函数返回的每个函数/闭包(除了那个是高阶函数)闭包这个事实而受到轻微的阻碍,因为它使用相同的初始值绑定'calledCount' value(0),而通常在创建闭包时,将不同的初始化器传递给高阶函数更有用,就像将不同的值传递给类的构造函数一样。

因此,假设当'calledCount'达到某个值时,您希望结束用户的会话;您可能需要不同的值,具体取决于请求是来自本地网络还是来自大型恶意互联网(是的,这是一个人为的例子)。要实现这一点,您可以将calledCount的不同初始值传递给warnUser(即-3或0?)。

文献中的部分问题是用于描述它们的命名法(“词汇范围”,“自由变量”)。不要让它欺骗你,封闭比看起来更简单......表面上看起来; - )

答案 10 :(得分:2)

这里我有一个简单的闭包概念示例,我们可以在我们的电子商务网站或许多其他网站中使用它。 我正在添加我的jsfiddle链接示例。 它包含一个包含3个项目和一个购物车柜台的小型产品清单。

Jsfiddle

//Counter clouser implemented function;
var CartCouter = function(){
	var counter = 0;
  function changeCounter(val){
  	counter += val
  }
  return {
  	increment: function(){
    	changeCounter(1);
    },
    decrement: function(){
    changeCounter(-1);
    },
    value: function(){
    return counter;
    }
  }
}

var cartCount = CartCouter();
function updateCart(){
	document.getElementById('cartcount').innerHTML = cartCount.value();
  }

var productlist = document.getElementsByClassName('item');
for(var i = 0; i< productlist.length; i++){
	productlist[i].addEventListener('click',function(){
  	if(this.className.indexOf('selected')<0){
    		this.className += " selected";
        cartCount.increment();
        updateCart();
    } else{
    	this.className = this.className.replace("selected", "");
      cartCount.decrement();
      updateCart();
    }
  })
}
.productslist{
  padding:10px;
}
ul li{
  display: inline-block;
  padding: 5px;
  border: 1px solid #ddd;
  text-align: center;
  width: 25%;
  cursor: pointer;
}
.selected{
  background-color: #7CFEF0;
  color: #333;
}
.cartdiv{
  position: relative;
  float:right;
  padding: 5px;
  box-sizing: border-box;
  border: 1px solid #f1f1f1;
}
<div>
<h3>
Practical Use of JavaScript Closure consept/private variable.
</h3>
<div class="cartdiv">
    <span id="cartcount">0</span>
</div>
<div class="productslist">
    <ul >
    <li class="item">Product 1</li>
     <li class="item">Product 2</li>
     <li class="item">Product 3</li>
    </ul>

</div>
</div>

答案 11 :(得分:1)

闭包有多种用例。在这里,我将解释闭包概念的最重要用法。

  • Closure 可用于创建私有方法和变量,就像 Java、c++ 等面向对象的语言一样。一旦你实现了私有方法和变量,你在函数中定义的变量就不能被 window 对象访问。这有助于数据隐藏和数据安全。
const privateClass = () => {
  let name = "sundar";
  function setName(changeName) {
    name = changeName;
  }
  function getName() {
    return name;
  }
  return {
    setName: setName,
    getName: getName,
  };
};

let javaLikeObject = privateClass(); \\ similar to new Class() in OOPS.

console.log(javaLikeObject.getName()); \\this will give sundar
javaLikeObject.setName("suresh");
console.log(javaLikeObject.getName()); \\this will give suresh

  • 另一个现实生活中的闭包例子:

创建 index.html:

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Program with Javascript</title>
  </head>
  <body>
    <p id="first"></p>
    <p id="second"></p>
    <button onclick="applyingConcepts()">Click</button>
    <script src="./index.js"></script>
  </body>
</html>

2)在 index.js 中:

  let count = 0;
  return () => {
    document.getElementById("first").innerHTML = count++;
  };
})();

  • 在此示例中,当您单击按钮时,您的计数将在 p#id 上更新。 注意:您可能想知道这段代码有什么特别之处。当您检查时,您会注意到您无法使用 window 对象更改 count 的值。这意味着您已声明私有变量 count 以防止您的状态被客户端破坏。

答案 12 :(得分:1)

解释JavaScript中闭包的实际用途

当我们在另一个函数中创建一个函数时,我们正在创建一个闭包。闭包很强大,因为它们能够读取和操作其外部功能的数据。每当调用一个函数时,都会为该调用创建一个新的作用域。在函数内部声明的局部变量属于该范围,并且只能从该函数访问它们。函数完成执行后,通常会破坏作用域。

这种功能的一个简单例子是:

function buildName(name) {
    const greeting = "Hello, " + name;
    return greeting;
}

在上面的示例中,函数buildName()声明一个局部变量greeting并返回它。每个函数调用都会使用新的局部变量创建一个新的作用域。函数执行完后,我们无法再次引用该作用域,因此已被垃圾回收。

但是,当我们有指向该范围的链接时呢?

让我们看看下一个功能:

function buildName(name) {
    const greeting = "Hello, " + name + " Welcome ";
    const sayName = function() {
        console.log(greeting);
    };
    return sayName;
}

const sayMyName = buildName("Mandeep");
sayMyName();  // Hello, Mandeep Welcome

此示例中的函数sayName()是一个闭包。 sayName()函数有自己的本地范围(带有变量welcome),并且还可以访问外部(封闭的)函数的范围。在这种情况下,来自buildName()的变量greeting。

在执行buildName之后,在这种情况下不会破坏作用域。 sayMyName()函数仍然可以访问它,因此不会被垃圾回收。但是,除了闭包之外,没有其他方法可以从外部范围访问数据。闭包充当全局上下文和外部范围之间的门户。

答案 13 :(得分:1)

这个主题帮助我更好地理解了闭包的工作原理。我已经做了一些自己的实验,并提出了这个相当简单的代码,可以帮助其他人看到如何以实用的方式使用闭包,以及如何在不同的级别使用闭包来维护类似于静态的变量/或全局变量,没有被全局变量覆盖或混淆的风险。它的作用是跟踪每个按钮和全局级别的本地级别的按钮点击次数,计算每个按钮的点击次数,为单个数字做出贡献。注意我没有使用任何全局变量来执行此操作,这是练习的重点 - 具有可应用于任何按钮的处理程序,该按钮也有助于全局变量。

请专家,如果我在这里犯了任何不良行为,请告诉我!我自己还在学习这些东西。

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Closures on button presses</title>
<script type="text/javascript">

window.addEventListener("load" , function () {
    /*
    grab the function from the first closure,
    and assign to a temporary variable 
    this will set the totalButtonCount variable
    that is used to count the total of all button clicks

    */
    var buttonHandler = buttonsCount(); 

    /*
    using the result from the first closure (a function is returned) 
    assign and run the sub closure that carries the 
    individual variable for button count and assign to the click handlers 
    */
    document.getElementById("button1").addEventListener("click" , buttonHandler() );
    document.getElementById("button2").addEventListener("click" , buttonHandler() );
    document.getElementById("button3").addEventListener("click" , buttonHandler() );

    // Now that buttonHandler has served its purpose it can be deleted if needs be
    buttonHandler = null;
});



function buttonsCount() {
    /* 
        First closure level 
        - totalButtonCount acts as a sort of global counter to count any button presses
    */
    var totalButtonCount = 0;

    return  function () {
        //second closure level
        var myButtonCount = 0;

        return function (event) {
            //actual function that is called on the button click
            event.preventDefault();
            /*  
               increment the button counts.
               myButtonCount only exists in the scope that is 
               applied to each event handler, therefore acts 
               to count each button individually whereas because 
               of the first closure totalButtonCount exists at 
               the scope just outside, so maintains a sort 
               of static or global variable state 
            */

            totalButtonCount++;
            myButtonCount++;

            /* 
                do something with the values ... fairly pointless 
                but it shows that each button contributes to both 
                it's own variable and the outer variable in the 
                first closure 
            */
            console.log("Total button clicks: "+totalButtonCount);
            console.log("This button count: "+myButtonCount);
        }
    }
}

</script>
</head>

<body>
    <a href="#" id="button1">Button 1</a>
    <a href="#" id="button2">Button 2</a>
    <a href="#" id="button3">Button 3</a>
</body>
</html>

答案 14 :(得分:1)

JavaScript模块模式使用闭包。它漂亮的图案让你有类似的东西和#34;公共&#34;和#34;私人&#34;瓦尔。

var myNamespace = (function () {

  var myPrivateVar, myPrivateMethod;

  // A private counter variable
  myPrivateVar = 0;

  // A private function which logs any arguments
  myPrivateMethod = function( foo ) {
      console.log( foo );
  };

  return {

    // A public variable
    myPublicVar: "foo",

    // A public function utilizing privates
    myPublicFunction: function( bar ) {

      // Increment our private counter
      myPrivateVar++;

      // Call our private method using bar
      myPrivateMethod( bar );

    }
  };

})();

答案 15 :(得分:1)

我喜欢Mozilla的功能工厂example

name<-list.files("C:/path/to/file/")[grep("foot",list.files(path = "c:/path/to/file/"),ignore.case = T)]

答案 16 :(得分:1)

使用闭包

闭包是JavaScript最强大的功能之一。 JavaScript允许嵌套函数并授予内部函数对外部函数内定义的所有变量和函数(以及外部函数可以访问的所有其他变量和函数)的完全访问权限。但是,外部函数无法访问内部函数内定义的变量和函数。这为内部函数的变量提供了一种安全性。此外,由于内部函数可以访问外部函数的范围,因此如果内部函数设法超出外部函数的寿命,则外部函数中定义的变量和函数将比外部函数本身更长寿命。当内部函数以某种方式可用于外部函数之外的任何作用域时,将创建闭包。

示例:

<script>
var createPet = function(name) {
  var sex;

  return {
    setName: function(newName) {
      name = newName;
    },

    getName: function() {
      return name;
    },

    getSex: function() {
      return sex;
    },

    setSex: function(newSex) {
      if(typeof newSex == "string" && (newSex.toLowerCase() == "male" || newSex.toLowerCase() == "female")) {
        sex = newSex;
      }
    }
  }
}

var pet = createPet("Vivie");
console.log(pet.getName());                  // Vivie

console.log(pet.setName("Oliver"));   
console.log(pet.setSex("male"));
console.log(pet.getSex());                   // male
console.log(pet.getName());                  // Oliver
</script>

在上面的代码中,外部函数的name变量可以被内部函数访问,除了通过内部函数之外没有其他方法可以访问内部变量。内部函数的内部变量充当内部函数的安全存储。他们持有&#34;持久的#34;但安全的数据,供内部函数使用。甚至不必将函数分配给变量或具有名称。 请阅读here了解详情

答案 17 :(得分:1)

我前段时间写了一篇关于如何使用闭包来简化事件处理代码的文章。它将ASP.NET事件处理与客户端jQuery进行了比较。

http://www.hackification.com/2009/02/20/closures-simplify-event-handling-code/

答案 18 :(得分:0)

  

我们在前端JavaScript中编写的大部分代码都是基于事件的 - 我们定义了一些行为,然后将其附加到用户触发的事件(例如点击或按键)。我们的代码通常作为回调附加:单个函数,它是为响应事件而执行的。   size12,size14和size16现在是将正文文本分别调整为12,14和16像素的函数。我们可以将它们附加到按钮(在本例中为链接),如下所示:

function makeSizer(size) {
    return function() {
    document.body.style.fontSize = size + 'px';
    };
}

var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);

document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;

Fiddle

答案 19 :(得分:0)

参考:Practical usage of closures

实际上,闭包可以创建优雅的设计,允许自定义各种计算,延迟调用,回调,创建封装范围等。

数组的排序方法示例,它接受排序条件函数作为参数:

[1, 2, 3].sort(function (a, b) {
    ... // sort conditions
});

将函数映射为数组的map方法,该函数根据函数参数的条件映射新数组:

[1, 2, 3].map(function (element) {
   return element * 2;
}); // [2, 4, 6]

通常使用定义几乎无限制的搜索条件的函数参数来实现搜索功能是很方便的:

 someCollection.find(function (element) {
        return element.someProperty == 'searchCondition';
    });

另外,我们可能会注意应用函数作为例如将函数应用于元素数组的forEach方法:

[1, 2, 3].forEach(function (element) {
    if (element % 2 != 0) {
        alert(element);
    }
}); // 1, 3

一个函数应用于参数(在参数列表中 - 在应用中,在定位参数中 - 在调用中):

(function () {
  alert([].join.call(arguments, ';')); // 1;2;3
}).apply(this, [1, 2, 3]);

延期电话:

var a = 10;
    setTimeout(function () {
      alert(a); // 10, after one second
    }, 1000);

回调函数:

var x = 10;
// only for example
xmlHttpRequestObject.onreadystatechange = function () {
  // callback, which will be called deferral ,
  // when data will be ready;
  // variable "x" here is available,
  // regardless that context in which,
  // it was created already finished
  alert(x); // 10
};

为隐藏辅助对象而创建封装范围:

var foo = {};
(function (object) {
  var x = 10;
  object.getX = function _getX() {
    return x;
  };
})(foo);
alert(foo.getX());// get closured "x" – 10

答案 20 :(得分:0)

在给定的样本中,封闭变量&#39;计数器的值为&#39;受保护,只能使用给定的函数(递增,递减)进行更改。因为它处于封闭状态,

&#13;
&#13;
var MyCounter= function (){
    var counter=0;
    return {
    	increment:function () {return counter += 1;},
        decrement:function () {return counter -= 1;},
        get:function () {return counter;}
    };
};

var x = MyCounter();
//or
var y = MyCounter();

alert(x.get());//0
alert(x.increment());//1
alert(x.increment());//2

alert(y.increment());//1
alert(x.get());// x is still 2
&#13;
&#13;
&#13;

答案 21 :(得分:0)

闭包是创建的有用方法,按顺序递增序列:

    var foobar = function(i){var count = count || i; return function(){return ++count;}}

    baz = foobar(1);
    console.log("first call: " + baz()); //2
    console.log("second call: " + baz()); //3

差异总结如下:

Anonymous functions                                    Defined functions

Cannot be used as a method                             Can be used as a method of an object

Exists only in the scope in which it is defined        Exists within the object it is defined in

Can only be called in the scope in which it is defined Can be called at any point in the code

Can be reassigned a new value or deleted               Cannot be deleted or changed

<强>参考

答案 22 :(得分:0)

我正在尝试学习闭包,我认为我创建的示例是一个实际的用例。您可以运行一个代码段并在控制台中查看结果。

我们有两个拥有独立数据的独立用户。他们每个人都可以看到实际状态并进行更新。

<form method="get">
    {{ filtered_person_list.form.as_p }}
    <button type="submit">Search</button>
</form>

{% for person in person_page_obj %}
    <img src="{{ person.picture.url }}" width="240">
    <h2>
        {{person.name}}
        <br>
        {{person.gender}}
        <br>
        {{person.category}}
        <br>
    </h2>
    <br>
{% endfor %}

<div class="pagination">
    <span class="step-links">
        {% if person_page_obj.has_previous %}
            <a href="?page=1">&laquo; first</a>
            <a href="?page={{ person_page_obj.previous_page_number }}">previous</a>
        {% endif %}

        <span class="current">
            Page {{ person_page_obj.number }} of {{ person_page_obj.paginator.num_pages }}.
        </span>

        {% if person_page_obj.has_next %}
            <a href="?page={{ person_page_obj.next_page_number }}">next</a>
            <a href="?page={{ person_page_obj.paginator.num_pages }}">last &raquo;</a>
        {% endif %}
    </span>
</div>